@workos/oagen-emitters 0.13.0 → 0.14.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-B9F2jmwy.mjs → plugin-BxVeu2v9.mjs} +610 -22
- package/dist/plugin-BxVeu2v9.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +2 -2
- package/src/node/discriminated-models.ts +735 -0
- package/src/node/index.ts +56 -5
- package/src/node/models.ts +15 -1
- package/src/node/node-overrides.ts +49 -6
- package/src/php/index.ts +25 -2
- package/src/ruby/index.ts +27 -2
- package/src/rust/index.ts +26 -2
- package/src/shared/model-utils.ts +15 -5
- package/dist/plugin-B9F2jmwy.mjs.map +0 -1
package/src/node/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { generateEnums as generateEnumFiles } from './enums.js';
|
|
|
16
16
|
import { generateResources, resolveResourceClassName, resolveResourceDir } from './resources.js';
|
|
17
17
|
import { generateClient } from './client.js';
|
|
18
18
|
import { generateTests as generateTestFiles } from './tests.js';
|
|
19
|
+
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
20
|
+
import { planDiscriminatedModels, generateDiscriminatedFiles } from './discriminated-models.js';
|
|
19
21
|
import { buildLiveSurface, emptyLiveSurface, setActiveLiveSurface, type LiveSurface } from './live-surface.js';
|
|
20
22
|
import {
|
|
21
23
|
setBaselineSerializedNames,
|
|
@@ -214,7 +216,10 @@ function computeOwnedServiceDirs(ctx: EmitterContext): Set<string> {
|
|
|
214
216
|
const mountGroups = groupByMount(ctx);
|
|
215
217
|
const services =
|
|
216
218
|
mountGroups.size > 0
|
|
217
|
-
? [...mountGroups].map(([name, group]) => ({
|
|
219
|
+
? [...mountGroups].map(([name, group]) => ({
|
|
220
|
+
name,
|
|
221
|
+
operations: group.operations,
|
|
222
|
+
}))
|
|
218
223
|
: ctx.spec.services;
|
|
219
224
|
const { resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
|
|
220
225
|
|
|
@@ -233,7 +238,10 @@ function computeAdoptedServiceDirs(ctx: EmitterContext, surface: LiveSurface): S
|
|
|
233
238
|
const mountGroups = groupByMount(ctx);
|
|
234
239
|
const services =
|
|
235
240
|
mountGroups.size > 0
|
|
236
|
-
? [...mountGroups].map(([name, group]) => ({
|
|
241
|
+
? [...mountGroups].map(([name, group]) => ({
|
|
242
|
+
name,
|
|
243
|
+
operations: group.operations,
|
|
244
|
+
}))
|
|
237
245
|
: ctx.spec.services;
|
|
238
246
|
const { resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
|
|
239
247
|
|
|
@@ -350,19 +358,58 @@ function applyLiveSurface(files: GeneratedFile[], ctx: EmitterContext, surface:
|
|
|
350
358
|
return out;
|
|
351
359
|
}
|
|
352
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Flatten oneOf / allOf+oneOf variant fields from the raw spec onto each
|
|
363
|
+
* model. `enrichModelsFromSpec` produces (a) extra optional fields on models
|
|
364
|
+
* whose schema is `allOf [base, oneOf [...]]`, and (b) synthetic models /
|
|
365
|
+
* enums for inline objects encountered inside variants (e.g. the inline
|
|
366
|
+
* `redirect_uris` item shape on `ConnectApplication`).
|
|
367
|
+
*
|
|
368
|
+
* Node, like Go / Kotlin / .NET, emits flat interfaces rather than a sum
|
|
369
|
+
* type, so on `enrichModelsFromSpec`-marked discriminated bases we restore
|
|
370
|
+
* the original IR fields — otherwise the base interface would be empty.
|
|
371
|
+
* A future change can emit a real TS discriminated union; for now the goal
|
|
372
|
+
* is parity with the other flat-emit languages so every variant field is
|
|
373
|
+
* at least reachable.
|
|
374
|
+
*/
|
|
375
|
+
function enrichModelsForNode(models: Model[]): Model[] {
|
|
376
|
+
const enriched = enrichModelsFromSpec(models);
|
|
377
|
+
const originalByName = new Map(models.map((m) => [m.name, m]));
|
|
378
|
+
return enriched.map((m) => {
|
|
379
|
+
if ((m as { discriminator?: unknown }).discriminator && m.fields.length === 0) {
|
|
380
|
+
const original = originalByName.get(m.name);
|
|
381
|
+
if (original && original.fields.length > 0) {
|
|
382
|
+
return { ...m, fields: original.fields };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return m;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
353
389
|
export const nodeEmitter: Emitter = {
|
|
354
390
|
language: 'node',
|
|
355
391
|
|
|
356
392
|
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
357
393
|
const nodeCtx = withNodeOperationOverrides(ctx);
|
|
358
394
|
const surface = getSurface(nodeCtx);
|
|
359
|
-
|
|
395
|
+
const enriched = enrichModelsForNode(models);
|
|
396
|
+
// Detect `allOf [base, oneOf [variant, …]]` schemas and hand them off
|
|
397
|
+
// to the discriminated-models module. Leave the model in the standard
|
|
398
|
+
// pipeline's input so its field-type deps stay reachable, but stash the
|
|
399
|
+
// name set on ctx so models.ts skips emitting an interface/serializer —
|
|
400
|
+
// the discriminated module owns those paths instead.
|
|
401
|
+
const discPlans = planDiscriminatedModels(enriched, nodeCtx);
|
|
402
|
+
(nodeCtx as { _discriminatedModelNames?: Set<string> })._discriminatedModelNames = new Set(discPlans.keys());
|
|
403
|
+
const standardFiles = generateModelsAndSerializers(enriched, nodeCtx);
|
|
404
|
+
const discFiles = generateDiscriminatedFiles(discPlans, nodeCtx);
|
|
405
|
+
return applyLiveSurface([...standardFiles, ...discFiles], nodeCtx, surface);
|
|
360
406
|
},
|
|
361
407
|
|
|
362
408
|
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
363
409
|
const nodeCtx = withNodeOperationOverrides(ctx);
|
|
364
410
|
const surface = getSurface(nodeCtx);
|
|
365
|
-
|
|
411
|
+
const syntheticEnums = getSyntheticEnums();
|
|
412
|
+
return applyLiveSurface(generateEnumFiles([...enums, ...syntheticEnums], nodeCtx), nodeCtx, surface);
|
|
366
413
|
},
|
|
367
414
|
|
|
368
415
|
generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
@@ -374,7 +421,11 @@ export const nodeEmitter: Emitter = {
|
|
|
374
421
|
generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
|
|
375
422
|
const nodeCtx = withNodeOperationOverrides(ctx);
|
|
376
423
|
const surface = getSurface(nodeCtx);
|
|
377
|
-
|
|
424
|
+
// `nodeCtx.spec` has the synthetic models that `enrichModelsFromSpec`
|
|
425
|
+
// produced (e.g. inline-object item types like `ConnectApplicationRedirectUri`).
|
|
426
|
+
// The `spec` param is the engine's pre-enrichment spec, so the barrel
|
|
427
|
+
// generator would miss those synthetic interfaces. Use the enriched one.
|
|
428
|
+
return applyLiveSurface(generateClient(nodeCtx.spec, nodeCtx), nodeCtx, surface);
|
|
378
429
|
},
|
|
379
430
|
|
|
380
431
|
// workos-node ships its own exception hierarchy under src/common/exceptions/.
|
package/src/node/models.ts
CHANGED
|
@@ -130,6 +130,13 @@ function isSupportedFieldType(
|
|
|
130
130
|
// silently dropped on first emission because the target interface
|
|
131
131
|
// (`UserObject` under the adopted `connect/` dir) hasn't landed yet.
|
|
132
132
|
if (isAdoptedModelName(ref.name)) return true;
|
|
133
|
+
// Synthetic models produced by `enrichModelsFromSpec` (e.g. the
|
|
134
|
+
// inline-object item type for `ConnectApplication.redirect_uris`)
|
|
135
|
+
// are added to the models list passed into this generation pass —
|
|
136
|
+
// and hence into `shared.modelToService` — but won't yet exist on
|
|
137
|
+
// disk or in `apiSurface`. Accept them so their parent field
|
|
138
|
+
// survives field-projection.
|
|
139
|
+
if (shared.modelToService.has(ref.name)) return true;
|
|
133
140
|
const relPath = `src/${shared.resolveDir(shared.modelToService.get(ref.name))}/interfaces/${fileName(ref.name)}.interface.ts`;
|
|
134
141
|
return liveSurfaceHasManagedFile(relPath);
|
|
135
142
|
}
|
|
@@ -201,12 +208,14 @@ export function generateModels(models: Model[], ctx: EmitterContext, shared?: Sh
|
|
|
201
208
|
}
|
|
202
209
|
}
|
|
203
210
|
|
|
211
|
+
const discriminatedSkip = (ctx as { _discriminatedModelNames?: Set<string> })._discriminatedModelNames;
|
|
204
212
|
for (const originalModel of models) {
|
|
205
213
|
const model = projectedByName.get(originalModel.name) ?? originalModel;
|
|
206
214
|
if (!reachableModels.has(model.name)) continue;
|
|
207
215
|
if (interfaceEligibleModels && !interfaceEligibleModels.has(model.name)) continue;
|
|
208
216
|
if (isListMetadataModel(model)) continue;
|
|
209
217
|
if (isListWrapperModel(model)) continue;
|
|
218
|
+
if (discriminatedSkip?.has(model.name)) continue;
|
|
210
219
|
const service = modelToService.get(model.name);
|
|
211
220
|
const isOwnedModel = isNodeOwnedService(ctx, service);
|
|
212
221
|
if (!isOwnedModel && !modelHasNewFields(model, ctx) && !forceGenerate.has(model.name)) continue;
|
|
@@ -723,6 +732,7 @@ export function generateSerializers(
|
|
|
723
732
|
}
|
|
724
733
|
}
|
|
725
734
|
|
|
735
|
+
const discriminatedSerializerSkip = (ctx as { _discriminatedModelNames?: Set<string> })._discriminatedModelNames;
|
|
726
736
|
const eligibleModels: Model[] = [];
|
|
727
737
|
for (const originalModel of models) {
|
|
728
738
|
const model = projectedByName.get(originalModel.name) ?? originalModel;
|
|
@@ -730,6 +740,7 @@ export function generateSerializers(
|
|
|
730
740
|
if (serializerEligibleModels && !serializerEligibleModels.has(model.name)) continue;
|
|
731
741
|
if (isListMetadataModel(model)) continue;
|
|
732
742
|
if (isListWrapperModel(model)) continue;
|
|
743
|
+
if (discriminatedSerializerSkip?.has(model.name)) continue;
|
|
733
744
|
const service = modelToService.get(model.name);
|
|
734
745
|
const isOwnedModel = isNodeOwnedService(ctx, service);
|
|
735
746
|
if (!isOwnedModel && !modelHasNewFields(model, ctx) && !forceGenerateSerializer.has(model.name)) continue;
|
|
@@ -928,7 +939,10 @@ function buildGeneratedResourceModelUsage(
|
|
|
928
939
|
const mountGroups = groupByMount(ctx);
|
|
929
940
|
const services: Service[] =
|
|
930
941
|
mountGroups.size > 0
|
|
931
|
-
? [...mountGroups].map(([name, group]) => ({
|
|
942
|
+
? [...mountGroups].map(([name, group]) => ({
|
|
943
|
+
name,
|
|
944
|
+
operations: group.operations,
|
|
945
|
+
}))
|
|
932
946
|
: ctx.spec.services;
|
|
933
947
|
|
|
934
948
|
for (const service of services) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { EmitterContext, ResolvedOperation } from '@workos/oagen';
|
|
1
|
+
import type { EmitterContext, Model, ResolvedOperation } from '@workos/oagen';
|
|
2
|
+
import { enrichModelsFromSpec } from '../shared/model-utils.js';
|
|
2
3
|
|
|
3
4
|
type OperationOverride = {
|
|
4
5
|
methodName?: string;
|
|
@@ -42,17 +43,52 @@ function operationKey(resolved: ResolvedOperation): string {
|
|
|
42
43
|
return `${resolved.operation.httpMethod.toUpperCase()} ${resolved.operation.path}`;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Apply oneOf / allOf+oneOf enrichment (flattening variant fields onto the
|
|
48
|
+
* parent model, plus synthetic models/enums for inline shapes) so the rest
|
|
49
|
+
* of the Node emitter sees a richer `spec.models`.
|
|
50
|
+
*
|
|
51
|
+
* Without this, `ConnectApplication` (and any other `allOf [base, oneOf [...]]`
|
|
52
|
+
* schema whose first variant is itself wrapped in `allOf`) loses every
|
|
53
|
+
* non-M2M field — the IR parser's discriminator detection silently skips
|
|
54
|
+
* variants whose properties live behind another `allOf`. Mirrors what the
|
|
55
|
+
* Go / Kotlin / .NET emitters already do.
|
|
56
|
+
*
|
|
57
|
+
* Discriminated bases produced by `enrichModelsFromSpec` get their original
|
|
58
|
+
* fields restored — Node emits flat interfaces today, not TS sum types, so
|
|
59
|
+
* an empty base would otherwise drop the common fields.
|
|
60
|
+
*/
|
|
61
|
+
function enrichSpecModels(models: readonly Model[]): Model[] {
|
|
62
|
+
const enriched = enrichModelsFromSpec(models as Model[]);
|
|
63
|
+
const originalByName = new Map(models.map((m) => [m.name, m]));
|
|
64
|
+
return enriched.map((m) => {
|
|
65
|
+
if ((m as { discriminator?: unknown }).discriminator && m.fields.length === 0) {
|
|
66
|
+
const original = originalByName.get(m.name);
|
|
67
|
+
if (original && original.fields.length > 0) {
|
|
68
|
+
return { ...m, fields: original.fields };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return m;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
45
75
|
export function withNodeOperationOverrides(ctx: EmitterContext): EmitterContext {
|
|
46
76
|
const cached = contextCache.get(ctx);
|
|
47
77
|
if (cached) return cached;
|
|
48
78
|
|
|
79
|
+
const enrichedModels = enrichSpecModels(ctx.spec.models);
|
|
80
|
+
const specChanged =
|
|
81
|
+
enrichedModels.length !== ctx.spec.models.length || enrichedModels.some((m, i) => m !== ctx.spec.models[i]);
|
|
82
|
+
const enrichedSpec = specChanged ? { ...ctx.spec, models: enrichedModels } : ctx.spec;
|
|
83
|
+
|
|
49
84
|
const resolvedOperations = ctx.resolvedOperations;
|
|
50
85
|
if (!resolvedOperations?.length) {
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
const next = specChanged ? { ...ctx, spec: enrichedSpec } : ctx;
|
|
87
|
+
contextCache.set(ctx, next);
|
|
88
|
+
return next;
|
|
53
89
|
}
|
|
54
90
|
|
|
55
|
-
let
|
|
91
|
+
let opsChanged = false;
|
|
56
92
|
const nextResolved = resolvedOperations.map((resolved) => {
|
|
57
93
|
const override = OPERATION_OVERRIDES[operationKey(resolved)];
|
|
58
94
|
if (!override) return resolved;
|
|
@@ -63,7 +99,7 @@ export function withNodeOperationOverrides(ctx: EmitterContext): EmitterContext
|
|
|
63
99
|
return resolved;
|
|
64
100
|
}
|
|
65
101
|
|
|
66
|
-
|
|
102
|
+
opsChanged = true;
|
|
67
103
|
return {
|
|
68
104
|
...resolved,
|
|
69
105
|
methodName,
|
|
@@ -71,7 +107,14 @@ export function withNodeOperationOverrides(ctx: EmitterContext): EmitterContext
|
|
|
71
107
|
};
|
|
72
108
|
});
|
|
73
109
|
|
|
74
|
-
const next =
|
|
110
|
+
const next =
|
|
111
|
+
opsChanged || specChanged
|
|
112
|
+
? {
|
|
113
|
+
...ctx,
|
|
114
|
+
...(opsChanged ? { resolvedOperations: nextResolved } : {}),
|
|
115
|
+
...(specChanged ? { spec: enrichedSpec } : {}),
|
|
116
|
+
}
|
|
117
|
+
: ctx;
|
|
75
118
|
contextCache.set(ctx, next);
|
|
76
119
|
return next;
|
|
77
120
|
}
|
package/src/php/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 { initializeEnumDedup } from './naming.js';
|
|
21
|
+
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
21
22
|
|
|
22
23
|
/** Initialize enum deduplication from spec data. */
|
|
23
24
|
function ensureNamingInitialized(ctx: EmitterContext): void {
|
|
@@ -34,17 +35,39 @@ function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
|
34
35
|
return files;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Flatten oneOf / allOf+oneOf variant fields onto each base model and pull
|
|
40
|
+
* in synthetic models / enums for inline variant shapes. PHP emits flat
|
|
41
|
+
* classes (no sum types), so a discriminated base whose IR fields the
|
|
42
|
+
* parser stripped (post-allOf-aware detection) gets its original fields
|
|
43
|
+
* restored to avoid silently dropping variant data.
|
|
44
|
+
*/
|
|
45
|
+
function enrichModelsForPhp(models: Model[]): Model[] {
|
|
46
|
+
const enriched = enrichModelsFromSpec(models);
|
|
47
|
+
const originalByName = new Map(models.map((m) => [m.name, m]));
|
|
48
|
+
return enriched.map((m) => {
|
|
49
|
+
if ((m as { discriminator?: unknown }).discriminator && m.fields.length === 0) {
|
|
50
|
+
const original = originalByName.get(m.name);
|
|
51
|
+
if (original && original.fields.length > 0) {
|
|
52
|
+
return { ...m, fields: original.fields };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return m;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
export const phpEmitter: Emitter = {
|
|
38
60
|
language: 'php',
|
|
39
61
|
|
|
40
62
|
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
41
63
|
ensureNamingInitialized(ctx);
|
|
42
|
-
return ensureTrailingNewlines(generateModels(models, ctx));
|
|
64
|
+
return ensureTrailingNewlines(generateModels(enrichModelsForPhp(models), ctx));
|
|
43
65
|
},
|
|
44
66
|
|
|
45
67
|
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
46
68
|
ensureNamingInitialized(ctx);
|
|
47
|
-
|
|
69
|
+
const syntheticEnums = getSyntheticEnums();
|
|
70
|
+
return ensureTrailingNewlines(generateEnums([...enums, ...syntheticEnums], ctx));
|
|
48
71
|
},
|
|
49
72
|
|
|
50
73
|
generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
package/src/ruby/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { generateClient } from './client.js';
|
|
|
15
15
|
import { generateTests } from './tests.js';
|
|
16
16
|
import { buildOperationsMap } from './manifest.js';
|
|
17
17
|
import { generateRbiFiles } from './rbi.js';
|
|
18
|
+
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
18
19
|
|
|
19
20
|
/** Ensure every generated file's content ends with a trailing newline. */
|
|
20
21
|
function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
@@ -26,16 +27,40 @@ function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
|
26
27
|
return files;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Flatten oneOf / allOf+oneOf variant fields onto each base model and pick
|
|
32
|
+
* up the synthetic models / enums `enrichModelsFromSpec` produces for inline
|
|
33
|
+
* variant shapes. Ruby emits flat hash-backed models, not sum types, so a
|
|
34
|
+
* discriminated base whose IR fields were stripped (the new EventSchema-
|
|
35
|
+
* style behaviour after the parser learned to walk allOf-wrapped variants)
|
|
36
|
+
* has its original fields restored — otherwise `ConnectApplication`-style
|
|
37
|
+
* bases would silently lose every variant field they had previously.
|
|
38
|
+
*/
|
|
39
|
+
function enrichModelsForRuby(models: Model[]): Model[] {
|
|
40
|
+
const enriched = enrichModelsFromSpec(models);
|
|
41
|
+
const originalByName = new Map(models.map((m) => [m.name, m]));
|
|
42
|
+
return enriched.map((m) => {
|
|
43
|
+
if ((m as { discriminator?: unknown }).discriminator && m.fields.length === 0) {
|
|
44
|
+
const original = originalByName.get(m.name);
|
|
45
|
+
if (original && original.fields.length > 0) {
|
|
46
|
+
return { ...m, fields: original.fields };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return m;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
29
53
|
export const rubyEmitter: Emitter = {
|
|
30
54
|
language: 'ruby',
|
|
31
55
|
|
|
32
56
|
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
33
|
-
const modelFiles = generateModels(models, ctx);
|
|
57
|
+
const modelFiles = generateModels(enrichModelsForRuby(models), ctx);
|
|
34
58
|
return ensureTrailingNewlines(modelFiles);
|
|
35
59
|
},
|
|
36
60
|
|
|
37
61
|
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
38
|
-
|
|
62
|
+
const syntheticEnums = getSyntheticEnums();
|
|
63
|
+
return ensureTrailingNewlines(generateEnums([...enums, ...syntheticEnums], ctx));
|
|
39
64
|
},
|
|
40
65
|
|
|
41
66
|
generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
package/src/rust/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { generateClient } from './client.js';
|
|
|
16
16
|
import { generateTests } from './tests.js';
|
|
17
17
|
import { buildOperationsMap } from './manifest.js';
|
|
18
18
|
import { UnionRegistry } from './type-map.js';
|
|
19
|
+
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Shared per-emit registry that collects synthesised oneOf-style unions
|
|
@@ -34,16 +35,39 @@ function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
|
34
35
|
return files;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Flatten oneOf / allOf+oneOf variant fields onto each base model and pull
|
|
40
|
+
* in synthetic models / enums for inline variant shapes. Rust emits flat
|
|
41
|
+
* structs (a synthesised enum-union from `UnionRegistry` exists, but the
|
|
42
|
+
* field-on-base pattern is what matches `ConnectApplication` today). A
|
|
43
|
+
* discriminated base whose IR fields the parser stripped gets its original
|
|
44
|
+
* fields restored to avoid losing variant data.
|
|
45
|
+
*/
|
|
46
|
+
function enrichModelsForRust(models: Model[]): Model[] {
|
|
47
|
+
const enriched = enrichModelsFromSpec(models);
|
|
48
|
+
const originalByName = new Map(models.map((m) => [m.name, m]));
|
|
49
|
+
return enriched.map((m) => {
|
|
50
|
+
if ((m as { discriminator?: unknown }).discriminator && m.fields.length === 0) {
|
|
51
|
+
const original = originalByName.get(m.name);
|
|
52
|
+
if (original && original.fields.length > 0) {
|
|
53
|
+
return { ...m, fields: original.fields };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return m;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
37
60
|
export const rustEmitter: Emitter = {
|
|
38
61
|
language: 'rust',
|
|
39
62
|
|
|
40
63
|
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[] {
|
|
41
64
|
unionRegistry.reset();
|
|
42
|
-
return ensureTrailingNewlines(generateModels(models, ctx, unionRegistry));
|
|
65
|
+
return ensureTrailingNewlines(generateModels(enrichModelsForRust(models), ctx, unionRegistry));
|
|
43
66
|
},
|
|
44
67
|
|
|
45
68
|
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
46
|
-
|
|
69
|
+
const syntheticEnums = getSyntheticEnums();
|
|
70
|
+
return ensureTrailingNewlines(generateEnums([...enums, ...syntheticEnums], ctx));
|
|
47
71
|
},
|
|
48
72
|
|
|
49
73
|
generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Model, Field, TypeRef, Enum } from '@workos/oagen';
|
|
2
|
-
import { toSnakeCase } from '@workos/oagen';
|
|
2
|
+
import { toSnakeCase, toUpperSnakeCase } from '@workos/oagen';
|
|
3
3
|
import { readFileSync, existsSync } from 'node:fs';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
// @ts-ignore -- js-yaml has no type declarations in this project
|
|
@@ -83,7 +83,7 @@ function discoverSpecPath(): string | null {
|
|
|
83
83
|
let _rawSpecCache: Record<string, any> | null = null;
|
|
84
84
|
let _rawSpecLoaded = false;
|
|
85
85
|
|
|
86
|
-
function loadRawSpec(): Record<string, any> | null {
|
|
86
|
+
export function loadRawSpec(): Record<string, any> | null {
|
|
87
87
|
if (_rawSpecLoaded) return _rawSpecCache;
|
|
88
88
|
_rawSpecLoaded = true;
|
|
89
89
|
const specPath = discoverSpecPath();
|
|
@@ -114,7 +114,10 @@ function lookupRawSchema(name: string): Record<string, any> | null {
|
|
|
114
114
|
*/
|
|
115
115
|
interface SyntheticCollector {
|
|
116
116
|
models: Model[];
|
|
117
|
-
enums: Array<{
|
|
117
|
+
enums: Array<{
|
|
118
|
+
name: string;
|
|
119
|
+
values: Array<{ value: string; description?: string }>;
|
|
120
|
+
}>;
|
|
118
121
|
/** Track names already used to avoid duplicates. */
|
|
119
122
|
usedNames: Set<string>;
|
|
120
123
|
}
|
|
@@ -582,10 +585,17 @@ export function enrichModelsFromSpec(models: Model[]): Model[] {
|
|
|
582
585
|
return modified ? { ...model, fields: newFields } : model;
|
|
583
586
|
});
|
|
584
587
|
|
|
585
|
-
// Convert synthetic enum collector entries to proper Enum objects
|
|
588
|
+
// Convert synthetic enum collector entries to proper Enum objects. PHP's
|
|
589
|
+
// emitter (and others built on top of `EnumValue.name`) crash when this
|
|
590
|
+
// field is missing, so derive it from the value via the same upper-snake
|
|
591
|
+
// transform the parser uses for declared enums.
|
|
586
592
|
_lastSyntheticEnums = collector.enums.map((e) => ({
|
|
587
593
|
name: e.name,
|
|
588
|
-
values: e.values.map((v) => ({
|
|
594
|
+
values: e.values.map((v) => ({
|
|
595
|
+
name: toUpperSnakeCase(String(v.value)),
|
|
596
|
+
value: v.value,
|
|
597
|
+
description: v.description,
|
|
598
|
+
})),
|
|
589
599
|
})) as Enum[];
|
|
590
600
|
|
|
591
601
|
// Append synthetic models, skipping those whose snake_case name collides
|