|
| 1 | +# Profile Method Taxonomy |
| 2 | + |
| 3 | +Status: Proposed. Captures the categories of code the TS writer currently emits inside a profile module, and the descriptors each emit needs. |
| 4 | + |
| 5 | +This doc is the foundation for moving method derivation out of the TS writer and onto `SnapshotProfileTypeSchema` — once every emit is classified, the writer becomes a renderer that walks a typed list of descriptors instead of computing decisions inline. |
| 6 | + |
| 7 | +## Categories |
| 8 | + |
| 9 | +### 1. Module type aliases |
| 10 | + |
| 11 | +Emitted before the class body. Each carries shape information consumers need to call factory / accessor methods. |
| 12 | + |
| 13 | +| Name | Source helper | When emitted | |
| 14 | +|---|---|---| |
| 15 | +| `XxxProfile_extName_Flat` | `generateInlineExtensionInputTypes` | per complex extension that lacks a resolved profile class | |
| 16 | +| `XxxProfile_field_sliceFlat` | `generateSliceInputTypes` | per slice — setter input, discriminator fields omitted | |
| 17 | +| `XxxProfile_field_sliceFlatAll` | `generateSliceInputTypes` | per slice — getter return, includes discriminator values as readonly literals | |
| 18 | +| `XxxProfileRaw` | `generateRawType` | factory args type — emitted when there are params or extension sub-slices | |
| 19 | +| `XxxProfileFlat` | `generateFlatInputType` | Extension-profile-only flat input shape | |
| 20 | + |
| 21 | +### 2. Class constants |
| 22 | + |
| 23 | +| Name | Source | |
| 24 | +|---|---| |
| 25 | +| `static readonly canonicalUrl` | `generateProfileClass` | |
| 26 | +| `private static readonly XxxSliceMatch: Record<string, unknown>` | `generateStaticSliceFields`, one per slice | |
| 27 | + |
| 28 | +### 3. Static factory methods |
| 29 | + |
| 30 | +| Method | Role | Emit condition | |
| 31 | +|---|---|---| |
| 32 | +| `static from(resource)` | wrap an existing FHIR resource and validate it | always | |
| 33 | +| `static is(resource): resource is T` | type predicate via `meta.profile` (resource) or `url` (Extension) | resource with `meta` ancestor, or Extension base | |
| 34 | +| `static apply(resource): Profile` | tag with canonicalUrl + auto-fields, return wrapper | always | |
| 35 | +| `static createResource(args): T` | build the underlying FHIR object | always (signature varies on hasParams) | |
| 36 | +| `static create(args): Profile` | `apply(createResource(args))` | always | |
| 37 | +| `private static resolveInput(args)` | Extension-only: convert Flat input → `Extension[]` | Extension base **and** has sub-extension slices | |
| 38 | + |
| 39 | +### 4. Instance lifecycle |
| 40 | + |
| 41 | +| Method | Role | |
| 42 | +|---|---| |
| 43 | +| `constructor(resource)` | store the resource reference | |
| 44 | +| `toResource(): T` | unwrap to the underlying FHIR object | |
| 45 | + |
| 46 | +### 5. Field accessors |
| 47 | + |
| 48 | +Pair of `getXxx() / setXxx(value)` per field. Three sub-flavors, all driven by `factoryInfo` from `collectProfileFactoryInfo`: |
| 49 | + |
| 50 | +| Flavor | Source | Setter emitted? | Notes | |
| 51 | +|---|---|---|---| |
| 52 | +| **param accessor** | `factoryInfo.params` | yes | required fields constrained by the profile, plus base-required fields not otherwise covered | |
| 53 | +| **auto accessor** | `factoryInfo.accessors` (autoAccessors branch) | only if not in `factoryInfo.fixedFields` | for `valueConstraint`-driven fields and required-slice array fields | |
| 54 | +| **choice accessor** | `factoryInfo.accessors` (choice instances) | yes | non-promoted choice instances | |
| 55 | + |
| 56 | +### 6. Slice accessors |
| 57 | + |
| 58 | +Pair of `setXxxSlice(input) / getXxxSlice(mode?)` per `SliceDef` from `collectSliceDefs`. Sub-flavors driven by `SliceDef` fields: |
| 59 | + |
| 60 | +| Sub-flavor | Triggering shape | Setter | Getter | |
| 61 | +|---|---|---|---| |
| 62 | +| **unbounded** | `def.array && max === 0/undefined` | accepts `(T \| Flat)[]`, replaces all matched | returns `T[] \| undefined`, with `mode: 'flat'\|'raw'` overloads | |
| 63 | +| **single-element** | `def.max === 1` | accepts `T \| Flat`, replaces single match | returns `T \| undefined`, with `mode` overloads | |
| 64 | +| **constrained-choice** | `def.constrainedChoice !== undefined` | wraps via `wrapSliceChoice` around the variant | unwraps via `unwrapSliceChoice` | |
| 65 | +| **type-discriminator** | `def.typeDiscriminator === true` | uses typed base (`BundleEntry<Patient>`) | same | |
| 66 | + |
| 67 | +### 7. Extension accessors |
| 68 | + |
| 69 | +Pair of `setXxxExt(value) / getXxxExt(mode?)` per `ProfileExtension`. Three branches: |
| 70 | + |
| 71 | +| Branch | Triggering condition | Setter shape | Getter overloads | |
| 72 | +|---|---|---|---| |
| 73 | +| **complex** | `ext.isComplex && ext.subExtensions` | `Flat \| ProfileClass \| Extension` (or just `Flat` if no profile class resolved) | `'flat' \| 'profile' \| 'raw'` | |
| 74 | +| **single-value** | exactly one `valueFieldTypes` entry | value-type \| `ProfileClass \| Extension` if profile class resolved | `'flat' \| 'profile' \| 'raw'` | |
| 75 | +| **generic** | otherwise | `Omit<Extension, "url"> \| Extension` | returns `Extension \| undefined` (no overloads) | |
| 76 | + |
| 77 | +Cross-cutting concern: nested-path extensions (e.g. `Patient.address.extension`) use `ensurePath` to walk into the target object before pushing. |
| 78 | + |
| 79 | +### 8. Validation |
| 80 | + |
| 81 | +| Method | Source | |
| 82 | +|---|---| |
| 83 | +| `validate(): { errors: string[]; warnings: string[] }` | `generateValidateMethod` | |
| 84 | + |
| 85 | +Per-field checks emitted into the body, from `collectRegularFieldValidation`: |
| 86 | +- `validateRequired` — required fields |
| 87 | +- `validateExcluded` — excluded fields and choice prohibitions |
| 88 | +- `validateFixedValue` — `valueConstraint`-driven exact values |
| 89 | +- `validateEnum` — closed enums (errors) vs open enums (warnings) |
| 90 | +- `validateMustSupport` — `mustSupport && !required` (warning) |
| 91 | +- `validateReference` — reference target type names |
| 92 | +- `validateSliceCardinality` — slice min/max |
| 93 | +- `validateSliceFields` — required fields inside matched slice elements (incl. constrained-choice variant) |
| 94 | +- `validateChoiceRequired` — required choice declarations |
| 95 | + |
| 96 | +## Helper imports |
| 97 | + |
| 98 | +Static asset imports from `assets/api/writer-generator/typescript/profile-helpers.ts`, gated by emitted-method footprint via `generateProfileHelpersImport`: |
| 99 | + |
| 100 | +`ensureProfile`, `isRawExtensionInput`, `applySliceMatch`, `matchesValue`, `setArraySlice`, `getArraySlice`, `setArraySliceAll`, `getArraySliceAll`, `ensureSliceDefaults`, `ensurePath`, `extractComplexExtension`, `wrapSliceChoice`, `unwrapSliceChoice`, `isExtension`, `getExtensionValue`, `pushExtension`, `upsertExtension`, plus the `validate*` set. |
| 101 | + |
| 102 | +## Proposed descriptor model |
| 103 | + |
| 104 | +The taxonomy collapses into eight emit roles. Each carries the data needed to render: |
| 105 | + |
| 106 | +``` |
| 107 | +StaticConstant — canonicalUrl, slice-match records |
| 108 | +Factory — from, is, apply, create, createResource, resolveInput |
| 109 | +Lifecycle — constructor, toResource |
| 110 | +FieldAccessor — param / auto / choice |
| 111 | +SliceAccessor — single / unbounded / constrained-choice / type-discriminator |
| 112 | +ExtensionAccessor — complex / single-value / generic |
| 113 | +Validator — validate() |
| 114 | +ModuleTypeAlias — Raw, Flat, *SliceFlat, *SliceFlatAll, *_extFlat |
| 115 | +``` |
| 116 | + |
| 117 | +The migration target: a `methods` array on `SnapshotProfileTypeSchema` (or a paired structure) where each entry is a tagged descriptor in one of these eight categories. The writer then dispatches on `entry.kind` and renders — no decision logic in the writer body. |
| 118 | + |
| 119 | +## What this enables |
| 120 | + |
| 121 | +- **Multi-language reuse** — Python/C# generators can consume the same descriptors with their own renderers. |
| 122 | +- **Snapshot inspectability** — `bun run src/cli/index.ts typeschema generate ...` could dump descriptors for review without running a language writer. |
| 123 | +- **Testability** — descriptors are pure data; today's tests assert generated text against snapshots, which is coarse. |
| 124 | +- **Stability** — adding a new method kind (e.g. a future `clone()` or `diff(other)`) is "add a descriptor variant + a renderer" instead of "edit the writer's monolithic generator". |
| 125 | + |
| 126 | +## Out of scope |
| 127 | + |
| 128 | +- Concrete method-spec record types (the `Param`, `SliceDef`, `ExtensionMethodSpec` shapes already exist in the writer; the migration step formalizes them as snapshot-level types). |
| 129 | +- Migration order across writer modules. |
| 130 | +- Multi-language renderer details. |
| 131 | + |
| 132 | +Those land in follow-up design notes once the descriptor model is agreed. |
0 commit comments