Skip to content

Commit 183067b

Browse files
authored
Merge pull request #113 from atomic-ehr/refactoring-typeschema-details
TypeSchema: Refactor type system for discriminated unions and type-safe resolution
2 parents dab78af + 1045526 commit 183067b

21 files changed

Lines changed: 228 additions & 149 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ FHIR Package → TypeSchema Generator → TypeSchema Format → Code Generators
123123
- In code generators (writer-generator): use `curlyBlock` and `squareBlock` helpers for writing structured output instead of manual indent/deindent or string concatenation
124124
- Use `Record` instead of `Map` unless there is a significant reason for `Map` (e.g. non-string keys, iteration order guarantees, frequent deletion)
125125
- Prefer single-line guard clauses without braces: `if (!x) throw new Error("...");` instead of wrapping in `{ }`
126+
- Do not check `kind` of `Identifier`/`TypeIdentifier`/`TypeSchema` by manually comparing the `kind` field. Use dedicated predicates (`isPrimitiveIdentifier`, `isSpecializationTypeSchema`, etc.)
126127

127128
### Testing Strategy
128129
- Uses Bun's built-in test runner

docs/guides/typeschema-index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ Why TypeSchemaIndex Matters
4848
Query schemas by type category:
4949

5050
```typescript
51-
collectComplexTypes(): SpecializationTypeSchema[]
51+
collectComplexTypes(): ComplexTypeTypeSchema[]
5252
Returns all complex types (datatypes, backbone elements)
5353

54-
collectResources(): SpecializationTypeSchema[]
54+
collectResources(): ResourceTypeSchema[]
5555
Returns all FHIR resources (Patient, Observation, etc.)
5656

57-
collectLogicalModels(): SpecializationTypeSchema[]
57+
collectLogicalModels(): LogicalTypeSchema[]
5858
Returns all logical models
5959

6060
collectProfiles(): ProfileTypeSchema[]

examples/typescript-r4/fhir-types/type-tree.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,7 @@ hl7.fhir.r4.core:
6464
http://hl7.org/fhir/StructureDefinition/Patient: {}
6565
http://hl7.org/fhir/StructureDefinition/Resource: {}
6666
value-set: {}
67-
nested:
68-
http://hl7.org/fhir/StructureDefinition/Observation#component: {}
69-
http://hl7.org/fhir/StructureDefinition/Observation#referenceRange: {}
67+
nested: {}
7068
binding: {}
7169
profile:
7270
http://hl7.org/fhir/StructureDefinition/patient-birthPlace: {}

examples/typescript-us-core/fhir-types/type-tree.yaml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,7 @@ hl7.fhir.r4.core:
8585
http://hl7.org/fhir/StructureDefinition/Patient: {}
8686
http://hl7.org/fhir/StructureDefinition/Resource: {}
8787
value-set: {}
88-
nested:
89-
http://hl7.org/fhir/StructureDefinition/Patient#communication: {}
90-
http://hl7.org/fhir/StructureDefinition/Observation#component: {}
91-
http://hl7.org/fhir/StructureDefinition/Observation#referenceRange: {}
88+
nested: {}
9289
binding: {}
9390
profile:
9491
http://hl7.org/fhir/StructureDefinition/vitalsigns: {}

src/api/mustache/generator/ViewModelFactory.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import type { IsPrefixed } from "@root/utils/types";
1515
import {
1616
type ChoiceFieldInstance,
1717
type Field,
18-
isComplexTypeIdentifier,
18+
isComplexTypeTypeSchema,
1919
isNotChoiceDeclarationField,
20-
isResourceIdentifier,
20+
isResourceTypeSchema,
2121
type NestedTypeSchema,
2222
type RegularField,
2323
type TypeIdentifier,
@@ -100,7 +100,7 @@ export class ViewModelFactory {
100100
cache: ViewModelCache,
101101
nestedIn?: TypeSchema,
102102
): TypeViewModel {
103-
const type = this.tsIndex.resolve(typeRef);
103+
const type = this.tsIndex.resolveType(typeRef);
104104
if (!type) {
105105
throw new Error(`ComplexType ${typeRef.name} not found`);
106106
}
@@ -113,7 +113,7 @@ export class ViewModelFactory {
113113
}
114114

115115
private _createForResource(typeRef: TypeIdentifier, cache: ViewModelCache, nestedIn?: TypeSchema): TypeViewModel {
116-
const type = this.tsIndex.resolve(typeRef);
116+
const type = this.tsIndex.resolveType(typeRef);
117117
if (!type) {
118118
throw new Error(`Resource ${typeRef.name} not found`);
119119
}
@@ -126,14 +126,14 @@ export class ViewModelFactory {
126126
}
127127

128128
private _createChildrenFor(typeRef: TypeIdentifier, cache: ViewModelCache, nestedIn?: TypeSchema): TypeViewModel[] {
129-
const schema = this.tsIndex.resolve(typeRef);
130-
if (!schema || !("typeFamily" in schema)) return [];
131-
if (isComplexTypeIdentifier(typeRef)) {
129+
const schema = this.tsIndex.resolveType(typeRef);
130+
if (!schema) return [];
131+
if (isComplexTypeTypeSchema(schema)) {
132132
return (schema.typeFamily?.complexTypes ?? [])
133133
.filter(this.filterPred)
134134
.map((childRef: TypeIdentifier) => this._createFor(childRef, cache, nestedIn));
135135
}
136-
if (isResourceIdentifier(typeRef)) {
136+
if (isResourceTypeSchema(schema)) {
137137
return (schema.typeFamily?.resources ?? [])
138138
.filter(this.filterPred)
139139
.map((childRef: TypeIdentifier) => this._createFor(childRef, cache, nestedIn));
@@ -146,7 +146,7 @@ export class ViewModelFactory {
146146
let parentRef: TypeIdentifier | undefined = "base" in base ? base.base : undefined;
147147
while (parentRef) {
148148
parents.push(this._createFor(parentRef, cache, undefined));
149-
const parent = this.tsIndex.resolve(parentRef);
149+
const parent = this.tsIndex.resolveType(parentRef);
150150
parentRef = parent && "base" in parent ? parent.base : undefined;
151151
}
152152
return parents;

src/api/writer-generator/csharp/csharp.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Field, RegularField, TypeIdentifier } from "@typeschema/types";
88
import {
99
type ChoiceFieldInstance,
1010
isChoiceDeclarationField,
11+
type NestedTypeSchema,
1112
type SpecializationTypeSchema,
1213
} from "@typeschema/types.ts";
1314
import type { TypeSchemaIndex } from "@typeschema/utils.ts";
@@ -53,12 +54,12 @@ const getFieldModifiers = (field: Field) => {
5354
return field.required ? ["required"] : [];
5455
};
5556

56-
const formatClassName = (schema: SpecializationTypeSchema) => {
57+
const formatClassName = (schema: SpecializationTypeSchema | NestedTypeSchema) => {
5758
const name = prefixReservedTypeName(getResourceName(schema.identifier));
5859
return uppercaseFirstLetter(name);
5960
};
6061

61-
const formatBaseClass = (schema: SpecializationTypeSchema) => {
62+
const formatBaseClass = (schema: SpecializationTypeSchema | NestedTypeSchema) => {
6263
return schema.base ? `: ${schema.base.name}` : "";
6364
};
6465

@@ -138,7 +139,7 @@ export class CSharp extends Writer<CSharpGeneratorOptions> {
138139
this.generateHelperFile();
139140
}
140141

141-
private generateType(schema: SpecializationTypeSchema, packageName: string): void {
142+
private generateType(schema: SpecializationTypeSchema | NestedTypeSchema, packageName: string): void {
142143
const className = formatClassName(schema);
143144
const baseClass = formatBaseClass(schema);
144145

@@ -151,7 +152,7 @@ export class CSharp extends Writer<CSharpGeneratorOptions> {
151152
this.line();
152153
}
153154

154-
private generateFields(schema: SpecializationTypeSchema, packageName: string): void {
155+
private generateFields(schema: SpecializationTypeSchema | NestedTypeSchema, packageName: string): void {
155156
if (!schema.fields) return;
156157

157158
const sortedFields = Object.entries(schema.fields).sort(([a], [b]) => a.localeCompare(b));
@@ -161,7 +162,7 @@ export class CSharp extends Writer<CSharpGeneratorOptions> {
161162
}
162163
}
163164

164-
private generateNestedTypes(schema: SpecializationTypeSchema, packageName: string): void {
165+
private generateNestedTypes(schema: SpecializationTypeSchema | NestedTypeSchema, packageName: string): void {
165166
if (!("nested" in schema) || !schema.nested) return;
166167

167168
this.line();

src/api/writer-generator/python.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type EnumDefinition,
1010
type Field,
1111
isResourceTypeSchema,
12+
type NestedTypeSchema,
1213
type SpecializationTypeSchema,
1314
type TypeIdentifier,
1415
} from "@typeschema/types.ts";
@@ -409,7 +410,7 @@ export class Python extends Writer<PythonGeneratorOptions> {
409410
this.pyImportFrom(`${this.opts.rootPackageName}.fhirpy_base_model`, "FhirpyBaseModel");
410411
}
411412

412-
private generateType(schema: SpecializationTypeSchema): void {
413+
private generateType(schema: SpecializationTypeSchema | NestedTypeSchema): void {
413414
const className = deriveResourceName(schema.identifier);
414415
const superClasses = this.getSuperClasses(schema);
415416

@@ -420,15 +421,15 @@ export class Python extends Writer<PythonGeneratorOptions> {
420421
this.line();
421422
}
422423

423-
private getSuperClasses(schema: SpecializationTypeSchema): string[] {
424+
private getSuperClasses(schema: SpecializationTypeSchema | NestedTypeSchema): string[] {
424425
const bases: string[] = [];
425426
if (schema.base) bases.push(schema.base.name);
426427
bases.push(...this.injectSuperClasses(schema.identifier.url));
427428
if (schema.identifier.name in GENERIC_FIELD_REWRITES) bases.push("Generic[T]");
428429
return bases;
429430
}
430431

431-
private generateClassBody(schema: SpecializationTypeSchema): void {
432+
private generateClassBody(schema: SpecializationTypeSchema | NestedTypeSchema): void {
432433
this.generateModelConfig();
433434

434435
if (!schema.fields) {
@@ -473,7 +474,7 @@ export class Python extends Writer<PythonGeneratorOptions> {
473474
this.line(")");
474475
}
475476

476-
private generateFields(schema: SpecializationTypeSchema, schemaName: string): void {
477+
private generateFields(schema: SpecializationTypeSchema | NestedTypeSchema, schemaName: string): void {
477478
const sortedFields = Object.entries(schema.fields ?? []).sort(([a], [b]) => a.localeCompare(b));
478479

479480
for (const [fieldName, field] of sortedFields) {

src/api/writer-generator/typescript/profile-slices.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type { TypeScript } from "./writer";
2222
/** Collect choice declaration field names from a base type schema */
2323
const collectChoiceBaseNames = (tsIndex: TypeSchemaIndex, typeId: TypeIdentifier): Set<string> => {
2424
const names = new Set<string>();
25-
const schema = tsIndex.resolve(typeId);
25+
const schema = tsIndex.resolveType(typeId);
2626
if (schema && "fields" in schema && schema.fields) {
2727
for (const [name, f] of Object.entries(schema.fields)) {
2828
if (isChoiceDeclarationField(f)) names.add(name);

src/api/writer-generator/typescript/profile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ const collectBaseRequiredParams = (
168168
coveredNames: string[],
169169
) => {
170170
const covered = new Set(coveredNames);
171-
const baseSchema = tsIndex.resolve(flatProfile.base);
171+
const baseSchema = tsIndex.resolveType(flatProfile.base);
172172
if (!baseSchema || !("fields" in baseSchema) || !baseSchema.fields) return;
173173
for (const [name, field] of Object.entries(baseSchema.fields)) {
174174
if (covered.has(name)) continue;

src/api/writer-generator/typescript/writer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
isProfileTypeSchema,
1212
isResourceTypeSchema,
1313
isSpecializationTypeSchema,
14+
type NestedTypeSchema,
1415
packageMeta,
1516
packageMetaToFhir,
1617
type SpecializationTypeSchema,
@@ -198,7 +199,7 @@ export class TypeScript extends Writer<TypeScriptOptions> {
198199
this.lineSM(`${extFieldName}?: ${typeExpr}`);
199200
}
200201

201-
generateType(tsIndex: TypeSchemaIndex, schema: SpecializationTypeSchema) {
202+
generateType(tsIndex: TypeSchemaIndex, schema: SpecializationTypeSchema | NestedTypeSchema) {
202203
let name: string;
203204
// Generic types: Reference, Coding, CodeableConcept
204205
const genericTypes = ["Reference", "Coding", "CodeableConcept"];
@@ -212,7 +213,7 @@ export class TypeScript extends Writer<TypeScriptOptions> {
212213
const typeFamilyFields: { fieldName: string; familyTypeName: string }[] = [];
213214
for (const [fieldName, field] of Object.entries(schema.fields ?? {})) {
214215
if (isChoiceDeclarationField(field) || !field.type) continue;
215-
const fieldTypeSchema = tsIndex.resolve(field.type);
216+
const fieldTypeSchema = tsIndex.resolveType(field.type);
216217
if (
217218
isSpecializationTypeSchema(fieldTypeSchema) &&
218219
(fieldTypeSchema.typeFamily?.resources?.length ?? 0) > 0
@@ -285,7 +286,7 @@ export class TypeScript extends Writer<TypeScriptOptions> {
285286
});
286287
}
287288

288-
withPrimitiveTypeExtension(schema: TypeSchema): boolean {
289+
withPrimitiveTypeExtension(schema: TypeSchema | NestedTypeSchema): boolean {
289290
if (!this.opts.primitiveTypeExtension) return false;
290291
if (!isSpecializationTypeSchema(schema)) return false;
291292
for (const field of Object.values(schema.fields ?? {})) {

0 commit comments

Comments
 (0)