Skip to content
Draft
443 changes: 381 additions & 62 deletions docs/type-schema.schema.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from fhir_types.hl7_fhir_r4_core.base import Extension
from fhir_types.hl7_fhir_r4_core.base import CodeableConcept, Period
from fhir_types.profile_helpers import (
_get_key, build_resource, get_extension_value, is_extension, push_extension, validate_fixed_value, validate_required
_get_key, build_resource, ensure_slice_defaults, get_extension_value, is_extension, push_extension, validate_fixed_value, \
validate_required
)


Expand Down Expand Up @@ -38,16 +39,25 @@ def apply(cls, resource: Extension) -> "NationalityExtension":
return cls(resource)

@classmethod
def create_resource(cls) -> Extension:
return build_resource(Extension, url="http://hl7.org/fhir/StructureDefinition/patient-nationality")
def create_resource(cls, *, extension: list[Extension] | None = None) -> Extension:
extension_with_defaults = list(extension or [])

return build_resource(Extension, url="http://hl7.org/fhir/StructureDefinition/patient-nationality", extension=extension_with_defaults)

@classmethod
def create(cls) -> "NationalityExtension":
return cls.apply(cls.create_resource())
def create(cls, *, extension: list[Extension] | None = None) -> "NationalityExtension":
return cls.apply(cls.create_resource(extension=extension))

def to_resource(self) -> Extension:
return self._resource

def get_extension(self) -> list[Extension] | None:
return cast('list[Extension] | None', getattr(self._resource, "extension", None))

def set_extension(self, value: list[Extension]) -> "NationalityExtension":
setattr(self._resource, "extension", value)
return self

def get_url(self) -> str | None:
return cast('str | None', getattr(self._resource, "url", None))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def apply(cls, resource: Extension) -> "UscoreEthnicityExtension":

@classmethod
def create_resource(cls, *, extension: list[Extension] | None = None) -> Extension:
extension_with_defaults = ensure_slice_defaults(list(extension or []), cls._text_slice_match)
extension_with_defaults = list(extension or [])

return build_resource(Extension, url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", extension=extension_with_defaults)

Expand Down Expand Up @@ -217,16 +217,16 @@ def validate(self) -> dict[str, list[str]]:
errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"ombCategory"}, "ombCategory", 0, 1))
errors.extend(
validate_slice_fields(self._resource, profile_name, "extension", {"url":"ombCategory"}, "ombCategory", [
"value","valueCoding"
"valueCoding"
]))
errors.extend(
validate_slice_fields(self._resource, profile_name, "extension", {"url":"detailed"}, "detailed", [
"value","valueCoding"
"valueCoding"
]))
errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"text"}, "text", 1, 1))
errors.extend(
validate_slice_fields(self._resource, profile_name, "extension", {"url":"text"}, "text", [
"value","valueString"
"valueString"
]))
errors.extend(validate_required(self._resource, profile_name, "url"))
errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def apply(cls, resource: Extension) -> "UscoreRaceExtension":

@classmethod
def create_resource(cls, *, extension: list[Extension] | None = None) -> Extension:
extension_with_defaults = ensure_slice_defaults(list(extension or []), cls._text_slice_match)
extension_with_defaults = list(extension or [])

return build_resource(Extension, url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", extension=extension_with_defaults)

Expand Down Expand Up @@ -220,16 +220,16 @@ def validate(self) -> dict[str, list[str]]:
errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"ombCategory"}, "ombCategory", 0, 6))
errors.extend(
validate_slice_fields(self._resource, profile_name, "extension", {"url":"ombCategory"}, "ombCategory", [
"value","valueCoding"
"valueCoding"
]))
errors.extend(
validate_slice_fields(self._resource, profile_name, "extension", {"url":"detailed"}, "detailed", [
"value","valueCoding"
"valueCoding"
]))
errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"text"}, "text", 1, 1))
errors.extend(
validate_slice_fields(self._resource, profile_name, "extension", {"url":"text"}, "text", [
"value","valueString"
"valueString"
]))
errors.extend(validate_required(self._resource, profile_name, "url"))
errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def apply(cls, resource: Extension) -> "UscoreTribalAffiliationExtension":

@classmethod
def create_resource(cls, *, extension: list[Extension] | None = None) -> Extension:
extension_with_defaults = ensure_slice_defaults(list(extension or []), cls._tribal_affiliation_slice_match)
extension_with_defaults = list(extension or [])

return build_resource(Extension, url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation", extension=extension_with_defaults)

Expand Down Expand Up @@ -167,12 +167,12 @@ def validate(self) -> dict[str, list[str]]:
errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"tribalAffiliation"}, "tribalAffiliation", 1, 1))
errors.extend(
validate_slice_fields(self._resource, profile_name, "extension", {"url":"tribalAffiliation"}, "tribalAffiliation", [
"value","valueCodeableConcept"
"valueCodeableConcept"
]))
errors.extend(validate_slice_cardinality(self._resource, profile_name, "extension", {"url":"isEnrolled"}, "isEnrolled", 0, 1))
errors.extend(
validate_slice_fields(self._resource, profile_name, "extension", {"url":"isEnrolled"}, "isEnrolled", [
"value","valueBoolean"
"valueBoolean"
]))
errors.extend(validate_required(self._resource, profile_name, "url"))
errors.extend(validate_fixed_value(self._resource, profile_name, "url", "http://hl7.org/fhir/us/core/StructureDefinition/us-core-tribal-affiliation"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,10 @@ export class USCoreEthnicityExtensionProfile {
errors: [
...validateRequired(res, profileName, "extension"),
...validateSliceCardinality(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", 0, 1),
...validateSliceFields(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", ["value","valueCoding"]),
...validateSliceFields(res, profileName, "extension", {"url":"detailed"}, "detailed", ["value","valueCoding"]),
...validateSliceFields(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", ["valueCoding"]),
...validateSliceFields(res, profileName, "extension", {"url":"detailed"}, "detailed", ["valueCoding"]),
...validateSliceCardinality(res, profileName, "extension", {"url":"text"}, "text", 1, 1),
...validateSliceFields(res, profileName, "extension", {"url":"text"}, "text", ["value","valueString"]),
...validateSliceFields(res, profileName, "extension", {"url":"text"}, "text", ["valueString"]),
...validateRequired(res, profileName, "url"),
...validateFixedValue(res, profileName, "url", USCoreEthnicityExtensionProfile.canonicalUrl),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,10 @@ export class USCoreRaceExtensionProfile {
errors: [
...validateRequired(res, profileName, "extension"),
...validateSliceCardinality(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", 0, 6),
...validateSliceFields(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", ["value","valueCoding"]),
...validateSliceFields(res, profileName, "extension", {"url":"detailed"}, "detailed", ["value","valueCoding"]),
...validateSliceFields(res, profileName, "extension", {"url":"ombCategory"}, "ombCategory", ["valueCoding"]),
...validateSliceFields(res, profileName, "extension", {"url":"detailed"}, "detailed", ["valueCoding"]),
...validateSliceCardinality(res, profileName, "extension", {"url":"text"}, "text", 1, 1),
...validateSliceFields(res, profileName, "extension", {"url":"text"}, "text", ["value","valueString"]),
...validateSliceFields(res, profileName, "extension", {"url":"text"}, "text", ["valueString"]),
...validateRequired(res, profileName, "url"),
...validateFixedValue(res, profileName, "url", USCoreRaceExtensionProfile.canonicalUrl),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,9 @@ export class USCoreTribalAffiliationExtensionProfile {
errors: [
...validateRequired(res, profileName, "extension"),
...validateSliceCardinality(res, profileName, "extension", {"url":"tribalAffiliation"}, "tribalAffiliation", 1, 1),
...validateSliceFields(res, profileName, "extension", {"url":"tribalAffiliation"}, "tribalAffiliation", ["value","valueCodeableConcept"]),
...validateSliceFields(res, profileName, "extension", {"url":"tribalAffiliation"}, "tribalAffiliation", ["valueCodeableConcept"]),
...validateSliceCardinality(res, profileName, "extension", {"url":"isEnrolled"}, "isEnrolled", 0, 1),
...validateSliceFields(res, profileName, "extension", {"url":"isEnrolled"}, "isEnrolled", ["value","valueBoolean"]),
...validateSliceFields(res, profileName, "extension", {"url":"isEnrolled"}, "isEnrolled", ["valueBoolean"]),
...validateRequired(res, profileName, "url"),
...validateFixedValue(res, profileName, "url", USCoreTribalAffiliationExtensionProfile.canonicalUrl),
],
Expand Down
21 changes: 17 additions & 4 deletions src/api/writer-generator/python/profile-factory.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isExtensionOwnedField } from "@root/api/writer-generator/utils";
import {
type ChoiceFieldInstance,
type Field,
Expand Down Expand Up @@ -174,11 +175,21 @@ export const collectProfileFactoryInfo = (
}

if (isNotChoiceDeclarationField(field)) {
const sliceNames = collectRequiredSliceNames(field, flatProfile.slicing?.[name]);
if (sliceNames) {
const sliceNames =
isExtensionOwnedField(name) && flatProfile.base.name !== "Extension"
? undefined
: collectRequiredSliceNames(field, flatProfile.slicing?.[name]);
// Extension profiles populate `extension` via sub-extension slice
// setters — keep it optional in create() even when no slice is
// auto-stubbable (mirrors the optional `extension` in the TS Raw type).
const slicedExtensionProfileField =
name === "extension" &&
flatProfile.base.name === "Extension" &&
flatProfile.slicing?.extension?.slices !== undefined;
if (sliceNames || slicedExtensionProfileField) {
if (field.type) {
const pyType = fieldPyType(field, resolveRef, tsIndex);
sliceAutoFields.push({ name, pyType, typeId: field.type, sliceNames });
sliceAutoFields.push({ name, pyType, typeId: field.type, sliceNames: sliceNames ?? [] });
autoAccessors.push({ name, pyType, typeId: field.type, refComment: pyReferenceComment(field) });
}
continue;
Expand Down Expand Up @@ -269,7 +280,9 @@ export const generateCreateResource = (
for (const f of factoryInfo.sliceAutoFields) {
const fieldName = pyFieldName(f.name, fmt);
const matchRefs = f.sliceNames.map((s) => `cls.${pySliceStaticName(s)}`);
if (matchRefs.length === 1) {
if (matchRefs.length === 0) {
w.line(`${fieldName}_with_defaults = list(${fieldName} or [])`);
} else if (matchRefs.length === 1) {
w.line(`${fieldName}_with_defaults = ensure_slice_defaults(list(${fieldName} or []), ${matchRefs[0]})`);
} else {
w.line(`${fieldName}_with_defaults = ensure_slice_defaults(`);
Expand Down
48 changes: 9 additions & 39 deletions src/api/writer-generator/python/profile-slices.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isExtensionOwnedField } from "@root/api/writer-generator/utils";
import {
type ConstrainedChoiceInfo,
type FieldSlicing,
isChoiceDeclarationField,
isNotChoiceDeclarationField,
isPrimitiveIdentifier,
isTypeDiscriminated,
Expand Down Expand Up @@ -40,10 +40,8 @@ export const collectRequiredSliceNames = (
fieldSlicing: FieldSlicing | undefined,
): string[] | undefined => {
if (!field.array || !fieldSlicing?.slices) return undefined;
// Type-discriminated slices ("type" discriminator) require explicit typed setters — no stubs.
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)
.filter(([_, s]) => s.autoStub)
.map(([name]) => name);
return names.length > 0 ? names : undefined;
};
Expand Down Expand Up @@ -93,60 +91,32 @@ export const normalizeMatchForPython = (
return result;
};

const extractTypeDiscriminatorResource = (
isTypeDiscriminated: boolean,
rawMatch: Record<string, unknown> | undefined,
): string | undefined => {
if (!isTypeDiscriminated || !rawMatch) return undefined;
for (const val of Object.values(rawMatch)) {
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
const rt = (val as Record<string, unknown>).resourceType;
if (typeof rt === "string") return rt;
}
}
return undefined;
};

export const collectSliceDefs = (tsIndex: TypeSchemaIndex, flatProfile: SnapshotProfileTypeSchema): SliceDef[] => {
const pkgName = flatProfile.identifier.package;
return Object.entries(flatProfile.slicing ?? {}).flatMap(([fieldName, fieldSlicing]) => {
if (isExtensionOwnedField(fieldName) && flatProfile.base.name !== "Extension") return [];
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);
}
}
const typeDiscriminated = isTypeDiscriminated(fieldSlicing);
return Object.entries(fieldSlicing.slices)
.filter(([_, slice]) => Object.keys(slice.match ?? {}).length > 0)
.filter(([_, slice]) => slice.match !== undefined)
.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;
const cc = slice.constrainedChoice;
// Skip flattening for primitive types — can't wrap/unwrap under a variant key.
const constrainedChoice = cc && !isPrimitiveIdentifier(cc.variantType) ? cc : undefined;
const typeDiscriminated = isTypeDiscriminated(fieldSlicing);
const typeDiscriminatorResource = extractTypeDiscriminatorResource(
typeDiscriminated,
slice.match as Record<string, unknown> | undefined,
);
return {
fieldName,
sliceName,
match: normalizeMatchForPython(tsIndex, slice.match ?? {}, baseSchema),
required,
match: normalizeMatchForPython(tsIndex, slice.match?.value ?? {}, baseSchema),
required: slice.effectiveRequired ?? [],
array: Boolean(field.array),
max: slice.max ?? 0,
constrainedChoice,
elementTypeName:
field.type && !isPrimitiveIdentifier(field.type) ? pyTypeFromIdentifier(field.type) : undefined,
elementTypeId: field.type && !isPrimitiveIdentifier(field.type) ? field.type : undefined,
isTypeDiscriminated: typeDiscriminated,
typeDiscriminatorResource,
typeDiscriminatorResource: slice.resourceType,
nameCandidates: slice.nameCandidates,
};
});
Expand Down
Loading
Loading