@workos/oagen-emitters 0.18.3 → 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 +9 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-1ckLMpgo.mjs → plugin-Cciic50q.mjs} +443 -99
- 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 +5 -2
- 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 +55 -17
- 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/rust/models.test.ts +49 -0
- package/dist/plugin-1ckLMpgo.mjs.map +0 -1
package/src/php/models.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Model, TypeRef, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
2
|
import { mapTypeRef, mapTypeRefForPHPDoc } from './type-map.js';
|
|
3
|
-
import { className, enumClassName,
|
|
3
|
+
import { className, enumClassName, domainFieldName } from './naming.js';
|
|
4
4
|
import { phpDocComment } from './utils.js';
|
|
5
5
|
|
|
6
6
|
// Import and re-export shared model detection utilities
|
|
@@ -67,7 +67,8 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
67
67
|
// Deduplicate fields that map to the same PHP name
|
|
68
68
|
const seenNames = new Set<string>();
|
|
69
69
|
const allFields = [...requiredFields, ...optionalFields].filter((f) => {
|
|
70
|
-
|
|
70
|
+
// DOMAIN identifier: the PHP property name (honors a `domainName` override).
|
|
71
|
+
const phpName = domainFieldName(f);
|
|
71
72
|
if (seenNames.has(phpName)) return false;
|
|
72
73
|
seenNames.add(phpName);
|
|
73
74
|
return true;
|
|
@@ -75,7 +76,8 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
75
76
|
|
|
76
77
|
for (let i = 0; i < allFields.length; i++) {
|
|
77
78
|
const field = allFields[i];
|
|
78
|
-
|
|
79
|
+
// DOMAIN identifier: the promoted constructor property name.
|
|
80
|
+
const phpName = domainFieldName(field);
|
|
79
81
|
const phpType = mapTypeRef(field.type);
|
|
80
82
|
const isOptional = !field.required;
|
|
81
83
|
const comma = i < allFields.length - 1 ? ',' : ',';
|
|
@@ -108,7 +110,9 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
108
110
|
lines.push(` return new self(`);
|
|
109
111
|
for (let i = 0; i < allFields.length; i++) {
|
|
110
112
|
const field = allFields[i];
|
|
111
|
-
|
|
113
|
+
// DOMAIN identifier: the named constructor argument (PHP property).
|
|
114
|
+
const phpName = domainFieldName(field);
|
|
115
|
+
// WIRE key: the JSON key read from `$data[...]` (stays `field.name`).
|
|
112
116
|
const wireName = field.name;
|
|
113
117
|
const comma = i < allFields.length - 1 ? ',' : ',';
|
|
114
118
|
const accessor = generateFromArrayAccessor(field.type, wireName, field.required);
|
|
@@ -124,7 +128,9 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
124
128
|
lines.push(' {');
|
|
125
129
|
lines.push(' return [');
|
|
126
130
|
for (const field of allFields) {
|
|
127
|
-
|
|
131
|
+
// DOMAIN identifier: the `$this->...` property being serialized.
|
|
132
|
+
const phpName = domainFieldName(field);
|
|
133
|
+
// WIRE key: the JSON key emitted into the array (stays `field.name`).
|
|
128
134
|
const wireName = field.name;
|
|
129
135
|
const serialized = generateToArrayValue(field.type, `$this->${phpName}`, !field.required);
|
|
130
136
|
lines.push(` '${wireName}' => ${serialized},`);
|
package/src/php/naming.ts
CHANGED
|
@@ -138,6 +138,16 @@ export function fieldName(name: string): string {
|
|
|
138
138
|
return toCamelCase(name);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* camelCase DOMAIN property name for a model field, honoring a `domainName`
|
|
143
|
+
* override (set via the `fieldHints` config) so a wire field can surface under
|
|
144
|
+
* a friendlier PHP property name. The wire key (see {@link wireName}) still
|
|
145
|
+
* derives from `field.name`. No-op when `domainName` is unset.
|
|
146
|
+
*/
|
|
147
|
+
export function domainFieldName(field: { name: string; domainName?: string }): string {
|
|
148
|
+
return fieldName(field.domainName ?? field.name);
|
|
149
|
+
}
|
|
150
|
+
|
|
141
151
|
/** snake_case name for fixtures and other snake_case contexts. */
|
|
142
152
|
export function snakeName(name: string): string {
|
|
143
153
|
return toSnakeCase(stripUrnPrefix(name));
|
package/src/php/tests.ts
CHANGED
|
@@ -8,7 +8,14 @@ import type {
|
|
|
8
8
|
ResolvedOperation,
|
|
9
9
|
} from '@workos/oagen';
|
|
10
10
|
import { planOperation, toCamelCase, toPascalCase } from '@workos/oagen';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
className,
|
|
13
|
+
enumClassName,
|
|
14
|
+
resolveMethodName,
|
|
15
|
+
snakeName,
|
|
16
|
+
servicePropertyName,
|
|
17
|
+
domainFieldName,
|
|
18
|
+
} from './naming.js';
|
|
12
19
|
import { isListWrapperModel } from './models.js';
|
|
13
20
|
import { generateFixtures } from './fixtures.js';
|
|
14
21
|
import {
|
|
@@ -503,7 +510,9 @@ function emitFieldHydrationAssertions(
|
|
|
503
510
|
|
|
504
511
|
for (const f of assertFields) {
|
|
505
512
|
if (!f) continue;
|
|
506
|
-
|
|
513
|
+
// DOMAIN identifier: the deserialized model's PHP property (honors `domainName`).
|
|
514
|
+
// The fixture key `f.name` is the WIRE key and stays unchanged.
|
|
515
|
+
const phpProp = domainFieldName(f);
|
|
507
516
|
lines.push(` $this->assertSame(${fixtureVar}['${f.name}'], ${resultVar}->${phpProp});`);
|
|
508
517
|
}
|
|
509
518
|
}
|
package/src/python/fixtures.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Model, TypeRef, Enum } from '@workos/oagen';
|
|
2
2
|
|
|
3
|
-
import { fileName,
|
|
3
|
+
import { fileName, domainFieldName } from './naming.js';
|
|
4
4
|
import { isListMetadataModel, isListWrapperModel } from './models.js';
|
|
5
5
|
import { collectNonPaginatedResponseModelNames, collectReferencedListMetadataModels } from '../shared/model-utils.js';
|
|
6
6
|
|
|
@@ -104,10 +104,11 @@ export function generateModelFixture(
|
|
|
104
104
|
): Record<string, any> {
|
|
105
105
|
const fixture: Record<string, any> = {};
|
|
106
106
|
|
|
107
|
-
// Deduplicate fields by
|
|
107
|
+
// Deduplicate fields by DOMAIN identifier (matching model generation in
|
|
108
|
+
// models.ts, which honors `domainName`); the wire key below stays `field.name`.
|
|
108
109
|
const seenFieldNames = new Set<string>();
|
|
109
110
|
const deduplicatedFields = model.fields.filter((f) => {
|
|
110
|
-
const pyName =
|
|
111
|
+
const pyName = domainFieldName(f);
|
|
111
112
|
if (seenFieldNames.has(pyName)) return false;
|
|
112
113
|
seenFieldNames.add(pyName);
|
|
113
114
|
return true;
|
package/src/python/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
|
|
12
12
|
import { generateModels } from './models.js';
|
|
13
13
|
import { detectDiscriminators } from '../shared/model-utils.js';
|
|
14
|
+
import { AUTOGEN_NOTICE } from '../shared/file-header.js';
|
|
14
15
|
import { generateEnums } from './enums.js';
|
|
15
16
|
import { generateResources } from './resources.js';
|
|
16
17
|
import { generateClient } from './client.js';
|
|
@@ -86,7 +87,7 @@ export const pythonEmitter: Emitter = {
|
|
|
86
87
|
},
|
|
87
88
|
|
|
88
89
|
fileHeader(): string {
|
|
89
|
-
return
|
|
90
|
+
return `# ${AUTOGEN_NOTICE}`;
|
|
90
91
|
},
|
|
91
92
|
|
|
92
93
|
formatCommand(_targetDir: string): FormatCommand | null {
|
package/src/python/models.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Model, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
2
|
import { collectFieldDependencies, walkTypeRef } from '@workos/oagen';
|
|
3
3
|
import { mapTypeRef } from './type-map.js';
|
|
4
|
-
import { className,
|
|
4
|
+
import { className, domainFieldName, fileName, buildMountDirMap, dirToModule } from './naming.js';
|
|
5
5
|
import { collectGeneratedEnumSymbolsByDir } from './enums.js';
|
|
6
6
|
import { computeSchemaPlacement } from './shared-schemas.js';
|
|
7
7
|
|
|
@@ -232,7 +232,9 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
232
232
|
// Deduplicate fields that map to the same snake_case name
|
|
233
233
|
const seenFieldNames = new Set<string>();
|
|
234
234
|
const deduplicatedFields = model.fields.filter((f) => {
|
|
235
|
-
|
|
235
|
+
// Dedup on the DOMAIN identifier (the dataclass attribute name), which
|
|
236
|
+
// honors a `domainName` override; the wire key stays `field.name`.
|
|
237
|
+
const pyName = domainFieldName(f);
|
|
236
238
|
if (seenFieldNames.has(pyName)) return false;
|
|
237
239
|
seenFieldNames.add(pyName);
|
|
238
240
|
return true;
|
|
@@ -343,7 +345,8 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
343
345
|
};
|
|
344
346
|
|
|
345
347
|
for (const field of requiredFields) {
|
|
346
|
-
|
|
348
|
+
// DOMAIN identifier: the dataclass attribute name (honors `domainName`).
|
|
349
|
+
const pyFieldName = domainFieldName(field);
|
|
347
350
|
const pyType = rewriteDiscriminatorType(resolveModelFieldType(field.type));
|
|
348
351
|
if (field.description || field.deprecated) {
|
|
349
352
|
const parts: string[] = [];
|
|
@@ -357,7 +360,8 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
357
360
|
}
|
|
358
361
|
|
|
359
362
|
for (const field of optionalFields) {
|
|
360
|
-
|
|
363
|
+
// DOMAIN identifier: the dataclass attribute name (honors `domainName`).
|
|
364
|
+
const pyFieldName = domainFieldName(field);
|
|
361
365
|
const innerType =
|
|
362
366
|
field.type.kind === 'nullable' ? resolveModelFieldType(field.type.inner) : resolveModelFieldType(field.type);
|
|
363
367
|
const pyType = `Optional[${rewriteDiscriminatorType(innerType)}]`;
|
|
@@ -383,7 +387,8 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
383
387
|
const fieldAssignmentLines: string[] = [];
|
|
384
388
|
|
|
385
389
|
for (const field of [...requiredFields, ...optionalFields]) {
|
|
386
|
-
|
|
390
|
+
// DOMAIN identifier (LHS of `cls(...)`); the wire key below stays `field.name`.
|
|
391
|
+
const pyFieldName = domainFieldName(field);
|
|
387
392
|
const wireKey = field.name; // Wire keys are snake_case from the spec
|
|
388
393
|
const isRequired = !isOptionalField(model.name, field, ctx);
|
|
389
394
|
|
|
@@ -424,7 +429,8 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
424
429
|
lines.push(' result: Dict[str, Any] = {}');
|
|
425
430
|
|
|
426
431
|
for (const field of [...requiredFields, ...optionalFields]) {
|
|
427
|
-
|
|
432
|
+
// DOMAIN identifier (`self.<attr>`); the wire key below stays `field.name`.
|
|
433
|
+
const pyFieldName = domainFieldName(field);
|
|
428
434
|
const wireKey = field.name;
|
|
429
435
|
const isRequired = !isOptionalField(model.name, field, ctx);
|
|
430
436
|
|
package/src/python/naming.ts
CHANGED
|
@@ -50,6 +50,16 @@ export function fieldName(name: string): string {
|
|
|
50
50
|
return toSnakeCase(name);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* snake_case domain field name for a model field, honoring a `domainName`
|
|
55
|
+
* override (set via the `fieldHints` config) so a wire field can surface under
|
|
56
|
+
* a friendlier name. The wire name (still derived from `field.name`) is
|
|
57
|
+
* unaffected, so the API contract is preserved.
|
|
58
|
+
*/
|
|
59
|
+
export function domainFieldName(field: { name: string; domainName?: string }): string {
|
|
60
|
+
return toSnakeCase(field.domainName ?? field.name);
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
/**
|
|
54
64
|
* Python builtins that should not be shadowed by parameter names.
|
|
55
65
|
* When a path/query param name collides, suffix with underscore.
|
package/src/python/tests.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
className,
|
|
14
14
|
fileName,
|
|
15
15
|
fieldName,
|
|
16
|
+
domainFieldName,
|
|
16
17
|
moduleName,
|
|
17
18
|
resolveMethodName,
|
|
18
19
|
buildMountDirMap,
|
|
@@ -1026,12 +1027,13 @@ function pickAssertableFields(
|
|
|
1026
1027
|
// Skip strings containing characters that are hard to represent as Python literals
|
|
1027
1028
|
if (val.includes('"') || val.includes("'") || val.includes('{') || val.includes('\\') || val.includes('\n'))
|
|
1028
1029
|
continue;
|
|
1029
|
-
|
|
1030
|
+
// DOMAIN identifier: asserted as `result.<attr>` (honors `domainName`).
|
|
1031
|
+
results.push({ field: domainFieldName(f), value: `"${val}"` });
|
|
1030
1032
|
} else if (typeof val === 'boolean') {
|
|
1031
1033
|
// Use "is True/False" to satisfy ruff E712
|
|
1032
|
-
results.push({ field:
|
|
1034
|
+
results.push({ field: domainFieldName(f), value: val ? 'True' : 'False', isBool: true });
|
|
1033
1035
|
} else if (typeof val === 'number') {
|
|
1034
|
-
results.push({ field:
|
|
1036
|
+
results.push({ field: domainFieldName(f), value: String(val) });
|
|
1035
1037
|
}
|
|
1036
1038
|
}
|
|
1037
1039
|
return results;
|
|
@@ -1114,7 +1116,9 @@ function buildTestArgs(op: Operation, spec: ApiSpec, hiddenParams?: Set<string>)
|
|
|
1114
1116
|
if (plan.hasBody && op.requestBody?.kind === 'model') {
|
|
1115
1117
|
const rbName = op.requestBody.name;
|
|
1116
1118
|
const bodyModel = spec.models.find((m) => m.name === rbName);
|
|
1117
|
-
|
|
1119
|
+
// Compare the body field's DOMAIN identifier (honors `domainName`)
|
|
1120
|
+
// against the param kwarg name; the param has no domainName override.
|
|
1121
|
+
if (bodyModel?.fields.some((f) => domainFieldName(f) === fieldName(param.name))) continue;
|
|
1118
1122
|
}
|
|
1119
1123
|
if (param.required && !pathParamNames.has(fieldName(param.name))) {
|
|
1120
1124
|
args.push(`${fieldName(param.name)}=${generateTestValue(param.type, param.name)}`);
|
|
@@ -1511,10 +1515,11 @@ function generateModelRoundTripTests(spec: ApiSpec, ctx: EmitterContext): Genera
|
|
|
1511
1515
|
// don't have a to_dict() and their round-trip semantics differ.
|
|
1512
1516
|
if (model.fields.length === 0) continue;
|
|
1513
1517
|
if ((model as any).discriminator) continue;
|
|
1514
|
-
// Deduplicate fields
|
|
1518
|
+
// Deduplicate fields by DOMAIN identifier (mirrors models.ts, which honors
|
|
1519
|
+
// `domainName`); the wire key stays `field.name`.
|
|
1515
1520
|
const seenFieldNames = new Set<string>();
|
|
1516
1521
|
const dedupFields = model.fields.filter((f) => {
|
|
1517
|
-
const pyName =
|
|
1522
|
+
const pyName = domainFieldName(f);
|
|
1518
1523
|
if (seenFieldNames.has(pyName)) return false;
|
|
1519
1524
|
seenFieldNames.add(pyName);
|
|
1520
1525
|
return true;
|
package/src/ruby/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { generateTests } from './tests.js';
|
|
|
16
16
|
import { buildOperationsMap } from './manifest.js';
|
|
17
17
|
import { generateRbiFiles } from './rbi.js';
|
|
18
18
|
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
19
|
+
import { AUTOGEN_NOTICE } from '../shared/file-header.js';
|
|
19
20
|
|
|
20
21
|
/** Ensure every generated file's content ends with a trailing newline. */
|
|
21
22
|
function ensureTrailingNewlines(files: GeneratedFile[]): GeneratedFile[] {
|
|
@@ -93,7 +94,7 @@ export const rubyEmitter: Emitter = {
|
|
|
93
94
|
},
|
|
94
95
|
|
|
95
96
|
fileHeader(): string {
|
|
96
|
-
return `# frozen_string_literal: true\n\n#
|
|
97
|
+
return `# frozen_string_literal: true\n\n# ${AUTOGEN_NOTICE}`;
|
|
97
98
|
},
|
|
98
99
|
|
|
99
100
|
formatCommand(targetDir: string): FormatCommand | null {
|
package/src/ruby/models.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Model, EmitterContext, GeneratedFile, TypeRef, Field } from '@workos/oagen';
|
|
2
2
|
import { walkTypeRef, assignModelsToServices } from '@workos/oagen';
|
|
3
|
-
import { className,
|
|
3
|
+
import { className, domainFieldName, fileName, buildMountDirMap } from './naming.js';
|
|
4
4
|
import {
|
|
5
5
|
isListWrapperModel,
|
|
6
6
|
isListMetadataModel,
|
|
@@ -131,7 +131,8 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
131
131
|
// Deduplicate field names that collide after snake_case.
|
|
132
132
|
const seenFieldNames = new Set<string>();
|
|
133
133
|
const fields = model.fields.filter((f) => {
|
|
134
|
-
|
|
134
|
+
// Dedup on the DOMAIN accessor name (honors fieldHints override).
|
|
135
|
+
const n = domainFieldName(f);
|
|
135
136
|
if (seenFieldNames.has(n)) return false;
|
|
136
137
|
seenFieldNames.add(n);
|
|
137
138
|
return true;
|
|
@@ -151,7 +152,8 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
151
152
|
lines.push(' HASH_ATTRS = {');
|
|
152
153
|
for (let i = 0; i < fields.length; i++) {
|
|
153
154
|
const field = fields[i];
|
|
154
|
-
|
|
155
|
+
// DOMAIN attr symbol (honors fieldHints); the key below is the WIRE name.
|
|
156
|
+
const fname = domainFieldName(field);
|
|
155
157
|
const sep = i === fields.length - 1 ? '' : ',';
|
|
156
158
|
lines.push(` ${rubyHashLiteralKey(field.name)} :${fname}${sep}`);
|
|
157
159
|
}
|
|
@@ -162,14 +164,14 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
162
164
|
if (deprecatedFields.length > 0) {
|
|
163
165
|
for (const f of deprecatedFields) {
|
|
164
166
|
const desc = f.description ? ` ${f.description.split('\n')[0].trim()}` : '';
|
|
165
|
-
lines.push(` # @!attribute ${
|
|
167
|
+
lines.push(` # @!attribute ${domainFieldName(f)}`);
|
|
166
168
|
lines.push(` # @deprecated${desc}`);
|
|
167
169
|
}
|
|
168
170
|
lines.push('');
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
if (accessorFields.length > 0) {
|
|
172
|
-
const attrs = accessorFields.map((f) => `:${
|
|
174
|
+
const attrs = accessorFields.map((f) => `:${domainFieldName(f)}`);
|
|
173
175
|
if (attrs.length === 1) {
|
|
174
176
|
lines.push(` attr_accessor ${attrs[0]}`);
|
|
175
177
|
} else {
|
|
@@ -184,7 +186,7 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
184
186
|
|
|
185
187
|
// Emit deprecated field accessors with runtime warnings.
|
|
186
188
|
for (const f of deprecatedFields) {
|
|
187
|
-
const fname =
|
|
189
|
+
const fname = domainFieldName(f);
|
|
188
190
|
lines.push(` def ${fname}`);
|
|
189
191
|
lines.push(
|
|
190
192
|
` warn "[DEPRECATION] \\\`${fname}\\\` is deprecated and will be removed in a future version.", uplevel: 1`,
|
|
@@ -202,7 +204,8 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
202
204
|
lines.push(' def initialize(json)');
|
|
203
205
|
lines.push(' hash = self.class.normalize(json)');
|
|
204
206
|
for (const field of fields) {
|
|
205
|
-
|
|
207
|
+
// DOMAIN ivar name (honors fieldHints); rawKey is the WIRE key read from the hash.
|
|
208
|
+
const fname = domainFieldName(field);
|
|
206
209
|
const rawKey = field.name;
|
|
207
210
|
lines.push(` ${deserializeAssignment(fname, rawKey, field.type, field.required, enumNames, modelNames)}`);
|
|
208
211
|
}
|
package/src/ruby/naming.ts
CHANGED
|
@@ -57,6 +57,16 @@ export function fieldName(name: string): string {
|
|
|
57
57
|
return toSnakeCase(name);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* snake_case domain field 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 (still derived from `field.name`) is what
|
|
64
|
+
* gets sent/received over the wire — only the domain attr/accessor name changes.
|
|
65
|
+
*/
|
|
66
|
+
export function domainFieldName(field: { name: string; domainName?: string }): string {
|
|
67
|
+
return toSnakeCase(field.domainName ?? field.name);
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
/**
|
|
61
71
|
* Ruby reserved words that cannot be used as parameter names.
|
|
62
72
|
* When a path/query param name collides, suffix with underscore.
|
package/src/ruby/rbi.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { mapTypeRef as irMapTypeRef } from '@workos/oagen';
|
|
|
3
3
|
import {
|
|
4
4
|
className,
|
|
5
5
|
fieldName,
|
|
6
|
+
domainFieldName,
|
|
6
7
|
fileName,
|
|
7
8
|
safeParamName,
|
|
8
9
|
scopedGroupVariantClassName,
|
|
@@ -92,7 +93,8 @@ export function generateRbiFiles(spec: ApiSpec, ctx: EmitterContext): GeneratedF
|
|
|
92
93
|
// Field accessors
|
|
93
94
|
const seenFieldNames = new Set<string>();
|
|
94
95
|
for (const f of model.fields) {
|
|
95
|
-
|
|
96
|
+
// DOMAIN accessor name in the .rbi (honors fieldHints override).
|
|
97
|
+
const fname = domainFieldName(f);
|
|
96
98
|
if (seenFieldNames.has(fname)) continue;
|
|
97
99
|
seenFieldNames.add(fname);
|
|
98
100
|
const sorbetType = f.required ? mapSorbetType(f.type) : wrapNilable(mapSorbetType(f.type));
|
package/src/ruby/tests.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
className,
|
|
4
4
|
fileName,
|
|
5
5
|
fieldName,
|
|
6
|
+
domainFieldName,
|
|
6
7
|
safeParamName,
|
|
7
8
|
scopedGroupVariantClassName,
|
|
8
9
|
servicePropertyName,
|
|
@@ -235,7 +236,9 @@ function generateModelRoundTripTest(spec: ApiSpec): GeneratedFile {
|
|
|
235
236
|
const dedupFields = new Set<string>();
|
|
236
237
|
for (const f of model.fields) {
|
|
237
238
|
const wireName = f.name;
|
|
238
|
-
|
|
239
|
+
// Dedup on the DOMAIN accessor name to mirror the model's field dedup
|
|
240
|
+
// (models.ts). The fixture/assertion keys below still use the WIRE name.
|
|
241
|
+
const rubyFieldName = domainFieldName(f);
|
|
239
242
|
if (dedupFields.has(rubyFieldName)) continue;
|
|
240
243
|
dedupFields.add(rubyFieldName);
|
|
241
244
|
const stub = roundTripStub(f.type, enumNames);
|
package/src/rust/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { generateTests } from './tests.js';
|
|
|
17
17
|
import { buildOperationsMap } from './manifest.js';
|
|
18
18
|
import { UnionRegistry } from './type-map.js';
|
|
19
19
|
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
20
|
+
import { AUTOGEN_NOTICE } from '../shared/file-header.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Shared per-emit registry that collects synthesised oneOf-style unions
|
|
@@ -98,7 +99,7 @@ export const rustEmitter: Emitter = {
|
|
|
98
99
|
},
|
|
99
100
|
|
|
100
101
|
fileHeader(): string {
|
|
101
|
-
return
|
|
102
|
+
return `// ${AUTOGEN_NOTICE}`;
|
|
102
103
|
},
|
|
103
104
|
|
|
104
105
|
formatCommand(_targetDir: string): FormatCommand | null {
|
package/src/rust/models.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Model, EmitterContext, GeneratedFile, Field } from '@workos/oagen';
|
|
2
|
-
import { typeName,
|
|
1
|
+
import type { Model, EmitterContext, GeneratedFile, Field, TypeRef } from '@workos/oagen';
|
|
2
|
+
import { typeName, domainFieldName, moduleName } from './naming.js';
|
|
3
3
|
import { mapTypeRef, makeOptional, UnionRegistry } from './type-map.js';
|
|
4
4
|
import { applySecretRedaction } from './secret.js';
|
|
5
5
|
|
|
@@ -18,6 +18,13 @@ export function generateModels(models: Model[], ctx: EmitterContext, registry: U
|
|
|
18
18
|
const moduleNames: string[] = [];
|
|
19
19
|
const seen = new Set<string>();
|
|
20
20
|
|
|
21
|
+
// Map of variant-model name -> discriminator wire-property for every model
|
|
22
|
+
// that appears as an arm of an internally-tagged (`#[serde(tag = ...)]`)
|
|
23
|
+
// union. serde consumes that property as the enum tag and strips it from the
|
|
24
|
+
// variant body during deserialization, so a *required* field of the same
|
|
25
|
+
// name can never be satisfied ("missing field `type`"). See renderField.
|
|
26
|
+
const taggedVariantFields = collectTaggedVariantFields(models);
|
|
27
|
+
|
|
21
28
|
for (const model of models) {
|
|
22
29
|
// Empty-field, non-discriminator models still need to be emitted as an
|
|
23
30
|
// empty struct so request bodies that reference them (e.g. an empty
|
|
@@ -29,7 +36,11 @@ export function generateModels(models: Model[], ctx: EmitterContext, registry: U
|
|
|
29
36
|
|
|
30
37
|
const hintPath = ctx.overlayLookup?.fileBySymbol?.get(model.name);
|
|
31
38
|
const path = hintPath ?? `src/models/${mod}.rs`;
|
|
32
|
-
files.push({
|
|
39
|
+
files.push({
|
|
40
|
+
path,
|
|
41
|
+
content: renderModel(model, registry, taggedVariantFields.get(model.name)),
|
|
42
|
+
overwriteExisting: true,
|
|
43
|
+
});
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
// Always include the unions module in the barrel so downstream stages
|
|
@@ -47,7 +58,48 @@ export function generateModels(models: Model[], ctx: EmitterContext, registry: U
|
|
|
47
58
|
return files;
|
|
48
59
|
}
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Walk every model field and record which models are arms of an
|
|
63
|
+
* internally-tagged union, mapped to that union's discriminator property.
|
|
64
|
+
*/
|
|
65
|
+
function collectTaggedVariantFields(models: Model[]): Map<string, string> {
|
|
66
|
+
const out = new Map<string, string>();
|
|
67
|
+
const visit = (ref: TypeRef): void => {
|
|
68
|
+
switch (ref.kind) {
|
|
69
|
+
case 'union':
|
|
70
|
+
if (ref.discriminator?.property) {
|
|
71
|
+
for (const variant of ref.variants) {
|
|
72
|
+
const name = variantModelName(variant);
|
|
73
|
+
if (name) out.set(name, ref.discriminator.property);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
ref.variants.forEach(visit);
|
|
77
|
+
break;
|
|
78
|
+
case 'array':
|
|
79
|
+
visit(ref.items);
|
|
80
|
+
break;
|
|
81
|
+
case 'nullable':
|
|
82
|
+
visit(ref.inner);
|
|
83
|
+
break;
|
|
84
|
+
case 'map':
|
|
85
|
+
visit(ref.valueType);
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
for (const model of models) for (const field of model.fields) visit(field.type);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Resolve the underlying model name of a union arm, unwrapping a nullable. */
|
|
96
|
+
function variantModelName(ref: TypeRef): string | null {
|
|
97
|
+
if (ref.kind === 'model') return ref.name;
|
|
98
|
+
if (ref.kind === 'nullable') return variantModelName(ref.inner);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderModel(model: Model, registry: UnionRegistry, tagField?: string): string {
|
|
51
103
|
const lines: string[] = [];
|
|
52
104
|
lines.push(HEADER_PLACEHOLDER);
|
|
53
105
|
// Match rustfmt's canonical grouping: keyword-rooted paths (`super`,
|
|
@@ -65,7 +117,7 @@ function renderModel(model: Model, registry: UnionRegistry): string {
|
|
|
65
117
|
lines.push('#[derive(Debug, Clone, Serialize, Deserialize)]');
|
|
66
118
|
|
|
67
119
|
const resolvedNames = resolveFieldNames(model.fields);
|
|
68
|
-
const fieldLines = model.fields.map((f, i) => renderField(f, resolvedNames[i]!, model.name, registry));
|
|
120
|
+
const fieldLines = model.fields.map((f, i) => renderField(f, resolvedNames[i]!, model.name, registry, tagField));
|
|
69
121
|
|
|
70
122
|
// rustfmt collapses zero-field structs to `pub struct Foo {}` on a single
|
|
71
123
|
// line. Match that shape so `cargo fmt --check` passes.
|
|
@@ -80,17 +132,20 @@ function renderModel(model: Model, registry: UnionRegistry): string {
|
|
|
80
132
|
}
|
|
81
133
|
|
|
82
134
|
/**
|
|
83
|
-
* Resolve unique Rust identifiers for struct fields.
|
|
84
|
-
*
|
|
85
|
-
* `
|
|
86
|
-
*
|
|
87
|
-
*
|
|
135
|
+
* Resolve unique Rust identifiers for struct fields. The domain identifier
|
|
136
|
+
* honors a `fieldHints` override (`domainName`, e.g. wire `connection_type` →
|
|
137
|
+
* domain `type`); the wire name (and the `#[serde(rename = ...)]` key emitted
|
|
138
|
+
* in `renderField`) still derives from `f.name`. Multiple names can collide
|
|
139
|
+
* after snake-casing (e.g. `integration_type` and `integrationType` both
|
|
140
|
+
* become `integration_type`). Subsequent collisions get a numeric suffix so
|
|
141
|
+
* the struct compiles; serde `rename` preserves the original wire name in every
|
|
142
|
+
* case.
|
|
88
143
|
*/
|
|
89
144
|
function resolveFieldNames(fields: Field[]): string[] {
|
|
90
145
|
const used = new Set<string>();
|
|
91
146
|
const out: string[] = [];
|
|
92
147
|
for (const f of fields) {
|
|
93
|
-
const base =
|
|
148
|
+
const base = domainFieldName(f);
|
|
94
149
|
let candidate = base;
|
|
95
150
|
let suffix = 2;
|
|
96
151
|
while (used.has(candidate)) {
|
|
@@ -103,7 +158,13 @@ function resolveFieldNames(fields: Field[]): string[] {
|
|
|
103
158
|
return out;
|
|
104
159
|
}
|
|
105
160
|
|
|
106
|
-
function renderField(
|
|
161
|
+
function renderField(
|
|
162
|
+
field: Field,
|
|
163
|
+
rustField: string,
|
|
164
|
+
modelName: string,
|
|
165
|
+
registry: UnionRegistry,
|
|
166
|
+
tagField?: string,
|
|
167
|
+
): string {
|
|
107
168
|
const lines: string[] = [];
|
|
108
169
|
const hasDescription = !!field.description;
|
|
109
170
|
if (hasDescription) {
|
|
@@ -128,9 +189,20 @@ function renderField(field: Field, rustField: string, modelName: string, registr
|
|
|
128
189
|
// the value is a credential or token. Wire format is unchanged.
|
|
129
190
|
baseType = applySecretRedaction(baseType, field.name);
|
|
130
191
|
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
192
|
+
if (tagField === field.name) {
|
|
193
|
+
// This field is the discriminator of an internally-tagged union it belongs
|
|
194
|
+
// to. serde reads it as the enum tag and strips it from the variant body,
|
|
195
|
+
// so `default` lets the struct deserialize without it; `skip_serializing`
|
|
196
|
+
// stops the struct from re-emitting it (serde injects the tag itself,
|
|
197
|
+
// which would otherwise produce a duplicate key). Standalone uses of the
|
|
198
|
+
// struct still deserialize the value normally because the key is present.
|
|
199
|
+
const args = rename ? `rename = "${rename}", default, skip_serializing` : 'default, skip_serializing';
|
|
200
|
+
lines.push(` #[serde(${args})]`);
|
|
201
|
+
} else {
|
|
202
|
+
if (rename) lines.push(` #[serde(rename = "${rename}")]`);
|
|
203
|
+
if (baseType.startsWith('Option<')) {
|
|
204
|
+
lines.push(' #[serde(skip_serializing_if = "Option::is_none", default)]');
|
|
205
|
+
}
|
|
134
206
|
}
|
|
135
207
|
if (field.deprecated) lines.push(' #[deprecated]');
|
|
136
208
|
lines.push(` pub ${rustField}: ${baseType},`);
|
package/src/rust/naming.ts
CHANGED
|
@@ -77,6 +77,16 @@ export function fieldName(name: string): string {
|
|
|
77
77
|
return escapeKeyword(toSnakeCase(name));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* snake_case domain field name for a model field, honoring a `domainName`
|
|
82
|
+
* override (set via the `fieldHints` config) so a wire field can surface under
|
|
83
|
+
* a friendlier identifier. The wire name (and thus the `#[serde(rename = ...)]`
|
|
84
|
+
* key) still derives from `field.name`.
|
|
85
|
+
*/
|
|
86
|
+
export function domainFieldName(field: { name: string; domainName?: string }): string {
|
|
87
|
+
return escapeKeyword(toSnakeCase(field.domainName ?? field.name));
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
/** PascalCase enum variant. */
|
|
81
91
|
export function variantName(value: string | number): string {
|
|
82
92
|
const s = String(value);
|
package/src/rust/resources.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
TypeRef,
|
|
11
11
|
} from '@workos/oagen';
|
|
12
12
|
import { planOperation } from '@workos/oagen';
|
|
13
|
-
import { fieldName, methodName, typeName, moduleName, variantName } from './naming.js';
|
|
13
|
+
import { fieldName, domainFieldName, methodName, typeName, moduleName, variantName } from './naming.js';
|
|
14
14
|
import { mapTypeRef, makeOptional, UnionRegistry } from './type-map.js';
|
|
15
15
|
import { applySecretRedaction } from './secret.js';
|
|
16
16
|
import { parsePathTemplate } from '../shared/path-template.js';
|
|
@@ -617,7 +617,11 @@ function registerSyntheticBody(
|
|
|
617
617
|
if (!f.required && !rust.startsWith('Option<')) rust = makeOptional(rust);
|
|
618
618
|
rust = applySecretRedaction(rust, f.name);
|
|
619
619
|
return {
|
|
620
|
-
|
|
620
|
+
// Domain identifier honors a `fieldHints` override (e.g. wire
|
|
621
|
+
// `connection_type` → domain `type`); `wireName` keeps `f.name`, and
|
|
622
|
+
// the `#[serde(rename = wireName)]` emitted in GroupEmitter.render
|
|
623
|
+
// fires whenever the two differ.
|
|
624
|
+
rustName: domainFieldName(f),
|
|
621
625
|
wireName: f.name,
|
|
622
626
|
rustType: rust,
|
|
623
627
|
required: !!f.required && !rust.startsWith('Option<'),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical "do not edit" banner text shared by every emitter's
|
|
3
|
+
* `fileHeader()`. The text is comment-syntax agnostic — each emitter prefixes
|
|
4
|
+
* it with the appropriate comment marker for its language (`//`, `#`, etc.).
|
|
5
|
+
*
|
|
6
|
+
* Keeping this in one place prevents the wording from drifting between
|
|
7
|
+
* languages (Rust previously emitted a different banner).
|
|
8
|
+
*
|
|
9
|
+
* Go intentionally does NOT use this constant: its header must match the
|
|
10
|
+
* standard `^// Code generated .* DO NOT EDIT\.$` marker that Go tooling
|
|
11
|
+
* relies on to recognize generated files. See `src/go/index.ts`.
|
|
12
|
+
*/
|
|
13
|
+
export const AUTOGEN_NOTICE = 'This file is auto-generated by oagen. Do not edit.';
|