Skip to content

Commit 65a5e8a

Browse files
committed
Actualize profiles design doc for current API
1 parent ac130ab commit 65a5e8a

1 file changed

Lines changed: 119 additions & 51 deletions

File tree

docs/design/profiles.md

Lines changed: 119 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# FHIR Profiles Representation
22

3-
Status: Implemented (TypeScript). See `examples/typescript-r4/` for working examples.
3+
Status: Implemented (TypeScript). See `examples/typescript-r4/` and `examples/typescript-us-core/` for working examples.
44

55
This document covers the representation of FHIR profiles in generated code: resource profiles, extension profiles, and their relationship to base resources.
66

@@ -82,16 +82,21 @@ Problem: if we have several levels of profiles on top of the resource how they s
8282
Arbitrary JSON can be converted to:
8383

8484
1. Resource type, independently from the profiles.
85-
1. Profile type via `ProfileClass.from(resource)`.
85+
1. Profile type via `ProfileClass.from(resource)` or `ProfileClass.apply(resource)`.
8686

8787
```typescript
8888
// Parse as resource
8989
const obs: Observation = JSON.parse(json)
9090

91-
// Wrap with profile
91+
// from() validates meta.profile and runs validate(), throws on errors
9292
const bodyweight = observation_bodyweightProfile.from(obs)
93-
bodyweight.getVSCat() // access slice
94-
bodyweight.toResource() // back to Observation (same object)
93+
94+
// apply() stamps meta.profile without validation, for incremental construction
95+
const bodyweight = observation_bodyweightProfile.apply(obs)
96+
97+
bodyweight.getVSCat() // access slice (flat by default)
98+
bodyweight.getVSCat("raw") // access raw FHIR element
99+
bodyweight.toResource() // back to Observation (same object)
95100
```
96101

97102
### Mutable/Immutable Representation
@@ -100,7 +105,7 @@ The profile class holds a mutable reference to the underlying resource. Mutation
100105

101106
```typescript
102107
const obs: Observation = { resourceType: "Observation", status: "preliminary", ... }
103-
const profile = observation_bodyweightProfile.from(obs)
108+
const profile = observation_bodyweightProfile.apply(obs)
104109
profile.setStatus("final")
105110
obs.status // "final" — same object
106111
```
@@ -120,38 +125,44 @@ export interface observation_bodyweight extends Observation {
120125
}
121126
```
122127

123-
2. **Profile class** — wraps the resource with typed getters/setters and slice accessors:
128+
2. **Profile class** — wraps the resource with factory methods, typed getters/setters, slice accessors, extension accessors, and validation:
124129

125130
```typescript
126131
export class observation_bodyweightProfile {
132+
static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/bodyweight"
127133
private resource: Observation
128134

129135
constructor(resource: Observation) { ... }
130-
static from(resource: Observation): observation_bodyweightProfile { ... }
131-
static createResource(args: observation_bodyweightProfileParams): Observation { ... }
132-
static create(args: observation_bodyweightProfileParams): observation_bodyweightProfile { ... }
136+
137+
// Factory methods
138+
static from(resource: Observation): observation_bodyweightProfile { ... } // validates, throws on error
139+
static apply(resource: Observation): observation_bodyweightProfile { ... } // stamps meta.profile, no validation
140+
static createResource(args: observation_bodyweightProfileRaw): Observation { ... }
141+
static create(args: observation_bodyweightProfileRaw): observation_bodyweightProfile { ... }
133142

134143
// Typed getters/setters for constrained fields
135144
getStatus(): (...) | undefined { ... }
136145
setStatus(value: ...): this { ... }
137146

138-
// Slice accessors (see slices.md)
139-
setVSCat(input?: Observation_bodyweight_Category_VSCatSliceInput): this { ... }
140-
getVSCat(): Observation_bodyweight_Category_VSCatSliceInput | undefined { ... }
141-
getVSCatRaw(): CodeableConcept | undefined { ... }
147+
// Slice accessors with mode overloads (see slices.md)
148+
setVSCat(input?: VSCatSliceFlat | CodeableConcept): this { ... }
149+
getVSCat(): VSCatSliceFlat | undefined { ... } // flat (default)
150+
getVSCat(mode: 'flat'): VSCatSliceFlat | undefined { ... }
151+
getVSCat(mode: 'raw'): CodeableConcept | undefined { ... }
142152

143153
// Conversion
144154
toResource(): Observation { ... }
145-
toProfile(): observation_bodyweight { ... }
155+
156+
// Validation
157+
validate(): { errors: string[]; warnings: string[] } { ... }
146158
}
147159
```
148160

149161
3. **Params type** — lists fields for the `createResource`/`create` factory methods. Array fields with required slices are optional -- stubs are auto-merged:
150162

151163
```typescript
152-
export type observation_bodyweightProfileParams = {
164+
export type observation_bodyweightProfileRaw = {
153165
status: (...);
154-
code: CodeableConcept<(...)>;
155166
subject: Reference<"Patient">;
156167
category?: CodeableConcept<(...)>[]; // optional -- required slice stubs auto-merged
157168
}
@@ -166,27 +177,24 @@ Extension profiles constrain the `Extension` type. They come in two forms:
166177
A simple extension carries one `value[x]` field (e.g., `patient-birthPlace` carries `valueAddress`):
167178
168179
```typescript
169-
export type birthPlaceProfileParams = {
180+
export type birthPlaceProfileRaw = {
170181
valueAddress: Address;
171182
}
172183

173184
export class birthPlaceProfile {
185+
static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/patient-birthPlace"
174186
private resource: Extension
175187

176-
static createResource(args: birthPlaceProfileParams): Extension {
177-
return {
178-
url: "http://hl7.org/fhir/StructureDefinition/patient-birthPlace",
179-
valueAddress: args.valueAddress,
180-
} as unknown as Extension
181-
}
182-
183-
static create(args: birthPlaceProfileParams): birthPlaceProfile { ... }
184-
static from(resource: Extension): birthPlaceProfile { ... }
188+
static from(resource: Extension): birthPlaceProfile { ... } // validates
189+
static apply(resource: Extension): birthPlaceProfile { ... } // no validation
190+
static createResource(args: birthPlaceProfileRaw): Extension { ... }
191+
static create(args: birthPlaceProfileRaw): birthPlaceProfile { ... }
185192

186193
getValueAddress(): Address | undefined { ... }
187194
setValueAddress(value: Address): this { ... }
188195

189196
toResource(): Extension { ... }
197+
validate(): { errors: string[]; warnings: string[] } { ... }
190198
}
191199
```
192200

@@ -206,36 +214,70 @@ const patient: Patient = {
206214
A complex extension has nested extension elements instead of a single value (e.g., `patient-nationality` has `code` and `period` sub-extensions):
207215

208216
```typescript
217+
// Raw input — pass extension[] directly
218+
export type nationalityProfileRaw = { extension?: Extension[] }
219+
220+
// Flat input — typed sub-extension fields
221+
export type nationalityProfileFlat = { code?: CodeableConcept; period?: Period }
222+
209223
export class nationalityProfile {
224+
static readonly canonicalUrl = "http://hl7.org/fhir/StructureDefinition/patient-nationality"
210225
private resource: Extension
211226

212-
static createResource(): Extension {
213-
return {
214-
url: "http://hl7.org/fhir/StructureDefinition/patient-nationality",
215-
} as unknown as Extension
216-
}
227+
static from(resource: Extension): nationalityProfile { ... }
228+
static apply(resource: Extension): nationalityProfile { ... }
229+
230+
// create/createResource accept both raw and flat input
231+
static createResource(args?: nationalityProfileRaw | nationalityProfileFlat): Extension { ... }
232+
static create(args?: nationalityProfileRaw | nationalityProfileFlat): nationalityProfile { ... }
217233

218-
// Sub-extension accessors
234+
// Sub-extension accessors with mode overloads
219235
setCode(value: CodeableConcept): this { ... }
236+
getCode(): CodeableConcept | undefined { ... } // flat (default)
237+
getCode(mode: 'flat'): CodeableConcept | undefined { ... }
238+
getCode(mode: 'raw'): Extension | undefined { ... } // raw sub-extension
239+
220240
setPeriod(value: Period): this { ... }
221-
getCode(): CodeableConcept | undefined { ... }
222-
getCodeExtension(): Extension | undefined { ... } // raw access
223241
getPeriod(): Period | undefined { ... }
224-
getPeriodExtension(): Extension | undefined { ... }
242+
getPeriod(mode: 'raw'): Extension | undefined { ... }
225243

226244
toResource(): Extension { ... }
245+
validate(): { errors: string[]; warnings: string[] } { ... }
227246
}
228247
```
229248

230249
Usage:
231250

232251
```typescript
233-
const profile = nationalityProfile.create()
234-
.setCode({ coding: [{ system: "urn:iso:std:iso:3166", code: "US" }] })
235-
.setPeriod({ start: "2000-01-01" })
252+
// Flat input
253+
const profile = nationalityProfile.create({
254+
code: { coding: [{ system: "urn:iso:std:iso:3166", code: "US" }] },
255+
period: { start: "2000-01-01" },
256+
})
257+
258+
// Read values back
259+
profile.getCode() // { coding: [...] }
260+
profile.getCode("raw") // { url: "code", valueCodeableConcept: { coding: [...] } }
261+
236262
const ext: Extension = profile.toResource()
237263
```
238264

265+
### Extension Accessors on Resource Profiles
266+
267+
Resource profiles that declare extensions (e.g., US Core Patient with `us-core-race`) generate multi-form setters and overloaded getters:
268+
269+
```typescript
270+
// Setter accepts flat input, profile instance, or raw Extension
271+
patient.setRace({ ombCategory: { code: "2028-9" }, text: "Asian" }) // flat input
272+
patient.setRace(USCoreRaceExtensionProfile.create({ ... })) // profile instance
273+
patient.setRace({ url: "http://.../us-core-race", extension: [...] }) // raw Extension
274+
275+
// Getter with mode overloads
276+
patient.getRace() // flat: { ombCategory: ..., text: "Asian" }
277+
patient.getRace("profile") // USCoreRaceExtensionProfile instance
278+
patient.getRace("raw") // raw FHIR Extension
279+
```
280+
239281
## TypeSchema Representation
240282

241283
Profiles use `kind = "constraint"` in TypeSchema.
@@ -246,20 +288,38 @@ Current approach: to collect all profile elements we traverse the inheritance tr
246288

247289
Profile classes depend on a generated `profile-helpers.ts` module that provides:
248290

291+
Slice helpers:
249292
- `applySliceMatch(input, match)` — merges discriminator values into a slice element
250-
- `matchesSlice(value, match)` — checks if an element matches a slice discriminator
251-
- `extractSliceSimplified(slice, matchKeys)` — strips discriminator keys from a slice for the simplified input type
252-
- `wrapSliceChoice(input, choiceVariant)` — wraps flat input fields under a single choice variant key (for setter)
253-
- `flattenSliceChoice(slice, matchKeys, choiceVariant)` — strips discriminator keys and flattens a single choice variant into parent (for getter)
254-
- `mergeMatch(target, match)` — deep-merges match values into target
293+
- `matchesValue(value, match)` — recursive structural match test
294+
- `setArraySlice(list, match, value)` — find-or-insert in array by discriminator
295+
- `getArraySlice(list, match)` — find first matching element
296+
- `ensureSliceDefaults(items, ...matches)` — ensure required slices have stubs
297+
- `stripMatchKeys(slice, matchKeys)` — remove discriminator keys from getter result
298+
- `wrapSliceChoice(input, choiceVariant)` — wrap flat input fields under a single choice variant key (for setter)
299+
- `unwrapSliceChoice(slice, matchKeys, choiceVariant)` — inverse of wrap
300+
301+
Extension helpers:
302+
- `ensurePath(root, path)` — navigate/create nested paths for deep extensions
255303
- `extractComplexExtension(extension, config)` — extracts typed values from nested extension elements
256-
- `validateRequired(r, field, path)` — checks that a required field is present
257-
- `validateMustSupport(r, field, path)` — checks that a must-support field is populated (warning, not error)
258-
- `validateExcluded(r, field, path)` — checks that a forbidden field is absent
259-
- `validateFixedValue(r, field, expected, path)` — checks that a field matches a fixed/pattern value
260-
- `validateSliceCardinality(items, match, sliceName, min, max, path)` — checks min/max counts for a named slice
261-
- `validateEnum(value, allowed, field, path)` — checks that a value is within a required value set (supports primitives, Coding, CodeableConcept)
262-
- `validateReference(value, allowed, field, path)` — checks that a reference targets an allowed resource type
304+
- `isExtension(input, url?)` — type guard for raw Extension detection
305+
- `isRawExtensionInput(input)` — discriminate raw vs flat input for extension profile factories
306+
- `getExtensionValue(ext, field)` — read a typed value field from Extension
307+
- `pushExtension(target, ext)` — push extension onto target.extension array
308+
309+
Factory helpers:
310+
- `buildResource(obj)` — cast object to resource type
311+
- `ensureProfile(resource, canonicalUrl)` — add profile URL to meta.profile
312+
- `mergeMatch(target, match)` — deep-merges match values into target
313+
314+
Validation helpers:
315+
- `validateRequired(res, profileName, field)` — checks that a required field is present
316+
- `validateMustSupport(res, profileName, field)` — checks that a must-support field is populated (warning, not error)
317+
- `validateExcluded(res, profileName, field)` — checks that a forbidden field is absent
318+
- `validateFixedValue(res, profileName, field, expected)` — checks that a field matches a fixed/pattern value
319+
- `validateSliceCardinality(res, profileName, field, match, sliceName, min, max)` — checks min/max counts for a named slice
320+
- `validateChoiceRequired(res, profileName, choices)` — checks that at least one choice variant is present
321+
- `validateEnum(res, profileName, field, allowed)` — checks that a value is within a value set (supports primitives, Coding, CodeableConcept)
322+
- `validateReference(res, profileName, field, allowed)` — checks that a reference targets an allowed resource type
263323

264324
## Configuration
265325

@@ -308,6 +368,14 @@ const { errors, warnings } = bp.validate();
308368
// Required slices (VSCat, SystolicBP, DiastolicBP) are auto-populated by create()
309369
```
310370

371+
`from()` uses `validate()` internally — it throws on errors but allows warnings:
372+
373+
```typescript
374+
const profile = observation_bodyweightProfile.from(obs)
375+
// throws if meta.profile is missing or validate().errors is non-empty
376+
// warnings are not thrown — retrieve them via profile.validate().warnings
377+
```
378+
311379
Validation helpers are emitted into `profile-helpers.ts` alongside the existing slice helpers.
312380

313381
## Future Work

0 commit comments

Comments
 (0)