@workos/oagen-emitters 0.18.3 → 0.19.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 +16 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-1ckLMpgo.mjs → plugin-BXDPA9pJ.mjs} +581 -172
- package/dist/plugin-BXDPA9pJ.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/docs/sdk-architecture/rust.md +2 -2
- package/package.json +5 -5
- package/src/dotnet/enums.ts +11 -5
- package/src/dotnet/fixtures.ts +5 -2
- package/src/dotnet/index.ts +2 -1
- package/src/dotnet/models.ts +41 -10
- package/src/dotnet/naming.ts +10 -0
- package/src/dotnet/resources.ts +3 -3
- package/src/dotnet/tests.ts +8 -4
- package/src/go/fixtures.ts +4 -2
- package/src/go/index.ts +4 -0
- package/src/go/models.ts +4 -2
- package/src/go/naming.ts +10 -0
- package/src/go/resources.ts +22 -9
- package/src/go/tests.ts +3 -3
- package/src/kotlin/enums.ts +21 -11
- package/src/kotlin/index.ts +2 -1
- package/src/kotlin/models.ts +24 -9
- package/src/kotlin/naming.ts +11 -0
- package/src/kotlin/resources.ts +2 -2
- package/src/kotlin/tests.ts +7 -3
- package/src/node/enums.ts +8 -5
- package/src/node/field-plan.ts +3 -3
- package/src/node/index.ts +2 -1
- package/src/node/models.ts +69 -22
- package/src/node/naming.ts +10 -0
- package/src/node/options.ts +45 -1
- package/src/node/resources.ts +67 -18
- package/src/node/tests.ts +302 -31
- package/src/php/enums.ts +18 -5
- package/src/php/index.ts +13 -4
- package/src/php/models.ts +22 -10
- package/src/php/naming.ts +10 -0
- package/src/php/resources.ts +6 -4
- package/src/php/tests.ts +17 -5
- package/src/python/enums.ts +39 -28
- package/src/python/fixtures.ts +4 -3
- package/src/python/index.ts +2 -1
- package/src/python/models.ts +39 -24
- package/src/python/naming.ts +10 -0
- package/src/python/resources.ts +3 -3
- package/src/python/tests.ts +14 -9
- package/src/ruby/enums.ts +28 -19
- package/src/ruby/index.ts +2 -1
- package/src/ruby/models.ts +33 -19
- package/src/ruby/naming.ts +10 -0
- package/src/ruby/rbi.ts +20 -7
- package/src/ruby/resources.ts +2 -2
- package/src/ruby/tests.ts +6 -3
- package/src/rust/enums.ts +9 -1
- package/src/rust/index.ts +2 -1
- package/src/rust/models.ts +100 -15
- package/src/rust/naming.ts +10 -0
- package/src/rust/resources.ts +14 -3
- package/src/rust/tests.ts +2 -2
- package/src/shared/file-header.ts +13 -0
- package/src/shared/resolved-ops.ts +47 -0
- package/test/rust/models.test.ts +49 -0
- package/test/shared/synthetic-enum-seed.test.ts +79 -0
- package/dist/plugin-1ckLMpgo.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-BXDPA9pJ.mjs";
|
|
2
2
|
export { workosEmittersPlugin };
|
|
@@ -316,8 +316,8 @@ surface that the `rust` extractor reads.
|
|
|
316
316
|
Every generated file begins with:
|
|
317
317
|
|
|
318
318
|
```rust
|
|
319
|
-
//
|
|
319
|
+
// This file is auto-generated by oagen. Do not edit.
|
|
320
320
|
```
|
|
321
321
|
|
|
322
|
-
`Cargo.toml` uses the TOML-style equivalent (`#
|
|
322
|
+
`Cargo.toml` uses the TOML-style equivalent (`# This file is auto-generated by oagen. Do not edit.`)
|
|
323
323
|
and JSON fixtures skip the header (`headerPlacement: 'skip'`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workos/oagen-emitters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "WorkOS' oagen emitters",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "WorkOS",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@commitlint/cli": "^21.0.2",
|
|
42
42
|
"@commitlint/config-conventional": "^21.0.2",
|
|
43
|
-
"@types/node": "^
|
|
43
|
+
"@types/node": "^26.0.0",
|
|
44
44
|
"husky": "^9.1.7",
|
|
45
|
-
"oxfmt": "^0.
|
|
46
|
-
"oxlint": "^1.
|
|
45
|
+
"oxfmt": "^0.56.0",
|
|
46
|
+
"oxlint": "^1.71.0",
|
|
47
47
|
"prettier": "^3.8.4",
|
|
48
48
|
"tsdown": "^0.22.3",
|
|
49
49
|
"tsx": "^4.22.4",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
"node": ">=24.10.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@workos/oagen": "^0.
|
|
57
|
+
"@workos/oagen": "^0.23.0"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/dotnet/enums.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { walkTypeRef } from '@workos/oagen';
|
|
|
3
3
|
import { className, deprecationMessage, escapeCsAttributeString, humanize } from './naming.js';
|
|
4
4
|
import { setEnumAliases, setSingleValueEnumNames } from './type-map.js';
|
|
5
5
|
import { enrichModelsFromSpec } from '../shared/model-utils.js';
|
|
6
|
+
import { isEnumInScope } from '../shared/resolved-ops.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Generate C# enum definitions from IR Enum definitions.
|
|
@@ -135,11 +136,16 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
135
136
|
lines.push(' }');
|
|
136
137
|
lines.push('}');
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
// FR-1.4: write the per-enum FILE only when in scope. .NET uses a flat
|
|
140
|
+
// Enums/ directory with C# namespaces (no barrel/index), so an
|
|
141
|
+
// out-of-scope enum left untouched on disk stays referenceable.
|
|
142
|
+
if (isEnumInScope(enumDef.name, ctx)) {
|
|
143
|
+
files.push({
|
|
144
|
+
path: `Enums/${typeName}.cs`,
|
|
145
|
+
content: lines.join('\n'),
|
|
146
|
+
overwriteExisting: true,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
143
149
|
}
|
|
144
150
|
|
|
145
151
|
return files;
|
package/src/dotnet/fixtures.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Model, TypeRef, Enum } from '@workos/oagen';
|
|
2
|
-
import { fixtureFileName,
|
|
2
|
+
import { fixtureFileName, domainFieldName } from './naming.js';
|
|
3
3
|
import { isListMetadataModel, isListWrapperModel } from './models.js';
|
|
4
4
|
import { collectNonPaginatedResponseModelNames } from '../shared/model-utils.js';
|
|
5
5
|
|
|
@@ -155,7 +155,10 @@ export function generateModelFixture(
|
|
|
155
155
|
|
|
156
156
|
const seenFieldNames = new Set<string>();
|
|
157
157
|
const deduplicatedFields = model.fields.filter((f) => {
|
|
158
|
-
|
|
158
|
+
// Dedup on the DOMAIN identifier (the C# property name, honoring a
|
|
159
|
+
// `domainName` override) to mirror the dedup in models.ts. The fixture
|
|
160
|
+
// payload below still keys on the wire name (`field.name`).
|
|
161
|
+
const csName = domainFieldName(f);
|
|
159
162
|
if (seenFieldNames.has(csName)) return false;
|
|
160
163
|
seenFieldNames.add(csName);
|
|
161
164
|
return true;
|
package/src/dotnet/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { generateTests } from './tests.js';
|
|
|
20
20
|
import { buildOperationsMap } from './manifest.js';
|
|
21
21
|
import { generateWrapperOptionsClasses } from './wrappers.js';
|
|
22
22
|
import { groupByMount } from '../shared/resolved-ops.js';
|
|
23
|
+
import { AUTOGEN_NOTICE } from '../shared/file-header.js';
|
|
23
24
|
import { discriminatedUnions, resolveModelName } from './type-map.js';
|
|
24
25
|
import { modelClassName } from './naming.js';
|
|
25
26
|
|
|
@@ -315,7 +316,7 @@ export const dotnetEmitter: Emitter = {
|
|
|
315
316
|
},
|
|
316
317
|
|
|
317
318
|
fileHeader(): string {
|
|
318
|
-
return
|
|
319
|
+
return `// ${AUTOGEN_NOTICE}`;
|
|
319
320
|
},
|
|
320
321
|
|
|
321
322
|
formatCommand(targetDir: string): FormatCommand | null {
|
package/src/dotnet/models.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
articleFor,
|
|
14
14
|
fieldName,
|
|
15
|
+
domainFieldName,
|
|
15
16
|
humanize,
|
|
16
17
|
emitXmlDoc,
|
|
17
18
|
deprecationMessage,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
isListMetadataModel,
|
|
26
27
|
collectNonPaginatedResponseModelNames,
|
|
27
28
|
} from '../shared/model-utils.js';
|
|
29
|
+
import { isModelInScope } from '../shared/resolved-ops.js';
|
|
28
30
|
export { isListWrapperModel, isListMetadataModel };
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -93,7 +95,10 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
93
95
|
const baseClassName = modelClassName(model.name);
|
|
94
96
|
const fieldMap = new Map<string, string>();
|
|
95
97
|
for (const field of model.fields) {
|
|
96
|
-
|
|
98
|
+
// DOMAIN identifier: the C# property name used for inheritance
|
|
99
|
+
// comparison (honors a `domainName` override). Must match the
|
|
100
|
+
// property name emitted below so variant fields dedup correctly.
|
|
101
|
+
let csName = domainFieldName(field);
|
|
97
102
|
if (csName === baseClassName) csName = `${csName}Value`;
|
|
98
103
|
fieldMap.set(csName, mapTypeRef(field.type));
|
|
99
104
|
}
|
|
@@ -119,8 +124,16 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
119
124
|
// Required enums need JsonProperty / STJS; a field whose PascalCase name
|
|
120
125
|
// collides with the enclosing class needs the same imports for the wire-
|
|
121
126
|
// name override emitted below.
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
// DOMAIN identifier: the emitted C# property name (honors `domainName`)
|
|
128
|
+
// is what can collide with the enclosing class name.
|
|
129
|
+
const hasClassNameCollision = model.fields.some((f) => domainFieldName(f) === csClassName);
|
|
130
|
+
// A `domainName` override renames the C# property away from the wire key
|
|
131
|
+
// (e.g. wire `connection_type` surfaced as domain `Type`). The
|
|
132
|
+
// SnakeCaseLower naming policy would otherwise serialize the domain name,
|
|
133
|
+
// so these fields need an explicit pinned wire name (and thus the imports).
|
|
134
|
+
const hasDomainRename = model.fields.some((f) => domainFieldName(f) !== fieldName(f.name));
|
|
135
|
+
const needsJsonAttrs =
|
|
136
|
+
hasClassNameCollision || hasDomainRename || model.fields.some((f) => f.required && isEnumRef(f.type));
|
|
124
137
|
|
|
125
138
|
lines.push(`namespace ${ctx.namespacePascal}`);
|
|
126
139
|
lines.push('{');
|
|
@@ -175,9 +188,16 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
175
188
|
// when that happens. Track the rename so we emit an explicit
|
|
176
189
|
// `[JsonProperty]` attribute below — the SnakeCaseLower naming policy
|
|
177
190
|
// would otherwise serialize `ErrorValue` as `error_value`, not `error`.
|
|
178
|
-
|
|
191
|
+
// DOMAIN identifier: the C# property name, honoring a `domainName`
|
|
192
|
+
// override (e.g. wire `connection_type` → domain `Type`). The wire key
|
|
193
|
+
// passed to `emitJsonPropertyAttributes` below still derives from
|
|
194
|
+
// `field.name`.
|
|
195
|
+
let csFieldName = domainFieldName(field);
|
|
179
196
|
const collidesWithClassName = csFieldName === csClassName;
|
|
180
197
|
if (collidesWithClassName) csFieldName = `${csFieldName}Value`;
|
|
198
|
+
// When the domain rename diverges from the wire key, the SnakeCaseLower
|
|
199
|
+
// naming policy can't recover the wire name from the property — pin it.
|
|
200
|
+
const hasDomainOverride = domainFieldName(field) !== fieldName(field.name);
|
|
181
201
|
if (seenFieldNames.has(csFieldName)) continue;
|
|
182
202
|
seenFieldNames.add(csFieldName);
|
|
183
203
|
|
|
@@ -257,8 +277,14 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
257
277
|
}
|
|
258
278
|
|
|
259
279
|
const isRequiredEnum = field.required && isEnumRef(field.type) && constInit === null;
|
|
280
|
+
// WIRE key: always derives from `field.name`. Pin it explicitly when the
|
|
281
|
+
// C# property name (collision suffix or `domainName` override) no longer
|
|
282
|
+
// round-trips to the wire name via the SnakeCaseLower naming policy.
|
|
260
283
|
lines.push(
|
|
261
|
-
...emitJsonPropertyAttributes(field.name, {
|
|
284
|
+
...emitJsonPropertyAttributes(field.name, {
|
|
285
|
+
isRequiredEnum,
|
|
286
|
+
explicitWireName: collidesWithClassName || hasDomainOverride,
|
|
287
|
+
}),
|
|
262
288
|
);
|
|
263
289
|
// Discriminated-union-typed field: attach the variant-dispatching converter
|
|
264
290
|
// so Newtonsoft picks the right subtype on deserialization. The converter
|
|
@@ -330,11 +356,16 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
330
356
|
lines.push(' }');
|
|
331
357
|
lines.push('}');
|
|
332
358
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
359
|
+
// FR-1.4: write the per-model FILE only when in scope. .NET uses a flat
|
|
360
|
+
// Entities/ directory with C# namespaces (no barrel/index), so an
|
|
361
|
+
// out-of-scope model left untouched on disk stays referenceable.
|
|
362
|
+
if (isModelInScope(model.name, ctx)) {
|
|
363
|
+
files.push({
|
|
364
|
+
path: `Entities/${csClassName}.cs`,
|
|
365
|
+
content: lines.join('\n'),
|
|
366
|
+
overwriteExisting: true,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
338
369
|
}
|
|
339
370
|
|
|
340
371
|
return files;
|
package/src/dotnet/naming.ts
CHANGED
|
@@ -39,6 +39,16 @@ export function fieldName(name: string): string {
|
|
|
39
39
|
return toPascalCase(name);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* PascalCase domain property name for a model field, honoring a `domainName`
|
|
44
|
+
* override (set via the `fieldHints` config) so a wire field can surface under
|
|
45
|
+
* a friendlier C# property name. The wire/serialization key (the
|
|
46
|
+
* `[JsonPropertyName("...")]` value) still derives from `field.name`.
|
|
47
|
+
*/
|
|
48
|
+
export function domainFieldName(field: { name: string; domainName?: string }): string {
|
|
49
|
+
return toPascalCase(field.domainName ?? field.name);
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
/** PascalCase directory name for service modules. */
|
|
43
53
|
export function moduleName(name: string): string {
|
|
44
54
|
return toPascalCase(name);
|
package/src/dotnet/resources.ts
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
import {
|
|
36
36
|
buildResolvedLookup,
|
|
37
37
|
lookupResolved,
|
|
38
|
-
|
|
38
|
+
scopedMountGroups,
|
|
39
39
|
getOpDefaults,
|
|
40
40
|
getOpInferFromClient,
|
|
41
41
|
buildHiddenParams,
|
|
@@ -71,10 +71,10 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
71
71
|
if (services.length === 0) return [];
|
|
72
72
|
|
|
73
73
|
const files: GeneratedFile[] = [];
|
|
74
|
-
const mountGroups =
|
|
74
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
75
75
|
|
|
76
76
|
const entries: Array<{ name: string; operations: Operation[] }> =
|
|
77
|
-
mountGroups.size > 0
|
|
77
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
78
78
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
79
79
|
: services.map((s) => ({ name: resolveResourceClassName(s, ctx), operations: s.operations }));
|
|
80
80
|
|
package/src/dotnet/tests.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { planOperation } from '@workos/oagen';
|
|
|
3
3
|
import {
|
|
4
4
|
fixtureFileName,
|
|
5
5
|
fieldName as csFieldName,
|
|
6
|
+
domainFieldName as csDomainFieldName,
|
|
6
7
|
methodName as csMethodName,
|
|
7
8
|
appendAsyncSuffix,
|
|
8
9
|
modelClassName,
|
|
@@ -15,7 +16,7 @@ import { resolveResourceClassName, sortPathParamsByTemplateOrder, optionsClassNa
|
|
|
15
16
|
import { generateFixtures, generateModelFixture } from './fixtures.js';
|
|
16
17
|
import { isListWrapperModel } from './models.js';
|
|
17
18
|
import {
|
|
18
|
-
|
|
19
|
+
scopedMountGroups,
|
|
19
20
|
buildResolvedLookup,
|
|
20
21
|
lookupResolved,
|
|
21
22
|
buildHiddenParams,
|
|
@@ -39,9 +40,9 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// Generate per-mount-target test files
|
|
42
|
-
const mountGroups =
|
|
43
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
43
44
|
const testEntries: Array<{ name: string; operations: Operation[] }> =
|
|
44
|
-
mountGroups.size > 0
|
|
45
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
45
46
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
46
47
|
: spec.services.map((s) => ({
|
|
47
48
|
name: resolveResourceClassName(s, ctx),
|
|
@@ -694,7 +695,10 @@ function buildFixtureAssertions(model: import('@workos/oagen').Model, spec: ApiS
|
|
|
694
695
|
if (field.type.kind !== 'primitive' || field.type.type !== 'string') continue;
|
|
695
696
|
if (field.type.format === 'date-time' || field.type.format === 'date') continue;
|
|
696
697
|
if (field.type.format === 'binary') continue;
|
|
697
|
-
|
|
698
|
+
// DOMAIN identifier: the C# property accessed on the deserialized model
|
|
699
|
+
// (honors a `domainName` override). The fixture lookup below uses the wire
|
|
700
|
+
// key (`field.name`).
|
|
701
|
+
const csField = csDomainFieldName(field);
|
|
698
702
|
const val = fixture[field.name];
|
|
699
703
|
if (typeof val === 'string' && val.length > 0) {
|
|
700
704
|
assertions.push(`Assert.Equal(${csStringLiteral(val)}, result.${csField});`);
|
package/src/go/fixtures.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Model, TypeRef, Enum } from '@workos/oagen';
|
|
2
|
-
import { fileName,
|
|
2
|
+
import { fileName, domainFieldName } from './naming.js';
|
|
3
3
|
import { isListMetadataModel, isListWrapperModel } from './models.js';
|
|
4
4
|
import { collectNonPaginatedResponseModelNames, collectReferencedListMetadataModels } from '../shared/model-utils.js';
|
|
5
5
|
|
|
@@ -131,7 +131,9 @@ export function generateModelFixture(
|
|
|
131
131
|
|
|
132
132
|
const seenFieldNames = new Set<string>();
|
|
133
133
|
const deduplicatedFields = model.fields.filter((f) => {
|
|
134
|
-
|
|
134
|
+
// Dedup by the domain Go field name to mirror the struct in models.ts; the
|
|
135
|
+
// fixture key itself (wireName below) still derives from field.name.
|
|
136
|
+
const goName = domainFieldName(f);
|
|
135
137
|
if (seenFieldNames.has(goName)) return false;
|
|
136
138
|
seenFieldNames.add(goName);
|
|
137
139
|
return true;
|
package/src/go/index.ts
CHANGED
|
@@ -88,6 +88,10 @@ export const goEmitter: Emitter = {
|
|
|
88
88
|
},
|
|
89
89
|
|
|
90
90
|
fileHeader(): string {
|
|
91
|
+
// Go-specific: this exact form matches the standard generated-file regex
|
|
92
|
+
// (`^// Code generated .* DO NOT EDIT\.$`) that gofmt, gopls, golangci-lint,
|
|
93
|
+
// and other Go tooling use to classify a file as generated. It intentionally
|
|
94
|
+
// does NOT use the shared AUTOGEN_NOTICE, which would break that detection.
|
|
91
95
|
return '// Code generated by oagen. DO NOT EDIT.';
|
|
92
96
|
},
|
|
93
97
|
|
package/src/go/models.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Model, EmitterContext, GeneratedFile, TypeRef, Service } from '@workos/oagen';
|
|
2
2
|
import { walkTypeRef } from '@workos/oagen';
|
|
3
3
|
import { mapTypeRef } from './type-map.js';
|
|
4
|
-
import { className,
|
|
4
|
+
import { className, domainFieldName } from './naming.js';
|
|
5
5
|
import { lowerFirstForDoc, fieldDocComment, articleFor } from '../shared/naming-utils.js';
|
|
6
6
|
|
|
7
7
|
// Import and re-export shared model detection utilities
|
|
@@ -185,7 +185,9 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
185
185
|
// Deduplicate fields by Go field name
|
|
186
186
|
const seenFieldNames = new Set<string>();
|
|
187
187
|
for (const field of model.fields) {
|
|
188
|
-
|
|
188
|
+
// Domain identifier honors a `fieldHints` override (e.g. connection_type
|
|
189
|
+
// → type); the json struct tag below still derives from `field.name`.
|
|
190
|
+
const goFieldName = domainFieldName(field);
|
|
189
191
|
if (seenFieldNames.has(goFieldName)) continue;
|
|
190
192
|
seenFieldNames.add(goFieldName);
|
|
191
193
|
|
package/src/go/naming.ts
CHANGED
|
@@ -61,6 +61,16 @@ export function fieldName(name: string): string {
|
|
|
61
61
|
return applyAcronyms(toPascalCase(name));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* PascalCase domain field name for a model field, honoring a `domainName`
|
|
66
|
+
* override (set via the `fieldHints` config) so a wire field can surface under
|
|
67
|
+
* a friendlier name. The wire name (the `json:"..."` struct tag) still derives
|
|
68
|
+
* from `field.name`.
|
|
69
|
+
*/
|
|
70
|
+
export function domainFieldName(field: { name: string; domainName?: string }): string {
|
|
71
|
+
return applyAcronyms(toPascalCase(field.domainName ?? field.name));
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
/** snake_case module/directory name. */
|
|
65
75
|
export function moduleName(name: string): string {
|
|
66
76
|
return toSnakeCase(name);
|
package/src/go/resources.ts
CHANGED
|
@@ -9,11 +9,19 @@ import type {
|
|
|
9
9
|
import { planOperation, toSnakeCase } from '@workos/oagen';
|
|
10
10
|
import { isListWrapperModel } from './models.js';
|
|
11
11
|
import { mapTypeRef, mapTypeRefValue } from './type-map.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
className,
|
|
14
|
+
domainFieldName,
|
|
15
|
+
fieldName,
|
|
16
|
+
methodName,
|
|
17
|
+
resolveClassName,
|
|
18
|
+
resolveMethodName,
|
|
19
|
+
unexportedName,
|
|
20
|
+
} from './naming.js';
|
|
13
21
|
import {
|
|
14
22
|
buildResolvedLookup,
|
|
15
23
|
lookupResolved,
|
|
16
|
-
|
|
24
|
+
scopedMountGroups,
|
|
17
25
|
getOpDefaults,
|
|
18
26
|
getOpInferFromClient,
|
|
19
27
|
buildHiddenParams,
|
|
@@ -52,11 +60,11 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
52
60
|
if (services.length === 0) return [];
|
|
53
61
|
|
|
54
62
|
const files: GeneratedFile[] = [];
|
|
55
|
-
const mountGroups =
|
|
63
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
56
64
|
|
|
57
65
|
// If no resolved operations, fall back to raw services
|
|
58
66
|
const entries: Array<{ name: string; operations: Operation[] }> =
|
|
59
|
-
mountGroups.size > 0
|
|
67
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
60
68
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
61
69
|
: services.map((s) => ({ name: resolveResourceClassName(s, ctx), operations: s.operations }));
|
|
62
70
|
|
|
@@ -404,7 +412,8 @@ function generateParamsStruct(
|
|
|
404
412
|
for (const field of bodyModel.fields) {
|
|
405
413
|
if (hidden.has(field.name)) continue;
|
|
406
414
|
if (groupedParams.has(field.name)) continue;
|
|
407
|
-
|
|
415
|
+
// Domain struct field; the json tag below keeps deriving from field.name.
|
|
416
|
+
const goField = domainFieldName(field);
|
|
408
417
|
if (emittedFields.has(goField)) continue;
|
|
409
418
|
emittedFields.add(goField);
|
|
410
419
|
const isOptional = !field.required;
|
|
@@ -942,7 +951,8 @@ function emitHiddenParamsBodyStruct(
|
|
|
942
951
|
if (hidden.has(field.name)) continue;
|
|
943
952
|
if (groupedParamNames.has(field.name)) continue;
|
|
944
953
|
if (!field.required) continue;
|
|
945
|
-
|
|
954
|
+
// Domain struct field; the json tag below keeps deriving from field.name.
|
|
955
|
+
const goField = domainFieldName(field);
|
|
946
956
|
const goType = mapTypeRef(field.type);
|
|
947
957
|
lines.push(`\t${goField} ${goType} \`json:"${field.name}"\``);
|
|
948
958
|
}
|
|
@@ -960,7 +970,8 @@ function emitHiddenParamsBodyStruct(
|
|
|
960
970
|
if (hidden.has(field.name)) continue;
|
|
961
971
|
if (groupedParamNames.has(field.name)) continue;
|
|
962
972
|
if (field.required) continue;
|
|
963
|
-
|
|
973
|
+
// Domain struct field; the json tag below keeps deriving from field.name.
|
|
974
|
+
const goField = domainFieldName(field);
|
|
964
975
|
const goType = makeOptional(mapTypeRef(field.type));
|
|
965
976
|
lines.push(`\t${goField} ${goType} \`json:"${field.name},omitempty"\``);
|
|
966
977
|
}
|
|
@@ -1007,7 +1018,8 @@ function emitBodyWithHiddenParams(
|
|
|
1007
1018
|
for (const field of bodyModel.fields) {
|
|
1008
1019
|
if (hidden.has(field.name)) continue;
|
|
1009
1020
|
if (!field.required) continue;
|
|
1010
|
-
|
|
1021
|
+
// Domain struct field on both the body literal and the params struct.
|
|
1022
|
+
const goField = domainFieldName(field);
|
|
1011
1023
|
lines.push(`\t\t${goField}: params.${goField},`);
|
|
1012
1024
|
}
|
|
1013
1025
|
}
|
|
@@ -1025,7 +1037,8 @@ function emitBodyWithHiddenParams(
|
|
|
1025
1037
|
for (const field of bodyModel.fields) {
|
|
1026
1038
|
if (hidden.has(field.name)) continue;
|
|
1027
1039
|
if (field.required) continue;
|
|
1028
|
-
|
|
1040
|
+
// Domain struct field on both the body struct and the params struct.
|
|
1041
|
+
const goField = domainFieldName(field);
|
|
1029
1042
|
lines.push(`\tbody.${goField} = params.${goField}`);
|
|
1030
1043
|
}
|
|
1031
1044
|
}
|
package/src/go/tests.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveResourceClassName, paramsStructName, sortPathParamsByTemplateOrd
|
|
|
5
5
|
import { buildServiceAccessPaths } from './client.js';
|
|
6
6
|
import { generateFixtures } from './fixtures.js';
|
|
7
7
|
import { isListWrapperModel } from './models.js';
|
|
8
|
-
import {
|
|
8
|
+
import { scopedMountGroups, buildResolvedLookup, lookupResolved, buildHiddenParams } from '../shared/resolved-ops.js';
|
|
9
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
10
10
|
import { resolve } from 'node:path';
|
|
11
11
|
|
|
@@ -85,9 +85,9 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
|
|
|
85
85
|
const accessPaths = buildServiceAccessPaths(spec.services, ctx);
|
|
86
86
|
|
|
87
87
|
// Generate per-mount-target test files
|
|
88
|
-
const mountGroups =
|
|
88
|
+
const mountGroups = scopedMountGroups(ctx);
|
|
89
89
|
const testEntries: Array<{ name: string; operations: Operation[] }> =
|
|
90
|
-
mountGroups.size > 0
|
|
90
|
+
mountGroups.size > 0 || ctx.scopedServices?.size
|
|
91
91
|
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
92
92
|
: spec.services.map((s) => ({
|
|
93
93
|
name: resolveResourceClassName(s, ctx),
|
package/src/kotlin/enums.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Enum, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
2
|
import { className, ktStringLiteral } from './naming.js';
|
|
3
|
+
import { isEnumInScope } from '../shared/resolved-ops.js';
|
|
3
4
|
|
|
4
5
|
const KOTLIN_SRC_PREFIX = 'src/main/kotlin/';
|
|
5
6
|
const ENUMS_PACKAGE = 'com.workos.types';
|
|
@@ -24,7 +25,7 @@ export const enumCanonicalMap = new Map<string, string>();
|
|
|
24
25
|
* shortest PascalCase name becomes canonical and the rest emit `typealias`
|
|
25
26
|
* files pointing at the canonical class.
|
|
26
27
|
*/
|
|
27
|
-
export function generateEnums(enums: Enum[],
|
|
28
|
+
export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
28
29
|
if (enums.length === 0) return [];
|
|
29
30
|
|
|
30
31
|
// Reset the canonical map on every generation run (guards against re-entry).
|
|
@@ -74,6 +75,11 @@ export function generateEnums(enums: Enum[], _ctx: EmitterContext): GeneratedFil
|
|
|
74
75
|
|
|
75
76
|
const typeName = canonicalEnumTypeName(enumDef);
|
|
76
77
|
|
|
78
|
+
// FR-1.4: write per-enum FILES only when in scope. Each enum is its own
|
|
79
|
+
// `.kt` file (no barrel), so an out-of-scope enum left untouched on disk
|
|
80
|
+
// stays importable.
|
|
81
|
+
const enumInScope = isEnumInScope(enumDef.name, ctx);
|
|
82
|
+
|
|
77
83
|
// Non-canonical enum: emit a typealias instead of a full enum class.
|
|
78
84
|
const sharedSortEmitter = sharedSortEmitters.has(enumDef.name);
|
|
79
85
|
const canonicalName = sharedSortEmitter
|
|
@@ -94,11 +100,13 @@ export function generateEnums(enums: Enum[], _ctx: EmitterContext): GeneratedFil
|
|
|
94
100
|
aliasLine,
|
|
95
101
|
'',
|
|
96
102
|
].join('\n');
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
if (enumInScope) {
|
|
104
|
+
files.push({
|
|
105
|
+
path: `${KOTLIN_SRC_PREFIX}${ENUMS_DIR}/${typeName}.kt`,
|
|
106
|
+
content: aliasContent,
|
|
107
|
+
overwriteExisting: true,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
102
110
|
continue;
|
|
103
111
|
}
|
|
104
112
|
|
|
@@ -175,11 +183,13 @@ export function generateEnums(enums: Enum[], _ctx: EmitterContext): GeneratedFil
|
|
|
175
183
|
lines.push('}');
|
|
176
184
|
lines.push('');
|
|
177
185
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
if (enumInScope) {
|
|
187
|
+
files.push({
|
|
188
|
+
path: `${KOTLIN_SRC_PREFIX}${ENUMS_DIR}/${typeName}.kt`,
|
|
189
|
+
content: lines.join('\n'),
|
|
190
|
+
overwriteExisting: true,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
183
193
|
}
|
|
184
194
|
|
|
185
195
|
return files;
|
package/src/kotlin/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { generateTests } from './tests.js';
|
|
|
19
19
|
import { buildOperationsMap } from './manifest.js';
|
|
20
20
|
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
21
21
|
import { flattenDiscriminatedUnionFields } from '../shared/union-flatten.js';
|
|
22
|
+
import { AUTOGEN_NOTICE } from '../shared/file-header.js';
|
|
22
23
|
|
|
23
24
|
/** Ensure every generated file ends with a trailing newline. */
|
|
24
25
|
function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
@@ -95,7 +96,7 @@ export const kotlinEmitter: Emitter = {
|
|
|
95
96
|
},
|
|
96
97
|
|
|
97
98
|
fileHeader(): string {
|
|
98
|
-
return
|
|
99
|
+
return `// ${AUTOGEN_NOTICE}`;
|
|
99
100
|
},
|
|
100
101
|
|
|
101
102
|
formatCommand(targetDir: string): FormatCommand | null {
|
package/src/kotlin/models.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Model, EmitterContext, GeneratedFile, TypeRef, Field } from '@workos/oagen';
|
|
2
2
|
import { mapTypeRef, discriminatedUnions } from './type-map.js';
|
|
3
|
-
import { className,
|
|
3
|
+
import { className, domainPropertyName, ktStringLiteral, humanize } from './naming.js';
|
|
4
4
|
import { enumCanonicalMap } from './enums.js';
|
|
5
5
|
import {
|
|
6
6
|
isListWrapperModel,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
collectNonPaginatedResponseModelNames,
|
|
9
9
|
collectReferencedListMetadataModels,
|
|
10
10
|
} from '../shared/model-utils.js';
|
|
11
|
+
import { isModelInScope } from '../shared/resolved-ops.js';
|
|
11
12
|
|
|
12
13
|
const KOTLIN_SRC_PREFIX = 'src/main/kotlin/';
|
|
13
14
|
const MODELS_PACKAGE = 'com.workos.models';
|
|
@@ -123,10 +124,17 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
123
124
|
for (const model of models) {
|
|
124
125
|
if (skipAsListWrapper(model) || skipAsListMetadata(model)) continue;
|
|
125
126
|
const typeName = className(model.name);
|
|
127
|
+
// FR-1.4: write per-model FILES only when in scope. Each model is its own
|
|
128
|
+
// `.kt` file (no barrel), so an out-of-scope model left untouched on disk
|
|
129
|
+
// stays importable. The WorkOSEvent sealed interface below is an aggregate
|
|
130
|
+
// built from many event models, so it is NOT gated.
|
|
131
|
+
const modelInScope = isModelInScope(model.name, ctx);
|
|
126
132
|
|
|
127
133
|
// Parent of a discriminated union: emit a sealed class.
|
|
128
134
|
if (model.fields.length === 0 && discriminatedUnions.has(typeName)) {
|
|
129
|
-
|
|
135
|
+
if (modelInScope) {
|
|
136
|
+
files.push(emitSealedUnion(typeName, discriminatedUnions.get(typeName)!));
|
|
137
|
+
}
|
|
130
138
|
continue;
|
|
131
139
|
}
|
|
132
140
|
|
|
@@ -142,15 +150,19 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
142
150
|
`typealias ${typeName} = ${canonicalType}`,
|
|
143
151
|
'',
|
|
144
152
|
].join('\n');
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
if (modelInScope) {
|
|
154
|
+
files.push({
|
|
155
|
+
path: `${KOTLIN_SRC_PREFIX}${MODELS_DIR}/${typeName}.kt`,
|
|
156
|
+
content: aliasContent,
|
|
157
|
+
overwriteExisting: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
150
160
|
continue;
|
|
151
161
|
}
|
|
152
162
|
|
|
153
|
-
|
|
163
|
+
if (modelInScope) {
|
|
164
|
+
files.push(emitDataClass(model));
|
|
165
|
+
}
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
// Generate the sealed WorkOSEvent interface. Collect all event envelope
|
|
@@ -374,7 +386,10 @@ function renderFields(fields: Field[], overrideFields: Set<string> = new Set()):
|
|
|
374
386
|
|
|
375
387
|
for (const rawField of fields) {
|
|
376
388
|
const field = promoteFieldType(rawField);
|
|
377
|
-
|
|
389
|
+
// DOMAIN identifier: the data class property name. Honors a `domainName`
|
|
390
|
+
// override (e.g. connection_type -> type); the `@JsonProperty(...)` wire
|
|
391
|
+
// key below still derives from `field.name`.
|
|
392
|
+
const kotlinName = domainPropertyName(field);
|
|
378
393
|
if (seen.has(kotlinName)) continue;
|
|
379
394
|
seen.add(kotlinName);
|
|
380
395
|
|
package/src/kotlin/naming.ts
CHANGED
|
@@ -57,6 +57,17 @@ export function propertyName(name: string): string {
|
|
|
57
57
|
return escapeReserved(camel);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* camelCase domain property name for a MODEL field, honoring a `domainName`
|
|
62
|
+
* override (set via the `fieldHints` config) so a wire field can surface under
|
|
63
|
+
* a friendlier name. The wire key (the `@JsonProperty("...")` argument) still
|
|
64
|
+
* derives from `field.name`. No-op when `domainName` is unset, so it is also
|
|
65
|
+
* safe on params. Only apply to model fields.
|
|
66
|
+
*/
|
|
67
|
+
export function domainPropertyName(field: { name: string; domainName?: string }): string {
|
|
68
|
+
return propertyName(field.domainName ?? field.name);
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
/** camelCase alias (kept for parity with other emitters). */
|
|
61
72
|
export const fieldName = propertyName;
|
|
62
73
|
export const localName = propertyName;
|