@workos/oagen-emitters 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lint.yml +1 -1
- package/.github/workflows/release.yml +1 -1
- package/.node-version +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +20 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-CpO8rePT.mjs → plugin-DAa-HsN5.mjs} +2655 -1930
- package/dist/plugin-DAa-HsN5.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +6 -6
- package/src/go/index.ts +6 -1
- package/src/kotlin/index.ts +9 -3
- package/src/node/discriminated-models.ts +197 -50
- package/src/node/index.ts +7 -1
- package/src/node/resources.ts +161 -10
- package/src/rust/resources.ts +78 -29
- package/src/rust/tests.ts +15 -4
- package/src/shared/union-flatten.ts +201 -0
- package/test/node/discriminated-pure-oneof.test.ts +108 -0
- package/test/node/resources.test.ts +147 -0
- package/test/rust/resources.test.ts +143 -3
- package/test/shared/union-flatten.test.ts +174 -0
- package/dist/plugin-CpO8rePT.mjs.map +0 -1
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as workosEmittersPlugin } from "./plugin-
|
|
1
|
+
import { t as workosEmittersPlugin } from "./plugin-DAa-HsN5.mjs";
|
|
2
2
|
export { workosEmittersPlugin };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workos/oagen-emitters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "WorkOS' oagen emitters",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "WorkOS",
|
|
@@ -42,18 +42,18 @@
|
|
|
42
42
|
"@commitlint/config-conventional": "^21.0.2",
|
|
43
43
|
"@types/node": "^25.9.3",
|
|
44
44
|
"husky": "^9.1.7",
|
|
45
|
-
"oxfmt": "^0.
|
|
46
|
-
"oxlint": "^1.
|
|
45
|
+
"oxfmt": "^0.55.0",
|
|
46
|
+
"oxlint": "^1.70.0",
|
|
47
47
|
"prettier": "^3.8.4",
|
|
48
|
-
"tsdown": "^0.22.
|
|
48
|
+
"tsdown": "^0.22.3",
|
|
49
49
|
"tsx": "^4.22.4",
|
|
50
50
|
"typescript": "^6.0.3",
|
|
51
|
-
"vitest": "^4.1.
|
|
51
|
+
"vitest": "^4.1.9"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=24.10.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@workos/oagen": "^0.22.
|
|
57
|
+
"@workos/oagen": "^0.22.6"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/go/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
|
|
12
12
|
import { generateModels } from './models.js';
|
|
13
13
|
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
14
|
+
import { flattenDiscriminatedUnionFields } from '../shared/union-flatten.js';
|
|
14
15
|
import { generateEnums } from './enums.js';
|
|
15
16
|
import { generateResources } from './resources.js';
|
|
16
17
|
import { generateClient } from './client.js';
|
|
@@ -47,7 +48,11 @@ export const goEmitter: Emitter = {
|
|
|
47
48
|
}
|
|
48
49
|
return m;
|
|
49
50
|
});
|
|
50
|
-
|
|
51
|
+
// Go has no sum types: a discriminated-union field (e.g. ApiKey.owner)
|
|
52
|
+
// renders as its first variant, dropping fields that only exist on later
|
|
53
|
+
// variants (organization_id on the user owner). Flatten such unions into a
|
|
54
|
+
// single superset struct so every variant field survives.
|
|
55
|
+
return ensureTrailingNewlines(generateModels(flattenDiscriminatedUnionFields(goModels), ctx));
|
|
51
56
|
},
|
|
52
57
|
|
|
53
58
|
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
package/src/kotlin/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { generateClient } from './client.js';
|
|
|
18
18
|
import { generateTests } from './tests.js';
|
|
19
19
|
import { buildOperationsMap } from './manifest.js';
|
|
20
20
|
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
21
|
+
import { flattenDiscriminatedUnionFields } from '../shared/union-flatten.js';
|
|
21
22
|
|
|
22
23
|
/** Ensure every generated file ends with a trailing newline. */
|
|
23
24
|
function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
@@ -49,7 +50,11 @@ export const kotlinEmitter: Emitter = {
|
|
|
49
50
|
}
|
|
50
51
|
return m;
|
|
51
52
|
});
|
|
52
|
-
|
|
53
|
+
// Kotlin renders a discriminated-union field as its first variant's data
|
|
54
|
+
// class, so fields unique to later variants (organization_id on the user
|
|
55
|
+
// owner) are lost. Flatten such unions into one superset data class so
|
|
56
|
+
// every variant field is reachable.
|
|
57
|
+
return ensureTrailingNewlines(generateModels(flattenDiscriminatedUnionFields(kotlinModels), ctx));
|
|
53
58
|
},
|
|
54
59
|
|
|
55
60
|
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
@@ -78,8 +83,9 @@ export const kotlinEmitter: Emitter = {
|
|
|
78
83
|
|
|
79
84
|
generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
80
85
|
// Pass enriched models so round-trip tests see the full field set
|
|
81
|
-
// (including optional oneOf-enriched fields
|
|
82
|
-
|
|
86
|
+
// (including optional oneOf-enriched fields and flattened discriminated-
|
|
87
|
+
// union owner fields) and can filter accurately.
|
|
88
|
+
const enrichedModels = flattenDiscriminatedUnionFields(enrichModelsFromSpec(spec.models));
|
|
83
89
|
const enrichedSpec: ApiSpec = { ...spec, models: enrichedModels };
|
|
84
90
|
return ensureTrailingNewlines(generateTests(enrichedSpec, { ...ctx, spec: enrichedSpec }));
|
|
85
91
|
},
|
|
@@ -50,13 +50,17 @@ interface FieldSpec {
|
|
|
50
50
|
modelDeps: Set<string>;
|
|
51
51
|
/** Whether the field requires date parsing/formatting (format: date-time). */
|
|
52
52
|
hasDateTime: boolean;
|
|
53
|
+
/** Inline string-enum values, rendered as a literal union (e.g. `'a' | 'b'`). */
|
|
54
|
+
enumValues?: string[];
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
interface VariantSpec {
|
|
56
58
|
/** Domain interface name suffix, e.g. `OAuth`, `M2M`. */
|
|
57
59
|
nameSuffix: string;
|
|
58
|
-
/** Discriminator
|
|
60
|
+
/** Discriminator value, e.g. `oauth`, `m2m`, or `true`/`false`. */
|
|
59
61
|
discriminatorValue: string;
|
|
62
|
+
/** Whether the discriminator value is a boolean literal (emit unquoted). */
|
|
63
|
+
discriminatorIsBoolean?: boolean;
|
|
60
64
|
/** Fields specific to this variant (excluding the discriminator). */
|
|
61
65
|
fields: FieldSpec[];
|
|
62
66
|
}
|
|
@@ -72,6 +76,13 @@ interface DiscriminatedShape {
|
|
|
72
76
|
/** Description from the OpenAPI spec, if present. */
|
|
73
77
|
discriminatorDescription?: string;
|
|
74
78
|
variants: VariantSpec[];
|
|
79
|
+
/**
|
|
80
|
+
* Set for pure `oneOf` schemas (no `allOf` base wrapper). These are emitted
|
|
81
|
+
* as an inline anonymous union (`type X = { … } | { … }`) rather than as a
|
|
82
|
+
* set of named variant interfaces, which keeps two-variant unions — e.g. the
|
|
83
|
+
* boolean-discriminated `active: true | false` token response — readable.
|
|
84
|
+
*/
|
|
85
|
+
inlineUnion?: boolean;
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
// ---------------------------------------------------------------------------
|
|
@@ -83,25 +94,45 @@ export function detectDiscriminatedShape(
|
|
|
83
94
|
rawSchemas: Record<string, RawSchema>,
|
|
84
95
|
): DiscriminatedShape | null {
|
|
85
96
|
const schema = rawSchemas[modelName];
|
|
86
|
-
if (!schema
|
|
97
|
+
if (!schema) return null;
|
|
87
98
|
|
|
88
|
-
// The expected shape: allOf contains exactly one base object and one oneOf
|
|
89
|
-
// wrapper. The base contributes shared fields; the oneOf contributes
|
|
90
|
-
// variant-specific fields.
|
|
91
99
|
let baseObject: RawSchema | null = null;
|
|
92
100
|
let oneOfVariants: RawSchema[] | null = null;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
let inlineUnion = false;
|
|
102
|
+
|
|
103
|
+
if (schema.allOf) {
|
|
104
|
+
// `allOf [base, oneOf [variant, …]]`: the base contributes shared fields;
|
|
105
|
+
// the oneOf contributes variant-specific fields. Emitted as named variant
|
|
106
|
+
// interfaces (one per variant) plus a union alias.
|
|
107
|
+
for (const member of schema.allOf) {
|
|
108
|
+
const resolved = resolveRef(member, rawSchemas);
|
|
109
|
+
if (resolved.oneOf) {
|
|
110
|
+
if (oneOfVariants) return null; // unexpected: multiple oneOf branches at top
|
|
111
|
+
oneOfVariants = resolved.oneOf;
|
|
112
|
+
} else if (resolved.properties) {
|
|
113
|
+
baseObject = mergeBase(baseObject, resolved);
|
|
114
|
+
} else if (resolved.allOf) {
|
|
115
|
+
// Nested allOf at top: walk it
|
|
116
|
+
const nestedBase = flattenObjectAllOf(resolved, rawSchemas);
|
|
117
|
+
baseObject = mergeBase(baseObject, nestedBase);
|
|
118
|
+
}
|
|
104
119
|
}
|
|
120
|
+
} else if (schema.oneOf && schema.oneOf.length >= 2) {
|
|
121
|
+
// Pure `oneOf` (no base wrapper): every branch must be a plain inline
|
|
122
|
+
// object so a shared const discriminator can tell them apart. This is the
|
|
123
|
+
// discriminated-union shape the parser would otherwise flatten into a
|
|
124
|
+
// single all-optional interface (e.g. the boolean-discriminated token
|
|
125
|
+
// response `{ active: true; … } | { active: false; … }`). The
|
|
126
|
+
// mutually-exclusive-field-group oneOf (no shared discriminator) is left
|
|
127
|
+
// alone by the `findSharedDiscriminator` check below.
|
|
128
|
+
const allInlineObjects = schema.oneOf.every(
|
|
129
|
+
(v) => v.properties && !v.$ref && (v.type === 'object' || !v.type) && !v.allOf && !v.oneOf,
|
|
130
|
+
);
|
|
131
|
+
if (!allInlineObjects) return null;
|
|
132
|
+
oneOfVariants = schema.oneOf;
|
|
133
|
+
inlineUnion = true;
|
|
134
|
+
} else {
|
|
135
|
+
return null;
|
|
105
136
|
}
|
|
106
137
|
if (!oneOfVariants || oneOfVariants.length < 2) return null;
|
|
107
138
|
|
|
@@ -120,12 +151,13 @@ export function detectDiscriminatedShape(
|
|
|
120
151
|
|
|
121
152
|
// Build variant specs.
|
|
122
153
|
const variants: VariantSpec[] = flattenedVariants
|
|
123
|
-
.map((fv) => {
|
|
124
|
-
const discValue =
|
|
125
|
-
if (
|
|
154
|
+
.map((fv): VariantSpec | null => {
|
|
155
|
+
const discValue = readConst(fv.alwaysProperties.get(discProp));
|
|
156
|
+
if (discValue === null) return null;
|
|
126
157
|
return {
|
|
127
|
-
nameSuffix: variantNameSuffix(discValue),
|
|
128
|
-
discriminatorValue: discValue,
|
|
158
|
+
nameSuffix: variantNameSuffix(String(discValue)),
|
|
159
|
+
discriminatorValue: String(discValue),
|
|
160
|
+
discriminatorIsBoolean: typeof discValue === 'boolean',
|
|
129
161
|
fields: variantFields(fv, discProp, modelName),
|
|
130
162
|
};
|
|
131
163
|
})
|
|
@@ -144,6 +176,7 @@ export function detectDiscriminatedShape(
|
|
|
144
176
|
discriminatorPropertyDomain: toCamelCase(discProp),
|
|
145
177
|
discriminatorDescription,
|
|
146
178
|
variants,
|
|
179
|
+
inlineUnion,
|
|
147
180
|
};
|
|
148
181
|
}
|
|
149
182
|
|
|
@@ -302,12 +335,12 @@ function findSharedDiscriminator(variants: FlattenedVariant[]): string | null {
|
|
|
302
335
|
const values: string[] = [];
|
|
303
336
|
for (const v of variants) {
|
|
304
337
|
const schema = v.alwaysProperties.get(propName);
|
|
305
|
-
const val =
|
|
338
|
+
const val = readConst(schema);
|
|
306
339
|
if (val === null) {
|
|
307
340
|
allConst = false;
|
|
308
341
|
break;
|
|
309
342
|
}
|
|
310
|
-
values.push(val);
|
|
343
|
+
values.push(String(val));
|
|
311
344
|
}
|
|
312
345
|
if (allConst && new Set(values).size === values.length) {
|
|
313
346
|
return propName;
|
|
@@ -316,11 +349,17 @@ function findSharedDiscriminator(variants: FlattenedVariant[]): string | null {
|
|
|
316
349
|
return null;
|
|
317
350
|
}
|
|
318
351
|
|
|
319
|
-
|
|
352
|
+
/**
|
|
353
|
+
* Read a discriminator value pinned by `const` (or a single-value `enum`).
|
|
354
|
+
* Supports string and boolean literals — the latter drives the
|
|
355
|
+
* `active: true | false` style token response union.
|
|
356
|
+
*/
|
|
357
|
+
function readConst(schema: RawSchema | undefined | null): string | boolean | null {
|
|
320
358
|
if (!schema) return null;
|
|
321
|
-
if (typeof schema.const === 'string') return schema.const;
|
|
322
|
-
if (Array.isArray(schema.enum) && schema.enum.length === 1
|
|
323
|
-
|
|
359
|
+
if (typeof schema.const === 'string' || typeof schema.const === 'boolean') return schema.const;
|
|
360
|
+
if (Array.isArray(schema.enum) && schema.enum.length === 1) {
|
|
361
|
+
const only = schema.enum[0];
|
|
362
|
+
if (typeof only === 'string' || typeof only === 'boolean') return only;
|
|
324
363
|
}
|
|
325
364
|
return null;
|
|
326
365
|
}
|
|
@@ -360,6 +399,10 @@ function buildField(rawName: string, schema: RawSchema, required: boolean, paren
|
|
|
360
399
|
const modelDeps = new Set<string>();
|
|
361
400
|
const domainType = rawSchemaToTS(schema, parentName, rawName, false, modelDeps);
|
|
362
401
|
const wireType = rawSchemaToTS(schema, parentName, rawName, true, modelDeps);
|
|
402
|
+
const enumValues =
|
|
403
|
+
Array.isArray(schema.enum) && schema.enum.length > 0 && schema.enum.every((e) => typeof e === 'string')
|
|
404
|
+
? (schema.enum as string[])
|
|
405
|
+
: undefined;
|
|
363
406
|
return {
|
|
364
407
|
name: rawName,
|
|
365
408
|
description: schema.description,
|
|
@@ -368,6 +411,7 @@ function buildField(rawName: string, schema: RawSchema, required: boolean, paren
|
|
|
368
411
|
wireType,
|
|
369
412
|
modelDeps,
|
|
370
413
|
hasDateTime: schemaHasDateTime(schema),
|
|
414
|
+
enumValues,
|
|
371
415
|
};
|
|
372
416
|
}
|
|
373
417
|
|
|
@@ -487,6 +531,15 @@ export function planDiscriminatedModels(models: Model[], ctx: EmitterContext): M
|
|
|
487
531
|
depDirMap.set(rawName, irModelDir.get(stripped)!);
|
|
488
532
|
}
|
|
489
533
|
}
|
|
534
|
+
// Synthetic IR models — e.g. inline-object variant fields like the token
|
|
535
|
+
// response's nested `access_token` — have PascalCase names that never appear
|
|
536
|
+
// in rawSchemas, so the loop above misses them. Seed their directories from
|
|
537
|
+
// irModelDir so `collectImports` (which only receives `depDirMap` via the
|
|
538
|
+
// plan) can resolve a cross-service inline-object dep instead of defaulting
|
|
539
|
+
// to a possibly-wrong same-dir import.
|
|
540
|
+
for (const [irName, dir] of irModelDir) {
|
|
541
|
+
if (!depDirMap.has(irName)) depDirMap.set(irName, dir);
|
|
542
|
+
}
|
|
490
543
|
|
|
491
544
|
for (const model of models) {
|
|
492
545
|
const shape = detectDiscriminatedShape(model.name, rawSchemas);
|
|
@@ -503,7 +556,17 @@ export function planDiscriminatedModels(models: Model[], ctx: EmitterContext): M
|
|
|
503
556
|
for (const d of field.modelDeps) allDeps.add(d);
|
|
504
557
|
}
|
|
505
558
|
}
|
|
506
|
-
|
|
559
|
+
// `modelDeps` may carry an inline-object synthetic name in raw form
|
|
560
|
+
// (`Parent_field`) while the resolution maps are keyed by the PascalCase IR
|
|
561
|
+
// model name (`ParentField`). Resolve either spelling before deciding a dep
|
|
562
|
+
// is unreachable — otherwise inline-object variant fields (e.g. the token
|
|
563
|
+
// response's nested `access_token`) would wrongly drop the whole plan.
|
|
564
|
+
const resolvable = (dep: string): boolean =>
|
|
565
|
+
depDirMap.has(dep) ||
|
|
566
|
+
irModelDir.has(dep) ||
|
|
567
|
+
depDirMap.has(toPascalCase(dep)) ||
|
|
568
|
+
irModelDir.has(toPascalCase(dep));
|
|
569
|
+
const hasUnresolvableDeps = [...allDeps].some((dep) => !resolvable(dep));
|
|
507
570
|
if (hasUnresolvableDeps) continue;
|
|
508
571
|
const modelDir = resolveDir(modelToService.get(model.name));
|
|
509
572
|
plans.set(model.name, { shape, modelDir, depDirMap });
|
|
@@ -525,6 +588,7 @@ export function generateDiscriminatedFiles(
|
|
|
525
588
|
|
|
526
589
|
function buildInterfaceFile(plan: DiscriminatedPlan, _ctx: EmitterContext): GeneratedFile {
|
|
527
590
|
const { shape, modelDir } = plan;
|
|
591
|
+
if (shape.inlineUnion) return buildInlineUnionFile(plan);
|
|
528
592
|
const domain = toPascalCase(shape.modelName);
|
|
529
593
|
const wire = wireInterfaceName(domain);
|
|
530
594
|
const lines: string[] = [];
|
|
@@ -561,6 +625,53 @@ function buildInterfaceFile(plan: DiscriminatedPlan, _ctx: EmitterContext): Gene
|
|
|
561
625
|
};
|
|
562
626
|
}
|
|
563
627
|
|
|
628
|
+
/**
|
|
629
|
+
* Emit a pure-`oneOf` discriminated union as a single inline type alias
|
|
630
|
+
* (`export type X = { … } | { … }`) for both the domain and wire shapes. Used
|
|
631
|
+
* instead of named per-variant interfaces, which read poorly for small
|
|
632
|
+
* (often two-variant, boolean-discriminated) unions like the token response.
|
|
633
|
+
*/
|
|
634
|
+
function buildInlineUnionFile(plan: DiscriminatedPlan): GeneratedFile {
|
|
635
|
+
const { shape, modelDir } = plan;
|
|
636
|
+
const domain = toPascalCase(shape.modelName);
|
|
637
|
+
const wire = wireInterfaceName(domain);
|
|
638
|
+
const lines: string[] = [];
|
|
639
|
+
|
|
640
|
+
const imports = collectImports(plan);
|
|
641
|
+
if (imports.length > 0) {
|
|
642
|
+
for (const imp of imports) {
|
|
643
|
+
lines.push(`import type { ${imp.symbols.join(', ')} } from '${imp.path}';`);
|
|
644
|
+
}
|
|
645
|
+
lines.push('');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
lines.push(...buildInlineUnionAlias(domain, shape, /*isWire*/ false));
|
|
649
|
+
lines.push('');
|
|
650
|
+
lines.push(...buildInlineUnionAlias(wire, shape, /*isWire*/ true));
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
path: `src/${modelDir}/interfaces/${fileName(shape.modelName)}.interface.ts`,
|
|
654
|
+
content: lines.join('\n') + '\n',
|
|
655
|
+
overwriteExisting: true,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function buildInlineUnionAlias(name: string, shape: DiscriminatedShape, isWire: boolean): string[] {
|
|
660
|
+
const lines: string[] = [`export type ${name} =`];
|
|
661
|
+
shape.variants.forEach((variant, idx) => {
|
|
662
|
+
const isLast = idx === shape.variants.length - 1;
|
|
663
|
+
const discKey = isWire ? shape.discriminatorProperty : shape.discriminatorPropertyDomain;
|
|
664
|
+
const members = [`${discKey}: ${discLiteral(variant)}`];
|
|
665
|
+
for (const field of variant.fields) {
|
|
666
|
+
const key = isWire ? field.name : toCamelCase(field.name);
|
|
667
|
+
const opt = field.required ? '' : '?';
|
|
668
|
+
members.push(`${key}${opt}: ${inlineFieldType(field, isWire)}`);
|
|
669
|
+
}
|
|
670
|
+
lines.push(` | { ${members.join('; ')} }${isLast ? ';' : ''}`);
|
|
671
|
+
});
|
|
672
|
+
return lines;
|
|
673
|
+
}
|
|
674
|
+
|
|
564
675
|
function buildInterfaceBody(name: string, shape: DiscriminatedShape, variant: VariantSpec, isWire: boolean): string[] {
|
|
565
676
|
const lines: string[] = [];
|
|
566
677
|
lines.push(`export interface ${name} {`);
|
|
@@ -578,7 +689,7 @@ function buildInterfaceBody(name: string, shape: DiscriminatedShape, variant: Va
|
|
|
578
689
|
if (shape.discriminatorDescription) {
|
|
579
690
|
lines.push(` /** ${shape.discriminatorDescription} */`);
|
|
580
691
|
}
|
|
581
|
-
lines.push(` ${discKey}:
|
|
692
|
+
lines.push(` ${discKey}: ${discLiteral(variant)};`);
|
|
582
693
|
// Variant-specific fields
|
|
583
694
|
for (const field of variant.fields) {
|
|
584
695
|
pushFieldLine(lines, field, isWire);
|
|
@@ -587,6 +698,31 @@ function buildInterfaceBody(name: string, shape: DiscriminatedShape, variant: Va
|
|
|
587
698
|
return lines;
|
|
588
699
|
}
|
|
589
700
|
|
|
701
|
+
/**
|
|
702
|
+
* The discriminator value as a TS literal: quoted for strings (`'oauth'`),
|
|
703
|
+
* bare for booleans (`true`).
|
|
704
|
+
*/
|
|
705
|
+
function discLiteral(variant: VariantSpec): string {
|
|
706
|
+
return variant.discriminatorIsBoolean ? variant.discriminatorValue : `'${variant.discriminatorValue}'`;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Field type for an inline-union member: a literal union for inline string
|
|
711
|
+
* enums, otherwise the resolved domain/wire type.
|
|
712
|
+
*
|
|
713
|
+
* The `enumValues` branch deliberately ignores `isWire`: `enumValues` is only
|
|
714
|
+
* populated for string enums (see `buildField`), whose domain and wire
|
|
715
|
+
* representations are identical literal unions. Any field whose domain/wire
|
|
716
|
+
* types actually differ (model refs, dates, non-string enums) leaves
|
|
717
|
+
* `enumValues` undefined and falls through to the type-specific branch.
|
|
718
|
+
*/
|
|
719
|
+
function inlineFieldType(field: FieldSpec, isWire: boolean): string {
|
|
720
|
+
if (field.enumValues) {
|
|
721
|
+
return field.enumValues.map((v) => `'${v}'`).join(' | ');
|
|
722
|
+
}
|
|
723
|
+
return isWire ? field.wireType : field.domainType;
|
|
724
|
+
}
|
|
725
|
+
|
|
590
726
|
function pushFieldLine(lines: string[], field: FieldSpec, isWire: boolean): void {
|
|
591
727
|
const key = isWire ? field.name : toCamelCase(field.name);
|
|
592
728
|
const opt = field.required ? '' : '?';
|
|
@@ -617,7 +753,10 @@ function collectImports(plan: DiscriminatedPlan): ImportSpec[] {
|
|
|
617
753
|
const domain = toPascalCase(dep);
|
|
618
754
|
const wire = wireInterfaceName(domain);
|
|
619
755
|
const symbols = wire !== domain ? [domain, wire] : [domain];
|
|
620
|
-
|
|
756
|
+
// `dep` may be a raw spec name or a synthetic inline-object name in snake
|
|
757
|
+
// form (`Parent_field`); `depDirMap` keys the latter under its PascalCase IR
|
|
758
|
+
// name, so fall back to that spelling before defaulting to a same-dir path.
|
|
759
|
+
const depDir = plan.depDirMap.get(dep) ?? plan.depDirMap.get(domain);
|
|
621
760
|
const baseName = fileName(toSnakeFromPascal(domain));
|
|
622
761
|
let importPath: string;
|
|
623
762
|
if (!depDir || depDir === plan.modelDir) {
|
|
@@ -660,10 +799,16 @@ function buildSerializerFile(plan: DiscriminatedPlan, _ctx: EmitterContext): Gen
|
|
|
660
799
|
for (const variant of shape.variants)
|
|
661
800
|
for (const field of variant.fields) for (const d of field.modelDeps) allDeps.add(d);
|
|
662
801
|
|
|
802
|
+
// Pure-`oneOf` unions are response shapes (e.g. the token response): they are
|
|
803
|
+
// only ever deserialized, and their inline-object fields may have no
|
|
804
|
+
// serializer generated. Emit a deserializer only and import just the
|
|
805
|
+
// deserialize helpers — mirrors the published hand-written serializer.
|
|
806
|
+
const deserializeOnly = shape.inlineUnion === true;
|
|
663
807
|
for (const dep of [...allDeps].sort()) {
|
|
664
808
|
const depDomain = toPascalCase(dep);
|
|
665
809
|
const depFile = fileName(toSnakeFromPascal(depDomain));
|
|
666
|
-
|
|
810
|
+
const helpers = deserializeOnly ? `deserialize${depDomain}` : `deserialize${depDomain}, serialize${depDomain}`;
|
|
811
|
+
lines.push(`import { ${helpers} } from './${depFile}.serializer';`);
|
|
667
812
|
}
|
|
668
813
|
lines.push('');
|
|
669
814
|
|
|
@@ -671,12 +816,12 @@ function buildSerializerFile(plan: DiscriminatedPlan, _ctx: EmitterContext): Gen
|
|
|
671
816
|
lines.push(`export const deserialize${domain} = (response: ${wire}): ${domain} => {`);
|
|
672
817
|
lines.push(` switch (response.${shape.discriminatorProperty}) {`);
|
|
673
818
|
for (const variant of shape.variants) {
|
|
674
|
-
lines.push(` case
|
|
819
|
+
lines.push(` case ${discLiteral(variant)}:`);
|
|
675
820
|
lines.push(` return {`);
|
|
676
821
|
for (const field of shape.baseFields) {
|
|
677
822
|
lines.push(` ${assignmentLine(field, /*serialize*/ false, allDeps)},`);
|
|
678
823
|
}
|
|
679
|
-
lines.push(` ${shape.discriminatorPropertyDomain}:
|
|
824
|
+
lines.push(` ${shape.discriminatorPropertyDomain}: ${discLiteral(variant)},`);
|
|
680
825
|
for (const field of variant.fields) {
|
|
681
826
|
lines.push(` ${assignmentLine(field, /*serialize*/ false, allDeps)},`);
|
|
682
827
|
}
|
|
@@ -684,29 +829,31 @@ function buildSerializerFile(plan: DiscriminatedPlan, _ctx: EmitterContext): Gen
|
|
|
684
829
|
}
|
|
685
830
|
lines.push(` default:`);
|
|
686
831
|
lines.push(
|
|
687
|
-
` throw new Error(\`Unknown ${shape.discriminatorProperty}: \${(response as
|
|
832
|
+
` throw new Error(\`Unknown ${shape.discriminatorProperty}: \${String((response as Record<string, unknown>).${shape.discriminatorProperty})}\`);`,
|
|
688
833
|
);
|
|
689
834
|
lines.push(` }`);
|
|
690
835
|
lines.push(`};`);
|
|
691
|
-
lines.push('');
|
|
692
836
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
lines.push(`
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
lines.push(`
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
lines.push(` ${
|
|
837
|
+
if (!deserializeOnly) {
|
|
838
|
+
lines.push('');
|
|
839
|
+
// Serializer
|
|
840
|
+
lines.push(`export const serialize${domain} = (model: ${domain}): ${wire} => {`);
|
|
841
|
+
lines.push(` switch (model.${shape.discriminatorPropertyDomain}) {`);
|
|
842
|
+
for (const variant of shape.variants) {
|
|
843
|
+
lines.push(` case ${discLiteral(variant)}:`);
|
|
844
|
+
lines.push(` return {`);
|
|
845
|
+
for (const field of shape.baseFields) {
|
|
846
|
+
lines.push(` ${assignmentLine(field, /*serialize*/ true, allDeps)},`);
|
|
847
|
+
}
|
|
848
|
+
lines.push(` ${shape.discriminatorProperty}: ${discLiteral(variant)},`);
|
|
849
|
+
for (const field of variant.fields) {
|
|
850
|
+
lines.push(` ${assignmentLine(field, /*serialize*/ true, allDeps)},`);
|
|
851
|
+
}
|
|
852
|
+
lines.push(` };`);
|
|
705
853
|
}
|
|
706
|
-
lines.push(`
|
|
854
|
+
lines.push(` }`);
|
|
855
|
+
lines.push(`};`);
|
|
707
856
|
}
|
|
708
|
-
lines.push(` }`);
|
|
709
|
-
lines.push(`};`);
|
|
710
857
|
|
|
711
858
|
return {
|
|
712
859
|
path: `src/${modelDir}/serializers/${fileName(shape.modelName)}.serializer.ts`,
|
package/src/node/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { generateResources, resolveResourceClassName, resolveResourceDir } from
|
|
|
18
18
|
import { generateClient } from './client.js';
|
|
19
19
|
import { generateTests as generateTestFiles } from './tests.js';
|
|
20
20
|
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
21
|
+
import { flattenDiscriminatedUnionFields } from '../shared/union-flatten.js';
|
|
21
22
|
import { planDiscriminatedModels, generateDiscriminatedFiles } from './discriminated-models.js';
|
|
22
23
|
import {
|
|
23
24
|
buildLiveSurface,
|
|
@@ -766,7 +767,7 @@ function carryForwardManagedFiles(ctx: EmitterContext, surface: LiveSurface): Ge
|
|
|
766
767
|
function enrichModelsForNode(models: Model[]): Model[] {
|
|
767
768
|
const enriched = enrichModelsFromSpec(models);
|
|
768
769
|
const originalByName = new Map(models.map((m) => [m.name, m]));
|
|
769
|
-
|
|
770
|
+
const restored = enriched.map((m) => {
|
|
770
771
|
if ((m as { discriminator?: unknown }).discriminator && m.fields.length === 0) {
|
|
771
772
|
const original = originalByName.get(m.name);
|
|
772
773
|
if (original && original.fields.length > 0) {
|
|
@@ -775,6 +776,11 @@ function enrichModelsForNode(models: Model[]): Model[] {
|
|
|
775
776
|
}
|
|
776
777
|
return m;
|
|
777
778
|
});
|
|
779
|
+
// Field-level discriminated unions (e.g. ApiKey.owner) otherwise render as
|
|
780
|
+
// `FirstVariant | SecondVariant`; collapse them to one flat superset
|
|
781
|
+
// interface so callers see every variant field (organization_id on the user
|
|
782
|
+
// owner) on a single type — parity with the other flat-emit languages.
|
|
783
|
+
return flattenDiscriminatedUnionFields(restored);
|
|
778
784
|
}
|
|
779
785
|
|
|
780
786
|
export const nodeEmitter: Emitter = {
|