@workos/oagen-emitters 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lint.yml +1 -1
- package/.github/workflows/release-please.yml +2 -2
- package/.github/workflows/release.yml +1 -1
- package/.husky/pre-push +11 -0
- package/.node-version +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +35 -224
- package/dist/index.d.mts +9 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -15234
- package/dist/plugin-BSop9f9z.mjs +21471 -0
- package/dist/plugin-BSop9f9z.mjs.map +1 -0
- package/dist/plugin.d.mts +7 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +2 -0
- package/docs/sdk-architecture/dotnet.md +5 -5
- package/oagen.config.ts +5 -373
- package/package.json +10 -34
- package/src/dotnet/index.ts +6 -4
- package/src/dotnet/models.ts +58 -82
- package/src/dotnet/naming.ts +44 -6
- package/src/dotnet/resources.ts +350 -29
- package/src/dotnet/tests.ts +44 -24
- package/src/dotnet/type-map.ts +44 -17
- package/src/dotnet/wrappers.ts +21 -10
- package/src/go/client.ts +35 -3
- package/src/go/enums.ts +4 -0
- package/src/go/index.ts +10 -5
- package/src/go/models.ts +6 -1
- package/src/go/resources.ts +534 -73
- package/src/go/tests.ts +39 -3
- package/src/go/type-map.ts +8 -3
- package/src/go/wrappers.ts +79 -21
- package/src/index.ts +14 -0
- package/src/kotlin/client.ts +7 -2
- package/src/kotlin/enums.ts +30 -3
- package/src/kotlin/models.ts +97 -6
- package/src/kotlin/naming.ts +7 -1
- package/src/kotlin/resources.ts +370 -39
- package/src/kotlin/tests.ts +120 -6
- package/src/node/client.ts +38 -11
- package/src/node/field-plan.ts +12 -14
- package/src/node/fixtures.ts +39 -3
- package/src/node/models.ts +281 -37
- package/src/node/resources.ts +156 -52
- package/src/node/tests.ts +76 -27
- package/src/node/type-map.ts +1 -31
- package/src/node/utils.ts +96 -6
- package/src/node/wrappers.ts +31 -1
- package/src/php/models.ts +0 -33
- package/src/php/resources.ts +199 -18
- package/src/php/tests.ts +26 -2
- package/src/php/type-map.ts +16 -2
- package/src/php/wrappers.ts +6 -2
- package/src/plugin.ts +50 -0
- package/src/python/client.ts +13 -3
- package/src/python/enums.ts +28 -3
- package/src/python/index.ts +35 -27
- package/src/python/models.ts +138 -1
- package/src/python/resources.ts +234 -17
- package/src/python/tests.ts +260 -16
- package/src/python/type-map.ts +16 -2
- package/src/ruby/client.ts +238 -0
- package/src/ruby/enums.ts +149 -0
- package/src/ruby/index.ts +93 -0
- package/src/ruby/manifest.ts +35 -0
- package/src/ruby/models.ts +360 -0
- package/src/ruby/naming.ts +187 -0
- package/src/ruby/rbi.ts +313 -0
- package/src/ruby/resources.ts +799 -0
- package/src/ruby/tests.ts +459 -0
- package/src/ruby/type-map.ts +97 -0
- package/src/ruby/wrappers.ts +161 -0
- package/src/shared/model-utils.ts +131 -7
- package/src/shared/naming-utils.ts +36 -0
- package/src/shared/non-spec-services.ts +13 -0
- package/src/shared/resolved-ops.ts +75 -1
- package/test/dotnet/client.test.ts +2 -2
- package/test/dotnet/models.test.ts +7 -9
- package/test/dotnet/resources.test.ts +135 -3
- package/test/dotnet/tests.test.ts +5 -5
- package/test/entrypoint.test.ts +89 -0
- package/test/go/client.test.ts +6 -6
- package/test/go/resources.test.ts +156 -7
- package/test/kotlin/models.test.ts +1 -1
- package/test/kotlin/resources.test.ts +210 -0
- package/test/node/models.test.ts +134 -1
- package/test/node/resources.test.ts +134 -26
- package/test/node/utils.test.ts +140 -0
- package/test/php/models.test.ts +5 -4
- package/test/php/resources.test.ts +66 -1
- package/test/plugin.test.ts +50 -0
- package/test/python/client.test.ts +56 -0
- package/test/python/models.test.ts +99 -0
- package/test/python/resources.test.ts +294 -0
- package/test/python/tests.test.ts +91 -0
- package/test/ruby/client.test.ts +81 -0
- package/test/ruby/resources.test.ts +386 -0
- package/test/shared/resolved-ops.test.ts +122 -0
- package/tsdown.config.ts +1 -1
- package/dist/index.mjs.map +0 -1
- package/scripts/generate-php.js +0 -13
- package/scripts/git-push-with-published-oagen.sh +0 -21
package/src/python/resources.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TypeRef,
|
|
8
8
|
ResolvedOperation,
|
|
9
9
|
Parameter,
|
|
10
|
+
Model,
|
|
10
11
|
} from '@workos/oagen';
|
|
11
12
|
|
|
12
13
|
/** Extend Parameter with `explode` until @workos/oagen publishes the field. */
|
|
@@ -38,6 +39,8 @@ import {
|
|
|
38
39
|
getOpDefaults,
|
|
39
40
|
getOpInferFromClient,
|
|
40
41
|
buildHiddenParams as buildHiddenParamsShared,
|
|
42
|
+
collectGroupedParamNames,
|
|
43
|
+
collectBodyFieldTypes,
|
|
41
44
|
} from '../shared/resolved-ops.js';
|
|
42
45
|
import {
|
|
43
46
|
generateSyncWrapperMethods,
|
|
@@ -65,6 +68,85 @@ export function resolveResourceClassName(service: Service, ctx: EmitterContext):
|
|
|
65
68
|
// buildHiddenParams is imported from ../shared/resolved-ops.js as buildHiddenParamsShared
|
|
66
69
|
const buildHiddenParams = buildHiddenParamsShared;
|
|
67
70
|
|
|
71
|
+
// ─── Parameter group support ─────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* PascalCase variant class name for a parameter group variant.
|
|
75
|
+
* E.g., group "parent_resource", variant "by_id" -> "ParentResourceById".
|
|
76
|
+
*/
|
|
77
|
+
function groupVariantClassName(groupName: string, variantName: string): string {
|
|
78
|
+
return className(`${groupName}_${variantName}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generate Python dataclass definitions for all parameter group variants
|
|
83
|
+
* across a set of operations. Returns lines to insert near the top of the
|
|
84
|
+
* resource file (after imports, before class definitions).
|
|
85
|
+
*/
|
|
86
|
+
function generateParameterGroupDataclasses(
|
|
87
|
+
operations: Operation[],
|
|
88
|
+
specEnumNames: Set<string>,
|
|
89
|
+
models: Model[],
|
|
90
|
+
): string[] {
|
|
91
|
+
const lines: string[] = [];
|
|
92
|
+
const emitted = new Set<string>();
|
|
93
|
+
|
|
94
|
+
for (const op of operations) {
|
|
95
|
+
const bodyFieldTypes = collectBodyFieldTypes(op, models);
|
|
96
|
+
for (const group of op.parameterGroups ?? []) {
|
|
97
|
+
for (const variant of group.variants) {
|
|
98
|
+
const variantClass = groupVariantClassName(group.name, variant.name);
|
|
99
|
+
if (emitted.has(variantClass)) continue;
|
|
100
|
+
emitted.add(variantClass);
|
|
101
|
+
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push('@dataclass');
|
|
104
|
+
lines.push(`class ${variantClass}:`);
|
|
105
|
+
const readableGroup = group.name.replace(/_/g, ' ');
|
|
106
|
+
const readableVariant = variant.name.replace(/_/g, ' ');
|
|
107
|
+
lines.push(` """Identify ${readableGroup} ${readableVariant}."""`);
|
|
108
|
+
for (const param of variant.parameters) {
|
|
109
|
+
const pyField = fieldName(param.name);
|
|
110
|
+
const effectiveType = bodyFieldTypes.get(param.name) ?? param.type;
|
|
111
|
+
const pyType = mapTypeRefUnquoted(effectiveType, specEnumNames, true);
|
|
112
|
+
lines.push(` ${pyField}: ${pyType}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check whether any operation has parameter groups.
|
|
123
|
+
*/
|
|
124
|
+
function hasParameterGroups(operations: Operation[]): boolean {
|
|
125
|
+
return operations.some((op) => (op.parameterGroups?.length ?? 0) > 0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Collect the PascalCase class names of all parameter group variants
|
|
130
|
+
* across a set of operations. Used by client.ts to re-export these
|
|
131
|
+
* dataclasses from the service __init__.py.
|
|
132
|
+
*/
|
|
133
|
+
export function collectParameterGroupClassNames(operations: Operation[]): string[] {
|
|
134
|
+
const names: string[] = [];
|
|
135
|
+
const seen = new Set<string>();
|
|
136
|
+
for (const op of operations) {
|
|
137
|
+
for (const group of op.parameterGroups ?? []) {
|
|
138
|
+
for (const variant of group.variants) {
|
|
139
|
+
const cls = groupVariantClassName(group.name, variant.name);
|
|
140
|
+
if (!seen.has(cls)) {
|
|
141
|
+
seen.add(cls);
|
|
142
|
+
names.push(cls);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return names;
|
|
148
|
+
}
|
|
149
|
+
|
|
68
150
|
// ─── Shared method-emission helpers ──────────────────────────────────
|
|
69
151
|
|
|
70
152
|
/** Metadata returned by emitMethodSignature, consumed by docstring & body emitters. */
|
|
@@ -133,11 +215,16 @@ function emitMethodSignature(
|
|
|
133
215
|
const pathParamNames = new Set(op.pathParams.map((p) => fieldName(p.name)));
|
|
134
216
|
|
|
135
217
|
// Request body fields as keyword args (rename fields that clash with path params)
|
|
218
|
+
const groupedBodyParamNames = collectGroupedParamNames(op);
|
|
136
219
|
if (plan.hasBody && op.requestBody) {
|
|
137
220
|
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
138
221
|
if (bodyModel) {
|
|
139
|
-
const reqFields = bodyModel.fields.filter(
|
|
140
|
-
|
|
222
|
+
const reqFields = bodyModel.fields.filter(
|
|
223
|
+
(f) => f.required && !hiddenParams.has(f.name) && !groupedBodyParamNames.has(f.name),
|
|
224
|
+
);
|
|
225
|
+
const optFields = bodyModel.fields.filter(
|
|
226
|
+
(f) => !f.required && !hiddenParams.has(f.name) && !groupedBodyParamNames.has(f.name),
|
|
227
|
+
);
|
|
141
228
|
for (const f of reqFields) {
|
|
142
229
|
const fieldType = mapTypeRefUnquoted(f.type, specEnumNames, true);
|
|
143
230
|
if (usesClientCredentialDefaults && (f.name === 'client_id' || f.name === 'client_secret')) {
|
|
@@ -175,9 +262,11 @@ function emitMethodSignature(
|
|
|
175
262
|
}
|
|
176
263
|
|
|
177
264
|
// Query params for non-paginated methods
|
|
265
|
+
const groupedParamNames = collectGroupedParamNames(op);
|
|
178
266
|
if (plan.hasQueryParams && !isPaginated) {
|
|
179
267
|
for (const param of op.queryParams) {
|
|
180
268
|
if (hiddenParams.has(param.name)) continue;
|
|
269
|
+
if (groupedParamNames.has(param.name)) continue;
|
|
181
270
|
const paramName = fieldName(param.name);
|
|
182
271
|
if (pathParamNames.has(paramName)) continue;
|
|
183
272
|
// Skip query params that collide with body field names (using possibly-renamed names)
|
|
@@ -207,11 +296,12 @@ function emitMethodSignature(
|
|
|
207
296
|
const orderParam = op.queryParams.find((p) => p.name === 'order');
|
|
208
297
|
const orderType =
|
|
209
298
|
orderParam && orderParam.type.kind === 'enum' ? mapTypeRefUnquoted(orderParam.type, specEnumNames, true) : 'str';
|
|
210
|
-
lines.push(` order: Optional[${orderType}] =
|
|
299
|
+
lines.push(` order: Optional[${orderType}] = "desc",`);
|
|
211
300
|
// Additional non-pagination query params
|
|
212
301
|
for (const param of op.queryParams) {
|
|
213
302
|
if (['limit', 'before', 'after', 'order'].includes(param.name)) continue;
|
|
214
303
|
if (hiddenParams.has(param.name)) continue;
|
|
304
|
+
if (groupedParamNames.has(param.name)) continue;
|
|
215
305
|
const paramName = fieldName(param.name);
|
|
216
306
|
const paramType = mapTypeRefUnquoted(param.type, specEnumNames, true);
|
|
217
307
|
if (param.required) {
|
|
@@ -222,6 +312,18 @@ function emitMethodSignature(
|
|
|
222
312
|
}
|
|
223
313
|
}
|
|
224
314
|
|
|
315
|
+
// Parameter group union kwargs
|
|
316
|
+
for (const group of op.parameterGroups ?? []) {
|
|
317
|
+
const variantClasses = group.variants.map((v) => groupVariantClassName(group.name, v.name));
|
|
318
|
+
const unionType = `Union[${variantClasses.join(', ')}]`;
|
|
319
|
+
const paramName = fieldName(group.name);
|
|
320
|
+
if (group.optional) {
|
|
321
|
+
lines.push(` ${paramName}: Optional[${unionType}] = None,`);
|
|
322
|
+
} else {
|
|
323
|
+
lines.push(` ${paramName}: ${unionType},`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
225
327
|
// Idempotency key for idempotent POSTs
|
|
226
328
|
if (plan.isIdempotentPost) {
|
|
227
329
|
lines.push(' idempotency_key: Optional[str] = None,');
|
|
@@ -249,7 +351,11 @@ function emitMethodSignature(
|
|
|
249
351
|
returnType = 'str';
|
|
250
352
|
} else if (isPaginated) {
|
|
251
353
|
const resolvedItem = resolvePageItemName(op.pagination!.itemType, listWrapperNames, ctx);
|
|
252
|
-
|
|
354
|
+
const resolvedItemModel = ctx.spec.models.find((m) => m.name === resolvedItem);
|
|
355
|
+
const itemTypeName = (resolvedItemModel as any)?.discriminator
|
|
356
|
+
? className(resolvedItem) + 'Variant'
|
|
357
|
+
: className(resolvedItem);
|
|
358
|
+
returnType = `${pageType}[${itemTypeName}]`;
|
|
253
359
|
} else if (isArrayResponse) {
|
|
254
360
|
returnType = `List[${className(plan.responseModelName!)}]`;
|
|
255
361
|
} else if (plan.responseModelName) {
|
|
@@ -299,6 +405,7 @@ function emitMethodDocstring(
|
|
|
299
405
|
}));
|
|
300
406
|
|
|
301
407
|
// Add body model fields to docs
|
|
408
|
+
const groupedDocBodyParams = collectGroupedParamNames(op);
|
|
302
409
|
if (plan.hasBody && op.requestBody) {
|
|
303
410
|
if (op.requestBody.kind === 'model') {
|
|
304
411
|
const requestBodyName = op.requestBody.name;
|
|
@@ -306,6 +413,7 @@ function emitMethodDocstring(
|
|
|
306
413
|
if (bodyModel) {
|
|
307
414
|
for (const f of bodyModel.fields) {
|
|
308
415
|
if (hiddenParams.has(f.name)) continue;
|
|
416
|
+
if (groupedDocBodyParams.has(f.name)) continue;
|
|
309
417
|
allParams.push({
|
|
310
418
|
name: bodyParamName(f, pathParamNames),
|
|
311
419
|
desc: f.deprecated ? (f.description ? `(deprecated) ${f.description}` : '(deprecated)') : f.description,
|
|
@@ -326,9 +434,11 @@ function emitMethodDocstring(
|
|
|
326
434
|
}
|
|
327
435
|
|
|
328
436
|
// Add query params for non-paginated methods
|
|
437
|
+
const groupedDocParams = collectGroupedParamNames(op);
|
|
329
438
|
if (plan.hasQueryParams && !isPaginated) {
|
|
330
439
|
for (const param of op.queryParams) {
|
|
331
440
|
if (hiddenParams.has(param.name)) continue;
|
|
441
|
+
if (groupedDocParams.has(param.name)) continue;
|
|
332
442
|
const pn = fieldName(param.name);
|
|
333
443
|
if (pathParamNames.has(pn)) continue;
|
|
334
444
|
// Skip params already documented from body fields
|
|
@@ -362,6 +472,7 @@ function emitMethodDocstring(
|
|
|
362
472
|
}
|
|
363
473
|
for (const param of op.queryParams) {
|
|
364
474
|
if (['limit', 'before', 'after', 'order'].includes(param.name)) continue;
|
|
475
|
+
if (groupedDocParams.has(param.name)) continue;
|
|
365
476
|
let desc = param.deprecated
|
|
366
477
|
? param.description
|
|
367
478
|
? `(deprecated) ${param.description}`
|
|
@@ -375,6 +486,14 @@ function emitMethodDocstring(
|
|
|
375
486
|
}
|
|
376
487
|
}
|
|
377
488
|
|
|
489
|
+
// Add parameter group docs
|
|
490
|
+
for (const group of op.parameterGroups ?? []) {
|
|
491
|
+
const variantClasses = group.variants.map((v) => groupVariantClassName(group.name, v.name));
|
|
492
|
+
const readableGroup = group.name.replace(/_/g, ' ');
|
|
493
|
+
const desc = `Identifies the ${readableGroup}. One of: ${variantClasses.join(', ')}.`;
|
|
494
|
+
allParams.push({ name: fieldName(group.name), desc });
|
|
495
|
+
}
|
|
496
|
+
|
|
378
497
|
// Add idempotency key parameter to docs
|
|
379
498
|
if (plan.isIdempotentPost) {
|
|
380
499
|
allParams.push({ name: 'idempotency_key', desc: 'Optional idempotency key for safe retries.' });
|
|
@@ -521,6 +640,10 @@ function emitMethodBody(
|
|
|
521
640
|
} else if (isPaginated) {
|
|
522
641
|
const resolvedItemName = resolvePageItemName(op.pagination!.itemType, listWrapperNames, ctx);
|
|
523
642
|
const itemTypeClass = className(resolvedItemName);
|
|
643
|
+
const resolvedItemModel = ctx.spec.models.find((m) => m.name === resolvedItemName);
|
|
644
|
+
const isDiscriminatorModel = !!(resolvedItemModel as any)?.discriminator;
|
|
645
|
+
const variantTypeName = isDiscriminatorModel ? itemTypeClass + 'Variant' : null;
|
|
646
|
+
const pageType = isAsync ? 'AsyncPage' : 'SyncPage';
|
|
524
647
|
const orderParam = op.queryParams.find((p) => p.name === 'order');
|
|
525
648
|
// Build query params dict
|
|
526
649
|
lines.push(' params = {k: v for k, v in {');
|
|
@@ -530,9 +653,11 @@ function emitMethodBody(
|
|
|
530
653
|
lines.push(
|
|
531
654
|
` "order": ${serializeParameterValue(orderParam?.type, 'order', false, (orderParam as ParameterExt | undefined)?.explode)},`,
|
|
532
655
|
);
|
|
656
|
+
const paginatedGroupedParams = collectGroupedParamNames(op);
|
|
533
657
|
for (const param of op.queryParams) {
|
|
534
658
|
if (['limit', 'before', 'after', 'order'].includes(param.name)) continue;
|
|
535
659
|
if (hiddenParams.has(param.name)) continue;
|
|
660
|
+
if (paginatedGroupedParams.has(param.name)) continue;
|
|
536
661
|
const pn = fieldName(param.name);
|
|
537
662
|
const value = serializeParameterValue(param.type, pn, param.required, (param as ParameterExt).explode);
|
|
538
663
|
lines.push(` "${param.name}": ${value},`);
|
|
@@ -552,20 +677,42 @@ function emitMethodBody(
|
|
|
552
677
|
lines.push(` params["${field}"] = ${expr}`);
|
|
553
678
|
}
|
|
554
679
|
}
|
|
555
|
-
|
|
556
|
-
lines
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
680
|
+
// isinstance dispatch for parameter groups
|
|
681
|
+
emitGroupDispatch(lines, op);
|
|
682
|
+
if (isDiscriminatorModel && variantTypeName) {
|
|
683
|
+
// Dispatcher model: cast the page to the variant union type so callers get
|
|
684
|
+
// rich type information when iterating events.
|
|
685
|
+
lines.push(` return cast(`);
|
|
686
|
+
lines.push(` ${pageType}[${variantTypeName}],`);
|
|
687
|
+
lines.push(` ${awaitPrefix}self._client.request_page(`);
|
|
688
|
+
lines.push(` method="${httpMethod}",`);
|
|
689
|
+
lines.push(` path=${pathStr},`);
|
|
690
|
+
lines.push(
|
|
691
|
+
` model=${itemTypeClass}, # type: ignore[arg-type] # dispatcher; pagination only calls from_dict`,
|
|
692
|
+
);
|
|
693
|
+
lines.push(' params=params,');
|
|
694
|
+
lines.push(' request_options=request_options,');
|
|
695
|
+
lines.push(' )');
|
|
696
|
+
lines.push(' )');
|
|
697
|
+
} else {
|
|
698
|
+
lines.push(` return ${awaitPrefix}self._client.request_page(`);
|
|
699
|
+
lines.push(` method="${httpMethod}",`);
|
|
700
|
+
lines.push(` path=${pathStr},`);
|
|
701
|
+
lines.push(` model=${itemTypeClass},`);
|
|
702
|
+
lines.push(' params=params,');
|
|
703
|
+
lines.push(' request_options=request_options,');
|
|
704
|
+
lines.push(' )');
|
|
705
|
+
}
|
|
562
706
|
} else if (plan.isDelete) {
|
|
563
707
|
// Build body dict if the DELETE has a request body
|
|
708
|
+
const deleteGroupedParams = collectGroupedParamNames(op);
|
|
564
709
|
const deleteBodyFieldNames = new Set<string>();
|
|
565
710
|
if (plan.hasBody && op.requestBody) {
|
|
566
711
|
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
567
712
|
if (bodyModel) {
|
|
568
|
-
const bodyFields = bodyModel.fields.filter(
|
|
713
|
+
const bodyFields = bodyModel.fields.filter(
|
|
714
|
+
(f) => !hiddenParams.has(f.name) && !deleteGroupedParams.has(f.name),
|
|
715
|
+
);
|
|
569
716
|
for (const f of bodyFields) deleteBodyFieldNames.add(bodyParamName(f, pathParamNames));
|
|
570
717
|
const hasOptionalBodyFields = bodyFields.some((f) => !f.required);
|
|
571
718
|
if (bodyFields.length > 0 && hasOptionalBodyFields) {
|
|
@@ -599,6 +746,8 @@ function emitMethodBody(
|
|
|
599
746
|
lines.push(` body["${field}"] = ${expr}`);
|
|
600
747
|
}
|
|
601
748
|
}
|
|
749
|
+
// isinstance dispatch for parameter groups into body
|
|
750
|
+
emitGroupDispatch(lines, op, 'body', collectBodyFieldTypes(op, ctx.spec.models));
|
|
602
751
|
}
|
|
603
752
|
}
|
|
604
753
|
// Build query params dict if any exist alongside the body/path
|
|
@@ -618,10 +767,11 @@ function emitMethodBody(
|
|
|
618
767
|
} else if (plan.hasBody && op.requestBody) {
|
|
619
768
|
const responseModel = plan.responseModelName ? className(plan.responseModelName) : 'None';
|
|
620
769
|
// Build body dict
|
|
770
|
+
const bodyGroupedParams = collectGroupedParamNames(op);
|
|
621
771
|
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
622
772
|
const bodyFieldNamesSet = new Set<string>();
|
|
623
773
|
if (bodyModel) {
|
|
624
|
-
const bodyFields = bodyModel.fields.filter((f) => !hiddenParams.has(f.name));
|
|
774
|
+
const bodyFields = bodyModel.fields.filter((f) => !hiddenParams.has(f.name) && !bodyGroupedParams.has(f.name));
|
|
625
775
|
for (const f of bodyFields) bodyFieldNamesSet.add(bodyParamName(f, pathParamNames));
|
|
626
776
|
const hasOptionalBodyFields = bodyFields.some((f) => !f.required);
|
|
627
777
|
if (bodyFields.length > 0 && hasOptionalBodyFields) {
|
|
@@ -657,6 +807,8 @@ function emitMethodBody(
|
|
|
657
807
|
lines.push(` body["${field}"] = ${expr}`);
|
|
658
808
|
}
|
|
659
809
|
}
|
|
810
|
+
// isinstance dispatch for parameter groups into body
|
|
811
|
+
emitGroupDispatch(lines, op, 'body', collectBodyFieldTypes(op, ctx.spec.models));
|
|
660
812
|
} else {
|
|
661
813
|
// Union or non-model body — convert model instances to dicts
|
|
662
814
|
lines.push(' _body: Dict[str, Any] = body if isinstance(body, dict) else body.to_dict()');
|
|
@@ -710,11 +862,14 @@ function emitMethodBody(
|
|
|
710
862
|
} else {
|
|
711
863
|
// GET or similar with query params
|
|
712
864
|
const responseModel = plan.responseModelName ? className(plan.responseModelName) : 'None';
|
|
713
|
-
const
|
|
865
|
+
const getGroupedParams = collectGroupedParamNames(op);
|
|
866
|
+
const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
|
|
867
|
+
const visibleQueryParams = op.queryParams.filter((p) => !hiddenParams.has(p.name) && !getGroupedParams.has(p.name));
|
|
714
868
|
const hasVisibleQueryParams = plan.hasQueryParams && visibleQueryParams.length > 0;
|
|
715
869
|
const hasInjections =
|
|
716
870
|
(opDefaults && Object.keys(opDefaults).length > 0) || (opInferFromClient && opInferFromClient.length > 0);
|
|
717
|
-
|
|
871
|
+
const needsParamsDict = hasVisibleQueryParams || hasInjections || hasGroups;
|
|
872
|
+
if (needsParamsDict) {
|
|
718
873
|
const hasOptionalQueryParams = visibleQueryParams.some((p) => !p.required);
|
|
719
874
|
if (hasOptionalQueryParams) {
|
|
720
875
|
lines.push(' params: Dict[str, Any] = {k: v for k, v in {');
|
|
@@ -733,7 +888,7 @@ function emitMethodBody(
|
|
|
733
888
|
}
|
|
734
889
|
lines.push(' }');
|
|
735
890
|
} else {
|
|
736
|
-
// No visible query params but we have injections — start with empty dict
|
|
891
|
+
// No visible query params but we have injections or groups — start with empty dict
|
|
737
892
|
lines.push(' params: Dict[str, Any] = {}');
|
|
738
893
|
}
|
|
739
894
|
// Inject constant defaults
|
|
@@ -760,8 +915,10 @@ function emitMethodBody(
|
|
|
760
915
|
);
|
|
761
916
|
}
|
|
762
917
|
}
|
|
918
|
+
// isinstance dispatch for parameter groups
|
|
919
|
+
emitGroupDispatch(lines, op);
|
|
763
920
|
}
|
|
764
|
-
const emittedParams =
|
|
921
|
+
const emittedParams = needsParamsDict;
|
|
765
922
|
if (isArrayResponse) {
|
|
766
923
|
// Array response: request_list returns List[Dict], then deserialize each item
|
|
767
924
|
const itemModel = className(plan.responseModelName!);
|
|
@@ -791,6 +948,44 @@ function emitMethodBody(
|
|
|
791
948
|
}
|
|
792
949
|
}
|
|
793
950
|
|
|
951
|
+
/**
|
|
952
|
+
* Emit isinstance dispatch blocks for all parameter groups on an operation.
|
|
953
|
+
* Each group produces a chain of if/elif branches that test the group kwarg
|
|
954
|
+
* against each variant dataclass and serializes the variant's parameters
|
|
955
|
+
* into the target dict using their wire names.
|
|
956
|
+
*
|
|
957
|
+
* @param target - The Python dict variable to write into (e.g., "params" or "body").
|
|
958
|
+
*/
|
|
959
|
+
function emitGroupDispatch(
|
|
960
|
+
lines: string[],
|
|
961
|
+
op: Operation,
|
|
962
|
+
target = 'params',
|
|
963
|
+
bodyFieldTypes?: Map<string, TypeRef>,
|
|
964
|
+
): void {
|
|
965
|
+
for (const group of op.parameterGroups ?? []) {
|
|
966
|
+
const groupParam = fieldName(group.name);
|
|
967
|
+
if (group.optional) {
|
|
968
|
+
lines.push(` if ${groupParam} is not None:`);
|
|
969
|
+
}
|
|
970
|
+
const indent = group.optional ? ' ' : '';
|
|
971
|
+
let first = true;
|
|
972
|
+
for (const variant of group.variants) {
|
|
973
|
+
const variantClass = groupVariantClassName(group.name, variant.name);
|
|
974
|
+
const keyword = first ? 'if' : 'elif';
|
|
975
|
+
first = false;
|
|
976
|
+
lines.push(` ${indent}${keyword} isinstance(${groupParam}, ${variantClass}):`);
|
|
977
|
+
for (const param of variant.parameters) {
|
|
978
|
+
const pyField = fieldName(param.name);
|
|
979
|
+
const resolvedType = bodyFieldTypes?.get(param.name) ?? param.type;
|
|
980
|
+
const effectiveType = resolvedType.kind === 'nullable' ? resolvedType.inner : resolvedType;
|
|
981
|
+
const isEnum = effectiveType.kind === 'enum';
|
|
982
|
+
const value = isEnum ? `enum_value(${groupParam}.${pyField})` : `${groupParam}.${pyField}`;
|
|
983
|
+
lines.push(` ${indent}${target}["${param.name}"] = ${value}`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
794
989
|
// ─── Main generator ──────────────────────────────────────────────────
|
|
795
990
|
|
|
796
991
|
/**
|
|
@@ -859,7 +1054,9 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
859
1054
|
// Also collect types from body model fields (expanded as keyword params)
|
|
860
1055
|
const bodyModel = ctx.spec.models.find((m) => m.name === requestBodyRef.name);
|
|
861
1056
|
if (bodyModel) {
|
|
1057
|
+
const importGroupedParams = collectGroupedParamNames(op);
|
|
862
1058
|
for (const f of bodyModel.fields) {
|
|
1059
|
+
if (importGroupedParams.has(f.name)) continue;
|
|
863
1060
|
for (const ref of collectModelRefs(f.type)) modelImports.add(ref);
|
|
864
1061
|
for (const ref of collectEnumRefs(f.type)) enumImports.add(ref);
|
|
865
1062
|
}
|
|
@@ -890,6 +1087,11 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
890
1087
|
}
|
|
891
1088
|
}
|
|
892
1089
|
modelImports.add(paginationItemName);
|
|
1090
|
+
// For discriminator (dispatcher) models also import the variant union type alias
|
|
1091
|
+
const paginationItemModel = ctx.spec.models.find((m) => m.name === paginationItemName);
|
|
1092
|
+
if ((paginationItemModel as any)?.discriminator) {
|
|
1093
|
+
modelImports.add(paginationItemName + 'Variant');
|
|
1094
|
+
}
|
|
893
1095
|
}
|
|
894
1096
|
// Collect model imports for union split wrapper response types
|
|
895
1097
|
const resolved = lookupResolved(op, resolvedLookup);
|
|
@@ -915,6 +1117,14 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
915
1117
|
|
|
916
1118
|
// Split imports into same-service and cross-service (using mount-based dirs)
|
|
917
1119
|
const modelToServiceMap = assignModelsToServices(ctx.spec.models, ctx.spec.services);
|
|
1120
|
+
// Discriminator variant type aliases (e.g. EventSchemaVariant) live in the same
|
|
1121
|
+
// service as their dispatcher model, so ensure they resolve to the same directory.
|
|
1122
|
+
for (const model of ctx.spec.models) {
|
|
1123
|
+
if ((model as any).discriminator) {
|
|
1124
|
+
const svc = modelToServiceMap.get(model.name);
|
|
1125
|
+
if (svc) modelToServiceMap.set(model.name + 'Variant', svc);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
918
1128
|
const resolveModelDir = (modelName: string) => {
|
|
919
1129
|
const svc = modelToServiceMap.get(modelName);
|
|
920
1130
|
return svc ? (mountDirMap.get(svc) ?? 'common') : 'common';
|
|
@@ -997,6 +1207,13 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
997
1207
|
if (hasPaginated) {
|
|
998
1208
|
lines.push(`from ${importPrefix}_pagination import AsyncPage, SyncPage`);
|
|
999
1209
|
}
|
|
1210
|
+
// dataclass import + group variant definitions
|
|
1211
|
+
const hasGroups = hasParameterGroups(allOperations);
|
|
1212
|
+
if (hasGroups) {
|
|
1213
|
+
lines.push('from dataclasses import dataclass');
|
|
1214
|
+
const dataclassLines = generateParameterGroupDataclasses(allOperations, specEnumNames, ctx.spec.models);
|
|
1215
|
+
lines.push(...dataclassLines);
|
|
1216
|
+
}
|
|
1000
1217
|
// --- Generate sync class ---
|
|
1001
1218
|
lines.push('');
|
|
1002
1219
|
lines.push(`class ${resourceClassName}:`);
|