Skip to content

Commit 6a464d7

Browse files
committed
fix(generator): give complex sub-extensions a valid, unfrozen date default
The two latent defects the previous commit recorded but left alone. complexExtValueDefault supplies the value[x] default for a sub-extension of a complex extension: - dateTime, instant and date fell through to `default: value`, emitting valueDateTime: "value", which is not a valid FHIR dateTime. - Period interpolated a generation-time new Date(), freezing the wall clock into the emitted source — the same defect just removed from getTypePlaceholder. Both now use the __DATE_EXPR__ / __DATE10_EXPR__ placeholders that the class emitter resolves into randomDate(), so the value is valid and fresh per call. The risk that held this back is cleared. The result becomes fixedValue on the extension child, and a placeholder leaking into a validator comparison would emit a check against the literal "__DATE_EXPR__". Every validator consumer of fixedValue guards on typeof string/number/boolean, and buildFixedValueValidations emits nothing for any other shape, so an array-of-objects fixedValue never reaches a comparison. Only the class emitter renders it, and objectToCodeExpression resolves the placeholder in a nested object value — checked by executing it, not by reading it. Verified on us-core@9.0.0: zero baked timestamps, zero unresolved placeholders in the emitted output, and HL7 parity unchanged at 54/54 random and 54/54 empty. Tests written first; 4 of 5 failed for the intended reasons before the change.
1 parent cab82f1 commit 6a464d7

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

src/generator/parser/sliceAggregatorComposition.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ export function complexExtValueDefault(fhirType: string): unknown {
2323
case 'canonical': return 'https://example.org';
2424
case 'Reference': return { reference: 'Resource/example' };
2525
case 'Quantity': return { value: 1, unit: 'unit' };
26-
case 'Period': return { start: new Date().toISOString() };
26+
// Dates go through the same placeholders the class emitter resolves into
27+
// randomDate(), so a sub-extension gets a fresh value per random() call.
28+
// Previously dateTime/instant/date fell through to 'value', which is not a
29+
// valid FHIR dateTime, and Period froze the generation-time clock into the
30+
// emitted source.
31+
case 'dateTime':
32+
case 'instant': return '__DATE_EXPR__';
33+
case 'date': return '__DATE10_EXPR__';
34+
case 'Period': return { start: '__DATE_EXPR__' };
2735
default: return 'value';
2836
}
2937
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* complexExtValueDefault supplies the value[x] default for a sub-extension of a
3+
* complex extension. Its result becomes `fixedValue` on the extension child and is
4+
* rendered into a generated class by objectToCodeExpression, which resolves the
5+
* __DATE_EXPR__ placeholders into randomDate() calls.
6+
*
7+
* Two defects it had:
8+
* - dateTime/instant/date fell through to `default: 'value'`, emitting
9+
* `valueDateTime: "value"` — not a valid FHIR dateTime.
10+
* - Period interpolated a generation-time `new Date()`, freezing the wall clock
11+
* into the emitted source.
12+
*
13+
* Safe to express as placeholders: every validator consumer of `fixedValue` guards
14+
* on typeof string/number/boolean, so an array-of-objects fixedValue never reaches
15+
* a comparison — only the class emitter renders it.
16+
*/
17+
import { describe, it, expect } from 'vitest';
18+
import { complexExtValueDefault } from '../generator/parser/sliceAggregatorComposition.js';
19+
import { objectToCodeExpression } from '../generator/emitters/class/sliceElementDefaults.js';
20+
21+
const ISO_TIMESTAMP = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
22+
23+
/** Render the way sliceAggregator builds a sub-extension, then emits it. */
24+
function renderSubExtension(fhirType: string, valueProp: string): string {
25+
return objectToCodeExpression({
26+
url: 'subExt',
27+
[valueProp]: complexExtValueDefault(fhirType),
28+
});
29+
}
30+
31+
describe('complexExtValueDefault date handling', () => {
32+
it('does not answer "value" for a dateTime', () => {
33+
expect(complexExtValueDefault('dateTime')).not.toBe('value');
34+
expect(complexExtValueDefault('instant')).not.toBe('value');
35+
expect(complexExtValueDefault('date')).not.toBe('value');
36+
});
37+
38+
it('renders dateTime and instant as a runtime date call', () => {
39+
expect(renderSubExtension('dateTime', 'valueDateTime')).toContain('randomDate()');
40+
expect(renderSubExtension('instant', 'valueInstant')).toContain('randomDate()');
41+
});
42+
43+
it('renders date at day precision', () => {
44+
const rendered = renderSubExtension('date', 'valueDate');
45+
expect(rendered).toContain('randomDate()');
46+
expect(rendered).toContain('slice(0, 10)');
47+
});
48+
49+
it('freezes no timestamp for Period, and stays stable across calls', async () => {
50+
const first = JSON.stringify(complexExtValueDefault('Period'));
51+
expect(first).not.toMatch(ISO_TIMESTAMP);
52+
await new Promise(r => setTimeout(r, 5));
53+
expect(JSON.stringify(complexExtValueDefault('Period'))).toBe(first);
54+
expect(renderSubExtension('Period', 'valuePeriod')).toContain('randomDate()');
55+
});
56+
57+
it('leaves the non-date defaults alone', () => {
58+
expect(complexExtValueDefault('boolean')).toBe(true);
59+
expect(complexExtValueDefault('string')).toBe('value');
60+
expect(complexExtValueDefault('code')).toBe('unknown');
61+
expect(complexExtValueDefault('integer')).toBe(1);
62+
expect(complexExtValueDefault('Reference')).toEqual({ reference: 'Resource/example' });
63+
// An unknown type still falls back to a plain string.
64+
expect(complexExtValueDefault('SomethingElse')).toBe('value');
65+
});
66+
});

0 commit comments

Comments
 (0)