Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/api/writer-generator/python/profile-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export const collectProfileFactoryInfo = (
}

if (isNotChoiceDeclarationField(field)) {
const sliceNames = collectRequiredSliceNames(field);
const sliceNames = collectRequiredSliceNames(field, flatProfile.slicing?.[name]);
if (sliceNames) {
if (field.type) {
const pyType = fieldPyType(field, resolveRef, tsIndex);
Expand Down
26 changes: 16 additions & 10 deletions src/api/writer-generator/python/profile-slices.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
type ConstrainedChoiceInfo,
type FieldSlicing,
isChoiceDeclarationField,
isNotChoiceDeclarationField,
isPrimitiveIdentifier,
isTypeDiscriminated,
type NameCandidates,
type RegularField,
type SnapshotProfileTypeSchema,
Expand Down Expand Up @@ -33,11 +35,14 @@ export type SliceDef = {
nameCandidates: NameCandidates;
};

export const collectRequiredSliceNames = (field: RegularField): string[] | undefined => {
if (!field.array || !field.slicing?.slices) return undefined;
export const collectRequiredSliceNames = (
field: RegularField,
fieldSlicing: FieldSlicing | undefined,
): string[] | undefined => {
if (!field.array || !fieldSlicing?.slices) return undefined;
// Type-discriminated slices ("type" discriminator) require explicit typed setters — no stubs.
if (field.slicing.discriminator?.some((d) => d.type === "type")) return undefined;
const names = Object.entries(field.slicing.slices)
if (isTypeDiscriminated(fieldSlicing)) return undefined;
const names = Object.entries(fieldSlicing.slices)
.filter(([_, s]) => s.min !== undefined && s.min >= 1 && s.match && Object.keys(s.match).length > 0)
.map(([name]) => name);
return names.length > 0 ? names : undefined;
Expand Down Expand Up @@ -104,16 +109,17 @@ const extractTypeDiscriminatorResource = (

export const collectSliceDefs = (tsIndex: TypeSchemaIndex, flatProfile: SnapshotProfileTypeSchema): SliceDef[] => {
const pkgName = flatProfile.identifier.package;
return Object.entries(flatProfile.fields).flatMap(([fieldName, field]) => {
if (!isNotChoiceDeclarationField(field) || !field.slicing?.slices || !field.type) return [];
return Object.entries(flatProfile.slicing ?? {}).flatMap(([fieldName, fieldSlicing]) => {
const field = flatProfile.fields[fieldName];
if (!isNotChoiceDeclarationField(field) || !fieldSlicing.slices || !field.type) return [];
const choiceBaseNames = new Set<string>();
const baseSchema = tsIndex.resolveType(field.type);
if (baseSchema && "fields" in baseSchema && baseSchema.fields) {
for (const [n, f] of Object.entries(baseSchema.fields)) {
if (isChoiceDeclarationField(f)) choiceBaseNames.add(n);
}
}
return Object.entries(field.slicing.slices)
return Object.entries(fieldSlicing.slices)
.filter(([_, slice]) => Object.keys(slice.match ?? {}).length > 0)
.map(([sliceName, slice]) => {
const matchFields = Object.keys(slice.match ?? {});
Expand All @@ -123,9 +129,9 @@ export const collectSliceDefs = (tsIndex: TypeSchemaIndex, flatProfile: Snapshot
const cc = slice.elements ? tsIndex.constrainedChoice(pkgName, field.type, slice.elements) : undefined;
// Skip flattening for primitive types — can't wrap/unwrap under a variant key.
const constrainedChoice = cc && !isPrimitiveIdentifier(cc.variantType) ? cc : undefined;
const isTypeDiscriminated = field.slicing?.discriminator?.some((d) => d.type === "type") ?? false;
const typeDiscriminated = isTypeDiscriminated(fieldSlicing);
const typeDiscriminatorResource = extractTypeDiscriminatorResource(
isTypeDiscriminated,
typeDiscriminated,
slice.match as Record<string, unknown> | undefined,
);
return {
Expand All @@ -139,7 +145,7 @@ export const collectSliceDefs = (tsIndex: TypeSchemaIndex, flatProfile: Snapshot
elementTypeName:
field.type && !isPrimitiveIdentifier(field.type) ? pyTypeFromIdentifier(field.type) : undefined,
elementTypeId: field.type && !isPrimitiveIdentifier(field.type) ? field.type : undefined,
isTypeDiscriminated,
isTypeDiscriminated: typeDiscriminated,
typeDiscriminatorResource,
nameCandidates: slice.nameCandidates,
};
Expand Down
22 changes: 17 additions & 5 deletions src/api/writer-generator/python/profile-validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
type ChoiceFieldInstance,
type FieldSlicing,
isChoiceDeclarationField,
isChoiceInstanceField,
isNotChoiceDeclarationField,
Expand Down Expand Up @@ -58,7 +59,16 @@ export const collectValidateBody = (
}
continue;
}
collectRegularFieldValidation(field, pyName, helpers, errorLines, warningLines, tsIndex, formatName);
collectRegularFieldValidation(
field,
flatProfile.slicing?.[name],
pyName,
helpers,
errorLines,
warningLines,
tsIndex,
formatName,
);
}
// Base-resource required fields the profile chain did not re-state.
// Emitted here (not via the regular field loop) because they intentionally
Expand All @@ -75,6 +85,7 @@ export const collectValidateBody = (

const collectRegularFieldValidation = (
field: RegularField | ChoiceFieldInstance,
fieldSlicing: FieldSlicing | undefined,
pyName: string,
helpers: Set<string>,
errorLines: string[],
Expand Down Expand Up @@ -116,22 +127,23 @@ const collectRegularFieldValidation = (
const allowed = field.reference.resource.map((ref) => tsIndex.findLastSpecializationByIdentifier(ref).name);
pushListValidation(errorLines, "errors", "validate_reference", [JSON.stringify(pyName)], allowed);
}
if (field.slicing?.slices) {
collectSliceValidation(field, pyName, helpers, errorLines, tsIndex, formatName);
if (fieldSlicing?.slices) {
collectSliceValidation(field, fieldSlicing, pyName, helpers, errorLines, tsIndex, formatName);
}
}
};

const collectSliceValidation = (
field: RegularField | ChoiceFieldInstance,
fieldSlicing: FieldSlicing,
name: string,
helpers: Set<string>,
errorLines: string[],
tsIndex: TypeSchemaIndex,
formatName: (s: string) => string,
): void => {
if (!field.slicing?.slices) return;
for (const [sliceName, slice] of Object.entries(field.slicing.slices)) {
if (!fieldSlicing.slices) return;
for (const [sliceName, slice] of Object.entries(fieldSlicing.slices)) {
const match = slice.match ?? {};
if (Object.keys(match).length === 0) continue;
if (slice.min !== undefined || slice.max !== undefined) {
Expand Down
5 changes: 3 additions & 2 deletions src/api/writer-generator/typescript/profile-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ export const valueFieldToTsType = (valueField: string): string => {
*/
export const collectSubExtensionSlices = (extProfile: SnapshotProfileTypeSchema): SubExtensionSliceInfo[] => {
const extensionField = extProfile.fields.extension;
if (!extensionField || isChoiceDeclarationField(extensionField) || !extensionField.slicing?.slices) return [];
const extensionSlicing = extProfile.slicing?.extension;
if (!extensionField || isChoiceDeclarationField(extensionField) || !extensionSlicing?.slices) return [];
const result: SubExtensionSliceInfo[] = [];
for (const [sliceName, slice] of Object.entries(extensionField.slicing.slices)) {
for (const [sliceName, slice] of Object.entries(extensionSlicing.slices)) {
const valueField = extractValueField(slice.elements);
if (!valueField) continue;
const tsType = valueFieldToTsType(valueField);
Expand Down
96 changes: 49 additions & 47 deletions src/api/writer-generator/typescript/profile-slices.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
type ConstrainedChoiceInfo,
type FieldSlicing,
isChoiceDeclarationField,
isNotChoiceDeclarationField,
isPrimitiveIdentifier,
isTypeDiscriminated,
type RegularField,
type SnapshotProfileTypeSchema,
type TypeIdentifier,
Expand Down Expand Up @@ -49,10 +51,11 @@ export const collectTypesFromSlices = (
addType: (typeId: TypeIdentifier) => void,
) => {
const pkgName = snapshot.identifier.package;
for (const field of Object.values(snapshot.fields)) {
if (!isNotChoiceDeclarationField(field) || !field.slicing?.slices || !field.type) continue;
const isTypeDisc = field.slicing.discriminator?.some((d) => d.type === "type") ?? false;
for (const slice of Object.values(field.slicing.slices)) {
for (const [fieldName, fieldSlicing] of Object.entries(snapshot.slicing ?? {})) {
const field = snapshot.fields[fieldName];
if (!isNotChoiceDeclarationField(field) || !fieldSlicing.slices || !field.type) continue;
const isTypeDisc = isTypeDiscriminated(fieldSlicing);
for (const slice of Object.values(fieldSlicing.slices)) {
if (Object.keys(slice.match ?? {}).length > 0) {
addType(field.type);
const cc = slice.elements ? tsIndex.constrainedChoice(pkgName, field.type, slice.elements) : undefined;
Expand All @@ -79,11 +82,13 @@ export const collectTypesFromSlices = (
* - The field uses a type discriminator (e.g. Bundle entry.resource) — the stub only sets
* resourceType, the user must provide the actual typed resource
*/
export const collectRequiredSliceNames = (field: RegularField): string[] | undefined => {
if (!field.array || !field.slicing?.slices) return undefined;
const isTypeDisc = field.slicing.discriminator?.some((d) => d.type === "type") ?? false;
if (isTypeDisc) return undefined;
const names = Object.entries(field.slicing.slices)
export const collectRequiredSliceNames = (
field: RegularField,
fieldSlicing: FieldSlicing | undefined,
): string[] | undefined => {
if (!field.array || !fieldSlicing?.slices) return undefined;
if (isTypeDiscriminated(fieldSlicing)) return undefined;
const names = Object.entries(fieldSlicing.slices)
.filter(([_, s]) => {
if (s.min === undefined || s.min < 1 || !s.match || Object.keys(s.match).length === 0) return false;
const matchKeys = new Set(Object.keys(s.match));
Expand Down Expand Up @@ -115,44 +120,41 @@ export type SliceDef = {
};

export const collectSliceDefs = (tsIndex: TypeSchemaIndex, snapshot: SnapshotProfileTypeSchema): SliceDef[] =>
Object.entries(snapshot.fields)
.filter(([_, field]) => isNotChoiceDeclarationField(field) && field.slicing?.slices)
.flatMap(([fieldName, field]) => {
if (!isNotChoiceDeclarationField(field) || !field.slicing?.slices || !field.type) return [];
const baseType = tsTypeFromIdentifier(field.type);
const pkgName = snapshot.identifier.package;
const choiceBaseNames = collectChoiceBaseNames(tsIndex, field.type);
const isTypeDisc = field.slicing.discriminator?.some((d) => d.type === "type") ?? false;
return Object.entries(field.slicing.slices)
.filter(([_, slice]) => Object.keys(slice.match ?? {}).length > 0)
.map(([sliceName, slice]) => {
const matchFields = Object.keys(slice.match ?? {});
const required = (slice.required ?? []).filter(
(name) => !matchFields.includes(name) && !choiceBaseNames.has(name),
);
const cc = slice.elements
? tsIndex.constrainedChoice(pkgName, field.type, slice.elements)
: undefined;
// Skip flattening for primitive types — can't intersect object with boolean/string/etc.
const constrainedChoice = cc && !isPrimitiveIdentifier(cc.variantType) ? cc : undefined;
const resourceType = isTypeDisc ? extractResourceTypeFromMatch(slice.match ?? {}) : undefined;
const typedBaseType = resourceType ? `${baseType}<${resourceType}>` : baseType;
return {
fieldName,
baseType,
typedBaseType,
sliceName,
baseName: slice.nameCandidates.recommended,
match: slice.match ?? {},
required,
excluded: slice.excluded ?? [],
array: Boolean(field.array),
constrainedChoice,
typeDiscriminator: isTypeDisc,
max: slice.max ?? 0,
};
});
});
Object.entries(snapshot.slicing ?? {}).flatMap(([fieldName, fieldSlicing]) => {
const field = snapshot.fields[fieldName];
if (!isNotChoiceDeclarationField(field) || !fieldSlicing.slices || !field.type) return [];
const baseType = tsTypeFromIdentifier(field.type);
const pkgName = snapshot.identifier.package;
const choiceBaseNames = collectChoiceBaseNames(tsIndex, field.type);
const isTypeDisc = isTypeDiscriminated(fieldSlicing);
return Object.entries(fieldSlicing.slices)
.filter(([_, slice]) => Object.keys(slice.match ?? {}).length > 0)
.map(([sliceName, slice]) => {
const matchFields = Object.keys(slice.match ?? {});
const required = (slice.required ?? []).filter(
(name) => !matchFields.includes(name) && !choiceBaseNames.has(name),
);
const cc = slice.elements ? tsIndex.constrainedChoice(pkgName, field.type, slice.elements) : undefined;
// Skip flattening for primitive types — can't intersect object with boolean/string/etc.
const constrainedChoice = cc && !isPrimitiveIdentifier(cc.variantType) ? cc : undefined;
const resourceType = isTypeDisc ? extractResourceTypeFromMatch(slice.match ?? {}) : undefined;
const typedBaseType = resourceType ? `${baseType}<${resourceType}>` : baseType;
return {
fieldName,
baseType,
typedBaseType,
sliceName,
baseName: slice.nameCandidates.recommended,
match: slice.match ?? {},
required,
excluded: slice.excluded ?? [],
array: Boolean(field.array),
constrainedChoice,
typeDiscriminator: isTypeDisc,
max: slice.max ?? 0,
};
});
});

export const generateSliceSetters = (w: TypeScript, sliceDefs: SliceDef[], snapshot: SnapshotProfileTypeSchema) => {
const profileClassName = tsProfileClassName(snapshot);
Expand Down
7 changes: 5 additions & 2 deletions src/api/writer-generator/typescript/profile-validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
type ChoiceFieldInstance,
type FieldSlicing,
isChoiceDeclarationField,
isChoiceInstanceField,
type RegularField,
Expand All @@ -18,6 +19,7 @@ export const collectRegularFieldValidation = (
resolveRef: (ref: TypeIdentifier) => TypeIdentifier,
canonicalUrlExpr?: { url: string; expr: string },
tsIndex?: TypeSchemaIndex,
fieldSlicing?: FieldSlicing,
) => {
if (field.excluded) {
errors.push(`...validateExcluded(res, profileName, ${JSON.stringify(name)})`);
Expand Down Expand Up @@ -47,8 +49,8 @@ export const collectRegularFieldValidation = (
`...validateReference(res, profileName, ${JSON.stringify(name)}, ${JSON.stringify(field.reference.resource.map((ref) => resolveRef(ref).name))})`,
);

if (field.slicing?.slices) {
for (const [sliceName, slice] of Object.entries(field.slicing.slices)) {
if (fieldSlicing?.slices) {
for (const [sliceName, slice] of Object.entries(fieldSlicing.slices)) {
const match = slice.match ?? {};
if (Object.keys(match).length === 0) continue;
if (slice.min !== undefined || slice.max !== undefined) {
Expand Down Expand Up @@ -114,6 +116,7 @@ export const generateValidateMethod = (
tsIndex.findLastSpecializationByIdentifier,
canonicalUrlExpr,
tsIndex,
snapshot.slicing?.[name],
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/api/writer-generator/typescript/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export const collectProfileFactoryInfo = (
}

if (isNotChoiceDeclarationField(field)) {
const sliceNames = collectRequiredSliceNames(field);
const sliceNames = collectRequiredSliceNames(field, snapshot.slicing?.[name]);
if (sliceNames) {
if (field.type) {
const tsType = fieldTsType(field, resolveRef, isFamilyType);
Expand Down
4 changes: 1 addition & 3 deletions src/typeschema/core/field-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ const computeMatchFromSchema = (
return result;
};

const buildSlicing = (fieldName: string, element: FHIRSchemaElement): FieldSlicing | undefined => {
export const buildSlicing = (fieldName: string, element: FHIRSchemaElement): FieldSlicing | undefined => {
const slicing = element.slicing;
if (!slicing) return undefined;

Expand Down Expand Up @@ -435,7 +435,6 @@ export const mkField = (
array: element.array || false,
min: element.min,
max: element.max,
slicing: buildSlicing(path[path.length - 1] ?? "", element),

choices: element.choices,
choiceOf: element.choiceOf,
Expand All @@ -459,6 +458,5 @@ export function mkNestedField(
array: element.array || false,
required: isRequired(register, fhirSchema, path),
excluded: isExcluded(register, fhirSchema, path),
slicing: buildSlicing(path[path.length - 1] ?? "", element),
};
}
12 changes: 6 additions & 6 deletions src/typeschema/core/name-candidates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ export const assignRecommendedBaseNames = (profile: ProfileTypeSchema): void =>
candidates: ext.nameCandidates.candidates,
}));

const sliceEntries: NameEntry[] = Object.entries(profile.fields ?? {}).flatMap(([fieldName, field]) => {
if (!("slicing" in field) || !field.slicing?.slices) return [];
return Object.entries(field.slicing.slices).map(([sliceName, slice]) => ({
const sliceEntries: NameEntry[] = Object.entries(profile.slicing ?? {}).flatMap(([fieldName, fieldSlicing]) => {
if (!fieldSlicing.slices) return [];
return Object.entries(fieldSlicing.slices).map(([sliceName, slice]) => ({
key: `slice:${fieldName}:${sliceName}`,
candidates: slice.nameCandidates.candidates,
}));
Expand All @@ -121,9 +121,9 @@ export const assignRecommendedBaseNames = (profile: ProfileTypeSchema): void =>
if (resolved[key]) ext.nameCandidates.recommended = resolved[key];
}

for (const [fieldName, field] of Object.entries(profile.fields ?? {})) {
if (!("slicing" in field) || !field.slicing?.slices) continue;
for (const [sliceName, slice] of Object.entries(field.slicing.slices)) {
for (const [fieldName, fieldSlicing] of Object.entries(profile.slicing ?? {})) {
if (!fieldSlicing.slices) continue;
for (const [sliceName, slice] of Object.entries(fieldSlicing.slices)) {
const key = `slice:${fieldName}:${sliceName}`;
if (resolved[key]) slice.nameCandidates.recommended = resolved[key];
}
Expand Down
Loading
Loading