@workos/oagen-emitters 0.18.2 → 0.18.4
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 +17 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-bqfwowQ3.mjs → plugin-Cciic50q.mjs} +457 -101
- package/dist/plugin-Cciic50q.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/docs/sdk-architecture/rust.md +2 -2
- package/package.json +3 -3
- package/src/dotnet/fixtures.ts +17 -3
- package/src/dotnet/index.ts +2 -1
- package/src/dotnet/models.ts +30 -5
- package/src/dotnet/naming.ts +10 -0
- package/src/dotnet/tests.ts +5 -1
- 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 +19 -6
- package/src/kotlin/index.ts +2 -1
- package/src/kotlin/models.ts +5 -2
- package/src/kotlin/naming.ts +11 -0
- package/src/kotlin/tests.ts +5 -1
- package/src/node/field-plan.ts +3 -3
- package/src/node/index.ts +2 -1
- package/src/node/models.ts +40 -1
- package/src/node/naming.ts +10 -0
- package/src/node/options.ts +45 -1
- package/src/node/resources.ts +76 -19
- package/src/node/tests.ts +296 -30
- package/src/php/index.ts +2 -1
- package/src/php/models.ts +11 -5
- package/src/php/naming.ts +10 -0
- package/src/php/tests.ts +11 -2
- package/src/python/fixtures.ts +4 -3
- package/src/python/index.ts +2 -1
- package/src/python/models.ts +12 -6
- package/src/python/naming.ts +10 -0
- package/src/python/tests.ts +11 -6
- package/src/ruby/index.ts +2 -1
- package/src/ruby/models.ts +10 -7
- package/src/ruby/naming.ts +10 -0
- package/src/ruby/rbi.ts +3 -1
- package/src/ruby/tests.ts +4 -1
- package/src/rust/index.ts +2 -1
- package/src/rust/models.ts +87 -15
- package/src/rust/naming.ts +10 -0
- package/src/rust/resources.ts +6 -2
- package/src/shared/file-header.ts +13 -0
- package/test/node/resources.test.ts +31 -2
- package/test/rust/models.test.ts +49 -0
- package/dist/plugin-bqfwowQ3.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-Cciic50q.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.18.
|
|
3
|
+
"version": "0.18.4",
|
|
4
4
|
"description": "WorkOS' oagen emitters",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "WorkOS",
|
|
@@ -40,7 +40,7 @@
|
|
|
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
45
|
"oxfmt": "^0.55.0",
|
|
46
46
|
"oxlint": "^1.70.0",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
"node": ">=24.10.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@workos/oagen": "^0.22.
|
|
57
|
+
"@workos/oagen": "^0.22.7"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/dotnet/fixtures.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
|
+
import { collectNonPaginatedResponseModelNames } from '../shared/model-utils.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Prefix mapping for generating realistic ID fixture values.
|
|
@@ -35,9 +36,19 @@ export function generateFixtures(spec: {
|
|
|
35
36
|
const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
|
|
36
37
|
const files: { path: string; content: string }[] = [];
|
|
37
38
|
|
|
39
|
+
// List-wrappers are normally represented only by the per-operation
|
|
40
|
+
// `list_<item>.json` fixtures generated from paginated operations below. But
|
|
41
|
+
// a wrapper returned by a NON-paginated operation (e.g.
|
|
42
|
+
// `PUT /authorization/groups/{id}/role_assignments` -> GroupRoleAssignmentList)
|
|
43
|
+
// is emitted as a real model (see models.ts) and its generated test references
|
|
44
|
+
// `testdata/<type>.json` (tests.ts). Emit that envelope fixture too, mirroring
|
|
45
|
+
// the non-wrapper `VersionListResponse` precedent — otherwise the test loads a
|
|
46
|
+
// file that was never written.
|
|
47
|
+
const nonPaginatedWrapperRefs = collectNonPaginatedResponseModelNames(spec.services);
|
|
48
|
+
|
|
38
49
|
for (const model of spec.models) {
|
|
39
50
|
if (isListMetadataModel(model)) continue;
|
|
40
|
-
if (isListWrapperModel(model)) continue;
|
|
51
|
+
if (isListWrapperModel(model) && !nonPaginatedWrapperRefs.has(model.name)) continue;
|
|
41
52
|
|
|
42
53
|
const fixture = model.fields.length === 0 ? {} : generateModelFixture(model, modelMap, enumMap);
|
|
43
54
|
|
|
@@ -144,7 +155,10 @@ export function generateModelFixture(
|
|
|
144
155
|
|
|
145
156
|
const seenFieldNames = new Set<string>();
|
|
146
157
|
const deduplicatedFields = model.fields.filter((f) => {
|
|
147
|
-
|
|
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);
|
|
148
162
|
if (seenFieldNames.has(csName)) return false;
|
|
149
163
|
seenFieldNames.add(csName);
|
|
150
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,
|
|
@@ -93,7 +94,10 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
93
94
|
const baseClassName = modelClassName(model.name);
|
|
94
95
|
const fieldMap = new Map<string, string>();
|
|
95
96
|
for (const field of model.fields) {
|
|
96
|
-
|
|
97
|
+
// DOMAIN identifier: the C# property name used for inheritance
|
|
98
|
+
// comparison (honors a `domainName` override). Must match the
|
|
99
|
+
// property name emitted below so variant fields dedup correctly.
|
|
100
|
+
let csName = domainFieldName(field);
|
|
97
101
|
if (csName === baseClassName) csName = `${csName}Value`;
|
|
98
102
|
fieldMap.set(csName, mapTypeRef(field.type));
|
|
99
103
|
}
|
|
@@ -119,8 +123,16 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
119
123
|
// Required enums need JsonProperty / STJS; a field whose PascalCase name
|
|
120
124
|
// collides with the enclosing class needs the same imports for the wire-
|
|
121
125
|
// name override emitted below.
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
// DOMAIN identifier: the emitted C# property name (honors `domainName`)
|
|
127
|
+
// is what can collide with the enclosing class name.
|
|
128
|
+
const hasClassNameCollision = model.fields.some((f) => domainFieldName(f) === csClassName);
|
|
129
|
+
// A `domainName` override renames the C# property away from the wire key
|
|
130
|
+
// (e.g. wire `connection_type` surfaced as domain `Type`). The
|
|
131
|
+
// SnakeCaseLower naming policy would otherwise serialize the domain name,
|
|
132
|
+
// so these fields need an explicit pinned wire name (and thus the imports).
|
|
133
|
+
const hasDomainRename = model.fields.some((f) => domainFieldName(f) !== fieldName(f.name));
|
|
134
|
+
const needsJsonAttrs =
|
|
135
|
+
hasClassNameCollision || hasDomainRename || model.fields.some((f) => f.required && isEnumRef(f.type));
|
|
124
136
|
|
|
125
137
|
lines.push(`namespace ${ctx.namespacePascal}`);
|
|
126
138
|
lines.push('{');
|
|
@@ -175,9 +187,16 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
175
187
|
// when that happens. Track the rename so we emit an explicit
|
|
176
188
|
// `[JsonProperty]` attribute below — the SnakeCaseLower naming policy
|
|
177
189
|
// would otherwise serialize `ErrorValue` as `error_value`, not `error`.
|
|
178
|
-
|
|
190
|
+
// DOMAIN identifier: the C# property name, honoring a `domainName`
|
|
191
|
+
// override (e.g. wire `connection_type` → domain `Type`). The wire key
|
|
192
|
+
// passed to `emitJsonPropertyAttributes` below still derives from
|
|
193
|
+
// `field.name`.
|
|
194
|
+
let csFieldName = domainFieldName(field);
|
|
179
195
|
const collidesWithClassName = csFieldName === csClassName;
|
|
180
196
|
if (collidesWithClassName) csFieldName = `${csFieldName}Value`;
|
|
197
|
+
// When the domain rename diverges from the wire key, the SnakeCaseLower
|
|
198
|
+
// naming policy can't recover the wire name from the property — pin it.
|
|
199
|
+
const hasDomainOverride = domainFieldName(field) !== fieldName(field.name);
|
|
181
200
|
if (seenFieldNames.has(csFieldName)) continue;
|
|
182
201
|
seenFieldNames.add(csFieldName);
|
|
183
202
|
|
|
@@ -257,8 +276,14 @@ export function generateModels(models: Model[], ctx: EmitterContext, discCtx?: D
|
|
|
257
276
|
}
|
|
258
277
|
|
|
259
278
|
const isRequiredEnum = field.required && isEnumRef(field.type) && constInit === null;
|
|
279
|
+
// WIRE key: always derives from `field.name`. Pin it explicitly when the
|
|
280
|
+
// C# property name (collision suffix or `domainName` override) no longer
|
|
281
|
+
// round-trips to the wire name via the SnakeCaseLower naming policy.
|
|
260
282
|
lines.push(
|
|
261
|
-
...emitJsonPropertyAttributes(field.name, {
|
|
283
|
+
...emitJsonPropertyAttributes(field.name, {
|
|
284
|
+
isRequiredEnum,
|
|
285
|
+
explicitWireName: collidesWithClassName || hasDomainOverride,
|
|
286
|
+
}),
|
|
262
287
|
);
|
|
263
288
|
// Discriminated-union-typed field: attach the variant-dispatching converter
|
|
264
289
|
// so Newtonsoft picks the right subtype on deserialization. The converter
|
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/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,
|
|
@@ -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,7 +9,15 @@ 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,
|
|
@@ -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/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,
|
|
@@ -374,7 +374,10 @@ function renderFields(fields: Field[], overrideFields: Set<string> = new Set()):
|
|
|
374
374
|
|
|
375
375
|
for (const rawField of fields) {
|
|
376
376
|
const field = promoteFieldType(rawField);
|
|
377
|
-
|
|
377
|
+
// DOMAIN identifier: the data class property name. Honors a `domainName`
|
|
378
|
+
// override (e.g. connection_type -> type); the `@JsonProperty(...)` wire
|
|
379
|
+
// key below still derives from `field.name`.
|
|
380
|
+
const kotlinName = domainPropertyName(field);
|
|
378
381
|
if (seen.has(kotlinName)) continue;
|
|
379
382
|
seen.add(kotlinName);
|
|
380
383
|
|
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;
|
package/src/kotlin/tests.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
ktStringLiteral,
|
|
18
18
|
className,
|
|
19
19
|
propertyName,
|
|
20
|
+
domainPropertyName,
|
|
20
21
|
buildExportedClassNameSet,
|
|
21
22
|
} from './naming.js';
|
|
22
23
|
import { mapTypeRef } from './type-map.js';
|
|
@@ -713,7 +714,10 @@ function buildResponseAssertions(
|
|
|
713
714
|
for (const field of model.fields) {
|
|
714
715
|
if (!field.required) continue;
|
|
715
716
|
if (assertions.length >= MAX_RESPONSE_ASSERTIONS) break;
|
|
716
|
-
|
|
717
|
+
// DOMAIN identifier: the property accessor on the deserialized model.
|
|
718
|
+
// Honors a `domainName` override; the synthesized JSON above keys off
|
|
719
|
+
// `field.name` (the wire key).
|
|
720
|
+
const ktProp = domainPropertyName(field);
|
|
717
721
|
const type = field.type;
|
|
718
722
|
if (type.kind === 'primitive') {
|
|
719
723
|
if (type.format === 'date-time') continue;
|
package/src/node/field-plan.ts
CHANGED
|
@@ -388,7 +388,7 @@ export function serializerHasBaselineIncompatibility(
|
|
|
388
388
|
const irDomainFields = new Set<string>();
|
|
389
389
|
for (const field of model.fields) {
|
|
390
390
|
irWireFields.add(wireFieldName(field.name));
|
|
391
|
-
irDomainFields.add(fieldName(field.name));
|
|
391
|
+
irDomainFields.add(fieldName(field.domainName ?? field.name));
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
for (const [wireField2, fieldDef] of Object.entries(baselineResponse.fields)) {
|
|
@@ -463,7 +463,7 @@ export function planDeserializeField(
|
|
|
463
463
|
skipFormatFields: Set<string>,
|
|
464
464
|
ctx: EmitterContext,
|
|
465
465
|
): { line: string; skip: boolean } {
|
|
466
|
-
const domain = fieldName(field.name);
|
|
466
|
+
const domain = fieldName(field.domainName ?? field.name);
|
|
467
467
|
const wire = wireFieldName(field.name);
|
|
468
468
|
const wireAccess = `response.${wire}`;
|
|
469
469
|
const skip = skipFormatFields.has(field.name);
|
|
@@ -543,7 +543,7 @@ export function planSerializeField(
|
|
|
543
543
|
ctx: EmitterContext,
|
|
544
544
|
): { line: string; skip: boolean } {
|
|
545
545
|
const wire = wireFieldName(field.name);
|
|
546
|
-
const domain = fieldName(field.name);
|
|
546
|
+
const domain = fieldName(field.domainName ?? field.name);
|
|
547
547
|
const domainAccess = `model.${domain}`;
|
|
548
548
|
const skip = skipFormatFields.has(field.name);
|
|
549
549
|
|
package/src/node/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { withNodeOperationOverrides } from './node-overrides.js';
|
|
|
40
40
|
import { isNodeOwnedService, nodeOptions } from './options.js';
|
|
41
41
|
import { setInlineEnumUnions, setDomainNameResolver } from './type-map.js';
|
|
42
42
|
import { groupByMount } from '../shared/resolved-ops.js';
|
|
43
|
+
import { AUTOGEN_NOTICE } from '../shared/file-header.js';
|
|
43
44
|
import { assignModelsToServices, createServiceDirResolver, relativeImport } from './utils.js';
|
|
44
45
|
import { fileName } from './naming.js';
|
|
45
46
|
|
|
@@ -869,7 +870,7 @@ export const nodeEmitter: Emitter = {
|
|
|
869
870
|
},
|
|
870
871
|
|
|
871
872
|
fileHeader(): string {
|
|
872
|
-
return
|
|
873
|
+
return `// ${AUTOGEN_NOTICE}`;
|
|
873
874
|
},
|
|
874
875
|
|
|
875
876
|
formatCommand(targetDir: string): FormatCommand | null {
|
package/src/node/models.ts
CHANGED
|
@@ -527,7 +527,9 @@ export function generateModels(models: Model[], ctx: EmitterContext, shared?: Sh
|
|
|
527
527
|
} else {
|
|
528
528
|
lines.push(`export interface ${domainName}${typeParams} {`);
|
|
529
529
|
for (const field of model.fields) {
|
|
530
|
-
|
|
530
|
+
// Domain identifier honors a `fieldHints` override (e.g. connection_type
|
|
531
|
+
// → type); the wire name below still derives from `field.name`.
|
|
532
|
+
const domainFieldName = fieldName(field.domainName ?? field.name);
|
|
531
533
|
if (seenDomainFields.has(domainFieldName)) continue;
|
|
532
534
|
seenDomainFields.add(domainFieldName);
|
|
533
535
|
if (field.description || field.deprecated || field.readOnly || field.writeOnly || field.default !== undefined) {
|
|
@@ -546,6 +548,19 @@ export function generateModels(models: Model[], ctx: EmitterContext, shared?: Sh
|
|
|
546
548
|
baselineField && !baselineField.optional && responseBaselineField && responseBaselineField.optional;
|
|
547
549
|
const readonlyPrefix = field.readOnly ? 'readonly ' : '';
|
|
548
550
|
if (
|
|
551
|
+
genericDefaults.has(model.name) &&
|
|
552
|
+
baselineField &&
|
|
553
|
+
typeReferencesUnresolvable(baselineField.type, unresolvableNames)
|
|
554
|
+
) {
|
|
555
|
+
// Baseline typed this field with the model's generic param (e.g.
|
|
556
|
+
// `customAttributes?: CustomAttributesType`). The emitted interface
|
|
557
|
+
// renames the param to `GenericType`; remap the field so the param is
|
|
558
|
+
// actually used — otherwise it trips TS6133 (declared, never read).
|
|
559
|
+
const opt = baselineField.optional ? '?' : '';
|
|
560
|
+
lines.push(
|
|
561
|
+
` ${readonlyPrefix}${domainFieldName}${opt}: ${substituteGenericParam(baselineField.type, unresolvableNames)};`,
|
|
562
|
+
);
|
|
563
|
+
} else if (
|
|
549
564
|
baselineField &&
|
|
550
565
|
!domainResponseOptionalMismatch &&
|
|
551
566
|
!hasDateTimeConversion(field.type) &&
|
|
@@ -597,6 +612,15 @@ export function generateModels(models: Model[], ctx: EmitterContext, shared?: Sh
|
|
|
597
612
|
seenWireFields.add(wireField);
|
|
598
613
|
const baselineField = baselineResponse?.fields?.[wireField];
|
|
599
614
|
if (
|
|
615
|
+
genericDefaults.has(model.name) &&
|
|
616
|
+
baselineField &&
|
|
617
|
+
typeReferencesUnresolvable(baselineField.type, unresolvableNames)
|
|
618
|
+
) {
|
|
619
|
+
// Mirror the domain side: keep the generic param wired on the
|
|
620
|
+
// response interface so `GenericType` is used, not orphaned.
|
|
621
|
+
const opt = baselineField.optional ? '?' : '';
|
|
622
|
+
lines.push(` ${wireField}${opt}: ${substituteGenericParam(baselineField.type, unresolvableNames)};`);
|
|
623
|
+
} else if (
|
|
600
624
|
baselineField &&
|
|
601
625
|
baselineTypeResolvable(baselineField.type, importableNames) &&
|
|
602
626
|
baselineFieldCompatible(baselineField, field)
|
|
@@ -1301,6 +1325,21 @@ function hasSpecificIRType(ref: TypeRef): boolean {
|
|
|
1301
1325
|
}
|
|
1302
1326
|
}
|
|
1303
1327
|
|
|
1328
|
+
/** True when a baseline field type references one of the model's generic
|
|
1329
|
+
* param identifiers (collected as `unresolvableNames` — bare type names that
|
|
1330
|
+
* resolve to nothing importable, which for a generic model are its params). */
|
|
1331
|
+
function typeReferencesUnresolvable(type: string, unresolvable: Set<string>): boolean {
|
|
1332
|
+
if (unresolvable.size === 0) return false;
|
|
1333
|
+
const names = type.match(/\b[A-Z][a-zA-Z0-9]*\b/g);
|
|
1334
|
+
return !!names && names.some((n) => unresolvable.has(n));
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/** Rewrite a baseline field type so references to the model's (renamed)
|
|
1338
|
+
* generic param resolve to the emitted `GenericType` param. */
|
|
1339
|
+
function substituteGenericParam(type: string, unresolvable: Set<string>): string {
|
|
1340
|
+
return type.replace(/\b[A-Z][a-zA-Z0-9]*\b/g, (name) => (unresolvable.has(name) ? 'GenericType' : name));
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1304
1343
|
function renderTypeParams(model: Model, genericDefaults?: Map<string, string>): string {
|
|
1305
1344
|
if (!model.typeParams?.length) {
|
|
1306
1345
|
if (genericDefaults?.has(model.name)) {
|
package/src/node/naming.ts
CHANGED
|
@@ -32,6 +32,16 @@ export function fieldName(name: string): string {
|
|
|
32
32
|
return toCamelCase(name);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* camelCase domain field name for a model field, honoring a `domainName`
|
|
37
|
+
* override (set via the `fieldHints` config) so a wire field can surface under
|
|
38
|
+
* a friendlier name. The wire name (see {@link wireFieldName}) still derives
|
|
39
|
+
* from `field.name`.
|
|
40
|
+
*/
|
|
41
|
+
export function domainFieldName(field: { name: string; domainName?: string }): string {
|
|
42
|
+
return toCamelCase(field.domainName ?? field.name);
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
/** snake_case field name for wire/response interfaces. */
|
|
36
46
|
export function wireFieldName(name: string): string {
|
|
37
47
|
return toSnakeCase(name);
|
package/src/node/options.ts
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
|
-
import type { EmitterContext } from '@workos/oagen';
|
|
1
|
+
import type { EmitterContext, Operation, OperationPlan } from '@workos/oagen';
|
|
2
|
+
import { planOperation } from '@workos/oagen';
|
|
2
3
|
|
|
3
4
|
export interface OperationOverride {
|
|
4
5
|
methodName?: string;
|
|
5
6
|
mountOn?: string;
|
|
6
7
|
optionsType?: string;
|
|
7
8
|
bodyFieldMap?: Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Rename spec path parameters to the SDK options-object field they should be
|
|
11
|
+
* exposed as. Keys are the camelCase identifier the param resolves to via
|
|
12
|
+
* `fieldName(param.name)` — NOT the raw (possibly snake_case) spec key — since
|
|
13
|
+
* the lookup is keyed on that camelCase form (e.g. `{ resourceId: 'targetId' }`,
|
|
14
|
+
* even for a `resource_id` spec param). Applied to the destructure, the URL
|
|
15
|
+
* template binding, and generated tests so a published SDK field name can
|
|
16
|
+
* diverge from the spec path-param name without a global spec rewrite (which
|
|
17
|
+
* would ripple across every language).
|
|
18
|
+
*/
|
|
19
|
+
pathFieldMap?: Record<string, string>;
|
|
8
20
|
returnType?: string;
|
|
9
21
|
returnDataProperty?: string;
|
|
10
22
|
returnTypeImports?: string[];
|
|
11
23
|
returnExpression?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Override the response model the operation deserializes, by model name.
|
|
26
|
+
* Replaces the spec-derived response model so the resource (and its test)
|
|
27
|
+
* reference a different wire type / deserializer — e.g. mapping
|
|
28
|
+
* Authorization's role responses to the full `OrganizationRole` instead of
|
|
29
|
+
* the slim `Role`/`RoleResponse` shape shared with SSO and UserManagement.
|
|
30
|
+
* Node-only; never affects the global spec or other SDKs.
|
|
31
|
+
*/
|
|
32
|
+
responseModel?: string;
|
|
12
33
|
}
|
|
13
34
|
|
|
14
35
|
export interface NodeEmitterOptions {
|
|
@@ -90,3 +111,26 @@ export function isHandOwnedType(ctx: EmitterContext, name: string | undefined):
|
|
|
90
111
|
if (!configured || configured.length === 0) return false;
|
|
91
112
|
return configured.includes(name);
|
|
92
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the Node operation override for an operation, keyed by "METHOD /path".
|
|
117
|
+
*/
|
|
118
|
+
export function operationOverrideFor(ctx: EmitterContext, op: Operation): OperationOverride | undefined {
|
|
119
|
+
return nodeOptions(ctx).operationOverrides?.[`${op.httpMethod.toUpperCase()} ${op.path}`];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `planOperation` plus the Node `responseModel` override. When an operation
|
|
124
|
+
* override supplies `responseModel`, the resolved response model name is
|
|
125
|
+
* replaced so the resource and its generated test reference the desired wire
|
|
126
|
+
* type and deserializer. Use this everywhere the Node emitter would otherwise
|
|
127
|
+
* call `planOperation(op)` directly so resource and test stay in lockstep.
|
|
128
|
+
*/
|
|
129
|
+
export function planOperationFor(op: Operation, ctx: EmitterContext): OperationPlan {
|
|
130
|
+
const plan = planOperation(op);
|
|
131
|
+
const responseModel = operationOverrideFor(ctx, op)?.responseModel;
|
|
132
|
+
if (responseModel && responseModel !== plan.responseModelName) {
|
|
133
|
+
return { ...plan, responseModelName: responseModel, isModelResponse: true };
|
|
134
|
+
}
|
|
135
|
+
return plan;
|
|
136
|
+
}
|