Skip to content

Commit a572bc1

Browse files
committed
docs: classify the methods the TS writer emits for profiles
Captures the 8 emit categories the TS writer currently produces inside a profile module (constants, factory, lifecycle, field/slice/extension accessors, validator, type aliases), the descriptor data each needs, and the migration target: a `methods` array on SnapshotProfileTypeSchema where each entry is a tagged descriptor and the writer becomes a renderer that dispatches on `entry.kind`. This is the foundation for the follow-up PR series that moves method derivation out of the writer.
1 parent 5c8b5f9 commit a572bc1

1 file changed

Lines changed: 132 additions & 0 deletions

File tree

docs/design/profile-methods.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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

Comments
 (0)