Skip to content

Commit 774c4ba

Browse files
committed
chore: early draft for profiles
1 parent 8beba25 commit 774c4ba

14 files changed

Lines changed: 1507 additions & 6 deletions

File tree

src/api/builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
import type { Config, TypeSchemaConfig } from "../config";
2525
import { CodegenLogger, createLogger } from "../utils/codegen-logger";
2626
import { TypeScriptGenerator as TypeScriptGeneratorDepricated } from "./generators/typescript";
27-
import * as TS2 from "./writer-generator/typescript";
27+
import * as TS2 from "./writer-generator/typescript/index";
2828
import type { Writer, WriterOptions } from "./writer-generator/writer";
2929
import type { GeneratorInput } from "./generators/base/BaseGenerator";
3030

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type { TypeSchemaIndex } from "@root/typeschema/utils";
2+
import type { Field, Identifier, RegularField } from "@root/typeschema/types";
3+
import {
4+
isChoiceDeclarationField,
5+
isNestedIdentifier,
6+
isPrimitiveIdentifier,
7+
} from "@root/typeschema/types";
8+
import { resolvePrimitiveType, tsResourceName } from "./utils";
9+
10+
/**
11+
* Resolve TypeScript type for a field
12+
*/
13+
export function resolveFieldType(
14+
field: Field,
15+
tsIndex: TypeSchemaIndex,
16+
): string {
17+
if (isChoiceDeclarationField(field)) {
18+
// Choice declarations don't have a single type
19+
// They are represented by their choice instances
20+
return "unknown"; // This shouldn't be used in practice
21+
}
22+
23+
return resolveIdentifierType(field.type, tsIndex);
24+
}
25+
26+
/**
27+
* Resolve TypeScript type from Identifier
28+
*/
29+
export function resolveIdentifierType(
30+
identifier: Identifier,
31+
tsIndex: TypeSchemaIndex,
32+
): string {
33+
// Check if it's a primitive type
34+
if (isPrimitiveIdentifier(identifier)) {
35+
return resolvePrimitiveType(identifier.name);
36+
}
37+
38+
// Nested or complex type
39+
if (isNestedIdentifier(identifier)) {
40+
return tsResourceName(identifier);
41+
}
42+
43+
// Default: use name directly
44+
return identifier.name;
45+
}
46+
47+
/**
48+
* Check if field should be optional
49+
*/
50+
export function isFieldOptional(field: Field): boolean {
51+
if (isChoiceDeclarationField(field)) {
52+
// Choice declarations are optional if no choice is required
53+
return !field.required;
54+
}
55+
return !field.required;
56+
}
57+
58+
/**
59+
* Get field cardinality constraints
60+
*/
61+
export interface CardinalityConstraint {
62+
min: number;
63+
max: number | undefined;
64+
}
65+
66+
export function getFieldCardinality(field: RegularField): CardinalityConstraint {
67+
return {
68+
min: field.min ?? 0,
69+
max: field.max,
70+
};
71+
}
72+
73+
/**
74+
* Check if field has cardinality constraints
75+
*/
76+
export function hasCardinalityConstraints(field: RegularField): boolean {
77+
return (
78+
(field.min !== undefined && field.min > 0) || field.max !== undefined
79+
);
80+
}
81+
82+
/**
83+
* Check if field is excluded (max=0)
84+
*/
85+
export function isFieldExcluded(field: RegularField): boolean {
86+
return field.excluded === true || field.max === 0;
87+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { kebabCase, pascalCase, uppercaseFirstLetter } from "@root/api/writer-generator/utils";
2+
import type { Writer } from "@root/api/writer-generator/writer";
3+
import type { RegularTypeSchema, TypeSchema } from "@root/typeschema/types";
4+
import { isNestedIdentifier } from "@root/typeschema/types";
5+
import { canonicalToName, tsResourceName } from "./utils";
6+
7+
/**
8+
* Generate import statements for dependencies
9+
*/
10+
export function generateDependenciesImports(
11+
writer: Writer,
12+
schema: RegularTypeSchema,
13+
): void {
14+
if (!schema.dependencies || schema.dependencies.length === 0) {
15+
return;
16+
}
17+
18+
const imports = [];
19+
const skipped = [];
20+
21+
for (const dep of schema.dependencies) {
22+
if (["complex-type", "resource", "logical"].includes(dep.kind)) {
23+
imports.push({
24+
tsPackage: `../${kebabCase(dep.package)}/${pascalCase(dep.name)}`,
25+
name: uppercaseFirstLetter(dep.name),
26+
dep: dep,
27+
});
28+
} else if (isNestedIdentifier(dep)) {
29+
imports.push({
30+
tsPackage: `../${kebabCase(dep.package)}/${pascalCase(canonicalToName(dep.url) ?? "")}`,
31+
name: tsResourceName(dep),
32+
dep: dep,
33+
});
34+
} else {
35+
skipped.push(dep);
36+
}
37+
}
38+
39+
// Sort imports by name for consistent output
40+
imports.sort((a, b) => a.name.localeCompare(b.name));
41+
42+
for (const imp of imports) {
43+
writer.debugComment(imp.dep);
44+
writer.lineSM(
45+
`import type { ${imp.name} } from "${imp.tsPackage}"`,
46+
);
47+
}
48+
49+
for (const dep of skipped) {
50+
writer.debugComment("skip:", dep);
51+
}
52+
53+
writer.line();
54+
}
55+
56+
/**
57+
* Generate re-exports for complex types
58+
*/
59+
export function generateComplexTypeReexports(
60+
writer: Writer,
61+
schema: RegularTypeSchema,
62+
): void {
63+
const complexTypeDeps = schema.dependencies
64+
?.filter((dep) => dep.kind === "complex-type")
65+
.map((dep) => ({
66+
tsPackage: `../${kebabCase(dep.package)}/${pascalCase(dep.name)}`,
67+
name: uppercaseFirstLetter(dep.name),
68+
}));
69+
70+
if (complexTypeDeps && complexTypeDeps.length > 0) {
71+
for (const dep of complexTypeDeps) {
72+
writer.lineSM(`export type { ${dep.name} } from "${dep.tsPackage}"`);
73+
}
74+
writer.line();
75+
}
76+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { pascalCase, uppercaseFirstLetter } from "@root/api/writer-generator/utils";
2+
import { Writer, type WriterOptions } from "@root/api/writer-generator/writer";
3+
import type {
4+
Identifier,
5+
ProfileTypeSchema,
6+
RegularTypeSchema,
7+
TypeSchema,
8+
} from "@root/typeschema/types";
9+
import { isProfileTypeSchema } from "@root/typeschema/types";
10+
import { groupByPackages, type TypeSchemaIndex } from "@root/typeschema/utils";
11+
import {
12+
generateComplexTypeReexports,
13+
generateDependenciesImports,
14+
} from "./imports";
15+
import {
16+
generateAttachProfile,
17+
generateExtractProfile,
18+
generateProfileType,
19+
} from "./profile";
20+
import { generateProfileAdapter } from "./profile-adapter";
21+
import { generateProfileFactory } from "./profile-factory";
22+
import { generateNestedTypes, generateType } from "./resource";
23+
import {
24+
tsFhirPackageDir,
25+
tsModuleFileName,
26+
tsModuleName,
27+
tsResourceName,
28+
} from "./utils";
29+
30+
export type TypeScriptOptions = WriterOptions;
31+
32+
/**
33+
* TypeScript code generator
34+
* Extends Writer base class for file system operations
35+
*/
36+
export class TypeScript extends Writer {
37+
/**
38+
* Generate package index file (exports all types)
39+
*/
40+
generateFhirPackageIndexFile(schemas: TypeSchema[]): void {
41+
this.cat("index.ts", () => {
42+
let exports = schemas
43+
.map((schema) => ({
44+
identifier: schema.identifier,
45+
tsPackageName: tsModuleName(schema.identifier),
46+
resourceName: tsResourceName(schema.identifier),
47+
}))
48+
.sort((a, b) => a.resourceName.localeCompare(b.resourceName));
49+
50+
// FIXME: actually, duplication may mean internal error...
51+
exports = Array.from(
52+
new Map(
53+
exports.map((exp) => [exp.resourceName.toLowerCase(), exp]),
54+
).values(),
55+
).sort((a, b) => a.resourceName.localeCompare(b.resourceName));
56+
57+
for (const exp of exports) {
58+
this.debugComment(exp.identifier);
59+
this.lineSM(
60+
`export type { ${exp.resourceName} } from "./${exp.tsPackageName}"`,
61+
);
62+
}
63+
});
64+
}
65+
66+
/**
67+
* Generate a single resource/profile module file
68+
*/
69+
generateResourceModule(tsIndex: TypeSchemaIndex, schema: TypeSchema): void {
70+
this.cat(tsModuleFileName(schema.identifier), () => {
71+
this.generateDisclaimer();
72+
73+
if (["complex-type", "resource", "logical"].includes(schema.identifier.kind)) {
74+
this.generateResourceFile(tsIndex, schema as RegularTypeSchema);
75+
} else if (isProfileTypeSchema(schema)) {
76+
this.generateProfileFile(tsIndex, schema);
77+
} else {
78+
throw new Error(
79+
`Profile generation not implemented for kind: ${schema.identifier.kind}`,
80+
);
81+
}
82+
});
83+
}
84+
85+
/**
86+
* Generate resource/complex type file
87+
*/
88+
private generateResourceFile(
89+
tsIndex: TypeSchemaIndex,
90+
schema: RegularTypeSchema,
91+
): void {
92+
// Generate imports
93+
generateDependenciesImports(this, schema);
94+
95+
// Re-export complex types
96+
generateComplexTypeReexports(this, schema);
97+
98+
// Generate nested types
99+
generateNestedTypes(this, tsIndex, schema);
100+
101+
// Add canonical URL comment
102+
this.comment("CanonicalURL:", schema.identifier.url);
103+
104+
// Generate main interface
105+
generateType(this, tsIndex, schema);
106+
}
107+
108+
/**
109+
* Generate profile file using adapter pattern
110+
*/
111+
private generateProfileFile(
112+
tsIndex: TypeSchemaIndex,
113+
schema: ProfileTypeSchema,
114+
): void {
115+
// Generate imports for base resource and dependencies
116+
generateDependenciesImports(this, schema);
117+
118+
// Add canonical URL comment
119+
this.comment("CanonicalURL:", schema.identifier.url || "");
120+
this.line();
121+
122+
// Generate adapter class
123+
generateProfileAdapter(this, tsIndex, schema);
124+
this.line();
125+
126+
// Generate factory function
127+
generateProfileFactory(this, schema);
128+
}
129+
130+
/**
131+
* Main generation entry point
132+
*/
133+
override generate(tsIndex: TypeSchemaIndex): void {
134+
// Collect all types to generate
135+
const typesToGenerate = [
136+
...tsIndex.collectComplexTypes(),
137+
...tsIndex.collectResources(),
138+
// ...tsIndex.collectLogicalModels(),
139+
...tsIndex
140+
.collectProfiles()
141+
// NOTE: because non Resource don't have `meta` field
142+
.filter((p) => tsIndex.isWithMetaField(p)),
143+
];
144+
145+
// Group by package
146+
const grouped = groupByPackages(typesToGenerate);
147+
148+
// Generate files
149+
this.cd("/", () => {
150+
for (const [packageName, packageSchemas] of Object.entries(grouped)) {
151+
const tsPackageDir = tsFhirPackageDir(packageName);
152+
this.cd(tsPackageDir, () => {
153+
for (const schema of packageSchemas) {
154+
this.generateResourceModule(tsIndex, schema);
155+
}
156+
this.generateFhirPackageIndexFile(packageSchemas);
157+
});
158+
}
159+
});
160+
}
161+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { TypeSchemaIndex } from "@root/typeschema/utils";
2+
import type { Writer } from "@root/api/writer-generator/writer";
3+
import type { NestedType, RegularTypeSchema, TypeSchema } from "@root/typeschema/types";
4+
import { tsResourceName } from "./utils";
5+
6+
/**
7+
* Generate nested type definitions
8+
*/
9+
export function generateNestedTypes(
10+
writer: Writer,
11+
tsIndex: TypeSchemaIndex,
12+
schema: RegularTypeSchema,
13+
): void {
14+
if (!schema.nested || schema.nested.length === 0) {
15+
return;
16+
}
17+
18+
for (const nested of schema.nested) {
19+
generateNestedType(writer, tsIndex, nested);
20+
writer.line();
21+
}
22+
}
23+
24+
/**
25+
* Generate a single nested type
26+
* This is imported from resource.ts to avoid circular dependencies
27+
*/
28+
function generateNestedType(
29+
writer: Writer,
30+
tsIndex: TypeSchemaIndex,
31+
nested: NestedType,
32+
): void {
33+
const typeName = tsResourceName(nested.identifier);
34+
35+
writer.comment(`Nested type: ${typeName}`);
36+
37+
// Import generateType from resource.ts would create circular dependency
38+
// So we'll handle this in resource.ts instead
39+
// For now, just mark it needs to be generated
40+
writer.debugComment("Nested type", nested);
41+
}

0 commit comments

Comments
 (0)