@workos/oagen-emitters 0.4.0 → 0.6.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 +15 -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-Dws9b6T7.mjs +21441 -0
- package/dist/plugin-Dws9b6T7.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 +17 -41
- package/smoke/sdk-dotnet.ts +11 -5
- package/smoke/sdk-elixir.ts +11 -5
- package/smoke/sdk-go.ts +10 -4
- package/smoke/sdk-kotlin.ts +11 -5
- package/smoke/sdk-node.ts +11 -5
- package/smoke/sdk-php.ts +9 -4
- package/smoke/sdk-python.ts +10 -4
- package/smoke/sdk-ruby.ts +10 -4
- package/smoke/sdk-rust.ts +11 -5
- package/src/dotnet/index.ts +9 -7
- package/src/dotnet/manifest.ts +5 -11
- 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 +13 -8
- package/src/go/manifest.ts +5 -11
- 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/index.ts +3 -3
- package/src/kotlin/manifest.ts +9 -15
- 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/index.ts +3 -3
- package/src/node/manifest.ts +4 -11
- 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/index.ts +3 -3
- package/src/php/manifest.ts +5 -11
- 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 +38 -30
- package/src/python/manifest.ts +5 -12
- 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 +28 -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/manifest.test.ts +13 -12
- 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/manifest.test.ts +7 -7
- 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/tsconfig.json +1 -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/dotnet/resources.ts
CHANGED
|
@@ -5,16 +5,20 @@ import type {
|
|
|
5
5
|
EmitterContext,
|
|
6
6
|
GeneratedFile,
|
|
7
7
|
ResolvedOperation,
|
|
8
|
+
Model,
|
|
9
|
+
TypeRef,
|
|
8
10
|
} from '@workos/oagen';
|
|
9
11
|
import { planOperation } from '@workos/oagen';
|
|
10
12
|
import { isListWrapperModel } from './models.js';
|
|
11
|
-
import { mapTypeRef, isValueTypeRef, isEnumRef, emitJsonPropertyAttributes } from './type-map.js';
|
|
13
|
+
import { mapTypeRef, isValueTypeRef, isEnumRef, emitJsonPropertyAttributes, resolveModelName } from './type-map.js';
|
|
12
14
|
import {
|
|
15
|
+
appendAsyncSuffix,
|
|
13
16
|
className,
|
|
14
17
|
fieldName,
|
|
15
18
|
methodName,
|
|
16
19
|
resolveClassName,
|
|
17
20
|
resolveMethodName,
|
|
21
|
+
resolveMethodStem,
|
|
18
22
|
serviceTypeName,
|
|
19
23
|
localName,
|
|
20
24
|
csLiteral,
|
|
@@ -26,6 +30,7 @@ import {
|
|
|
26
30
|
deprecationMessage,
|
|
27
31
|
escapeCsAttributeString,
|
|
28
32
|
humanize,
|
|
33
|
+
modelClassName,
|
|
29
34
|
} from './naming.js';
|
|
30
35
|
import {
|
|
31
36
|
buildResolvedLookup,
|
|
@@ -35,6 +40,8 @@ import {
|
|
|
35
40
|
getOpInferFromClient,
|
|
36
41
|
buildHiddenParams,
|
|
37
42
|
hasHiddenParams,
|
|
43
|
+
collectGroupedParamNames,
|
|
44
|
+
collectBodyFieldTypes,
|
|
38
45
|
} from '../shared/resolved-ops.js';
|
|
39
46
|
import { generateWrapperMethods } from './wrappers.js';
|
|
40
47
|
|
|
@@ -82,6 +89,193 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
|
|
|
82
89
|
return files;
|
|
83
90
|
}
|
|
84
91
|
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Mutually-exclusive parameter group support
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/** Abstract base class name for a parameter group (e.g. UserManagementRole). */
|
|
97
|
+
function groupBaseClassName(mountName: string, groupName: string): string {
|
|
98
|
+
return `${className(mountName)}${className(groupName)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Concrete variant class name (e.g. UserManagementRoleSingle). */
|
|
102
|
+
function groupVariantClassName(mountName: string, groupName: string, variantName: string): string {
|
|
103
|
+
return `${className(mountName)}${className(groupName)}${className(variantName)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate C# abstract base class + concrete subtypes for all parameter groups
|
|
108
|
+
* on an operation. Each group becomes an abstract class with concrete subclasses
|
|
109
|
+
* for each variant containing the variant's parameters as properties.
|
|
110
|
+
*/
|
|
111
|
+
function generateParameterGroupTypes(
|
|
112
|
+
mountName: string,
|
|
113
|
+
op: Operation,
|
|
114
|
+
models: Model[],
|
|
115
|
+
emitted?: Set<string>,
|
|
116
|
+
): string[] {
|
|
117
|
+
const lines: string[] = [];
|
|
118
|
+
const bodyFieldTypes = collectBodyFieldTypes(op, models);
|
|
119
|
+
|
|
120
|
+
for (const group of op.parameterGroups ?? []) {
|
|
121
|
+
const baseName = groupBaseClassName(mountName, group.name);
|
|
122
|
+
if (emitted?.has(baseName)) continue;
|
|
123
|
+
emitted?.add(baseName);
|
|
124
|
+
|
|
125
|
+
lines.push('');
|
|
126
|
+
lines.push(` public abstract class ${baseName} { }`);
|
|
127
|
+
|
|
128
|
+
for (const variant of group.variants) {
|
|
129
|
+
const variantName = groupVariantClassName(mountName, group.name, variant.name);
|
|
130
|
+
lines.push('');
|
|
131
|
+
lines.push(` public class ${variantName} : ${baseName}`);
|
|
132
|
+
lines.push(' {');
|
|
133
|
+
for (const param of variant.parameters) {
|
|
134
|
+
const csField = fieldName(param.name);
|
|
135
|
+
const effectiveType = bodyFieldTypes.get(param.name) ?? param.type;
|
|
136
|
+
const csType = mapTypeRef(effectiveType);
|
|
137
|
+
lines.push(` public ${csType} ${csField} { get; set; } = default!;`);
|
|
138
|
+
lines.push('');
|
|
139
|
+
}
|
|
140
|
+
lines.push(' }');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return lines;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Emit manual serialization for parameter group variants in the service
|
|
149
|
+
* method body. Each group field on the options class is pattern-matched via
|
|
150
|
+
* `is` checks and its variant parameters are added to the appropriate target
|
|
151
|
+
* (query string or request body).
|
|
152
|
+
*/
|
|
153
|
+
function emitGroupSerialization(
|
|
154
|
+
mountName: string,
|
|
155
|
+
op: Operation,
|
|
156
|
+
indent: string,
|
|
157
|
+
models: Model[],
|
|
158
|
+
target: 'query' | 'body',
|
|
159
|
+
): string[] {
|
|
160
|
+
const lines: string[] = [];
|
|
161
|
+
const bodyFieldTypes = collectBodyFieldTypes(op, models);
|
|
162
|
+
|
|
163
|
+
for (const group of op.parameterGroups ?? []) {
|
|
164
|
+
const groupField = fieldName(group.name);
|
|
165
|
+
let first = true;
|
|
166
|
+
|
|
167
|
+
for (const variant of group.variants) {
|
|
168
|
+
const variantName = groupVariantClassName(mountName, group.name, variant.name);
|
|
169
|
+
// Use a short local variable derived from the variant name
|
|
170
|
+
const localVar = localName(variant.name);
|
|
171
|
+
const keyword = first ? 'if' : 'else if';
|
|
172
|
+
first = false;
|
|
173
|
+
|
|
174
|
+
lines.push(`${indent}${keyword} (options?.${groupField} is ${variantName} ${localVar})`);
|
|
175
|
+
lines.push(`${indent}{`);
|
|
176
|
+
let prevWasBlock = false;
|
|
177
|
+
for (const param of variant.parameters) {
|
|
178
|
+
const csField = fieldName(param.name);
|
|
179
|
+
const effectiveType = bodyFieldTypes.get(param.name) ?? param.type;
|
|
180
|
+
const accessor = `${localVar}.${csField}`;
|
|
181
|
+
const paramLines = emitParamValue(param.name, accessor, effectiveType, indent + ' ', target);
|
|
182
|
+
// SA1513: closing brace must be followed by a blank line before the next statement
|
|
183
|
+
if (prevWasBlock) lines.push('');
|
|
184
|
+
lines.push(...paramLines);
|
|
185
|
+
prevWasBlock = paramLines.length > 1;
|
|
186
|
+
}
|
|
187
|
+
lines.push(`${indent}}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return lines;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Emit one or more lines to add a param to the query string or request body,
|
|
196
|
+
* adapting to the parameter's IR type: enums use JsonConvert for wire-value
|
|
197
|
+
* serialization, arrays are comma-joined, and reference types (string, List)
|
|
198
|
+
* get a null guard.
|
|
199
|
+
*
|
|
200
|
+
* Reference types always get a null guard because variant classes are shared
|
|
201
|
+
* across operations whose body models may disagree on nullability.
|
|
202
|
+
*/
|
|
203
|
+
function emitParamValue(
|
|
204
|
+
wireName: string,
|
|
205
|
+
accessor: string,
|
|
206
|
+
typeRef: TypeRef,
|
|
207
|
+
indent: string,
|
|
208
|
+
target: 'query' | 'body',
|
|
209
|
+
): string[] {
|
|
210
|
+
const method = target === 'body' ? 'AddBodyParam' : 'AddQueryParam';
|
|
211
|
+
const isNullable = typeRef.kind === 'nullable';
|
|
212
|
+
const inner: TypeRef = isNullable ? (typeRef as { kind: 'nullable'; inner: TypeRef }).inner : typeRef;
|
|
213
|
+
|
|
214
|
+
// Reference types (arrays, strings, models) are always guarded for null
|
|
215
|
+
// because the variant class property may be nullable even when the current
|
|
216
|
+
// operation's body model says "required".
|
|
217
|
+
const needsNullGuard = isNullable || !isValueTypeRef(inner);
|
|
218
|
+
|
|
219
|
+
if (inner.kind === 'array') {
|
|
220
|
+
// Body params pass the list directly so it serializes as a JSON array;
|
|
221
|
+
// query params comma-join into a single string value.
|
|
222
|
+
const valueExpr = target === 'body' ? accessor : `string.Join(",", ${accessor})`;
|
|
223
|
+
if (needsNullGuard) {
|
|
224
|
+
return [
|
|
225
|
+
`${indent}if (${accessor} != null)`,
|
|
226
|
+
`${indent}{`,
|
|
227
|
+
`${indent} request.${method}("${wireName}", ${valueExpr});`,
|
|
228
|
+
`${indent}}`,
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
return [`${indent}request.${method}("${wireName}", ${valueExpr});`];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (inner.kind === 'enum') {
|
|
235
|
+
const serExpr = `JsonConvert.SerializeObject(${accessor}).Trim('"')`;
|
|
236
|
+
if (isNullable) {
|
|
237
|
+
return [
|
|
238
|
+
`${indent}if (${accessor} != null)`,
|
|
239
|
+
`${indent}{`,
|
|
240
|
+
`${indent} request.${method}("${wireName}", ${serExpr});`,
|
|
241
|
+
`${indent}}`,
|
|
242
|
+
];
|
|
243
|
+
}
|
|
244
|
+
return [`${indent}request.${method}("${wireName}", ${serExpr});`];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (needsNullGuard) {
|
|
248
|
+
return [
|
|
249
|
+
`${indent}if (${accessor} != null)`,
|
|
250
|
+
`${indent}{`,
|
|
251
|
+
`${indent} request.${method}("${wireName}", ${accessor});`,
|
|
252
|
+
`${indent}}`,
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return [`${indent}request.${method}("${wireName}", ${accessor});`];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Check whether any parameter group variant contains an enum-typed parameter. */
|
|
260
|
+
function groupsNeedJsonConvert(operations: Operation[], models: Model[]): boolean {
|
|
261
|
+
for (const op of operations) {
|
|
262
|
+
const bodyFieldTypes = collectBodyFieldTypes(op, models);
|
|
263
|
+
for (const group of op.parameterGroups ?? []) {
|
|
264
|
+
for (const variant of group.variants) {
|
|
265
|
+
for (const param of variant.parameters) {
|
|
266
|
+
const effectiveType = bodyFieldTypes.get(param.name) ?? param.type;
|
|
267
|
+
const inner: TypeRef =
|
|
268
|
+
effectiveType.kind === 'nullable'
|
|
269
|
+
? (effectiveType as { kind: 'nullable'; inner: TypeRef }).inner
|
|
270
|
+
: effectiveType;
|
|
271
|
+
if (inner.kind === 'enum') return true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
85
279
|
function generateServiceFile(mountName: string, operations: Operation[], ctx: EmitterContext): GeneratedFile | null {
|
|
86
280
|
const lines: string[] = [];
|
|
87
281
|
const svcTypeName = serviceTypeName(mountName);
|
|
@@ -91,10 +285,14 @@ function generateServiceFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
91
285
|
|
|
92
286
|
lines.push(`namespace ${ctx.namespacePascal}`);
|
|
93
287
|
lines.push('{');
|
|
288
|
+
lines.push(' using System;');
|
|
94
289
|
lines.push(' using System.Collections.Generic;');
|
|
95
290
|
lines.push(' using System.Net.Http;');
|
|
96
291
|
lines.push(' using System.Threading;');
|
|
97
292
|
lines.push(' using System.Threading.Tasks;');
|
|
293
|
+
if (groupsNeedJsonConvert(operations, ctx.spec.models)) {
|
|
294
|
+
lines.push(' using Newtonsoft.Json;');
|
|
295
|
+
}
|
|
98
296
|
lines.push('');
|
|
99
297
|
lines.push(
|
|
100
298
|
` /// <summary>Service that exposes the ${humanize(mountName)} API operations on <see cref="WorkOSClient"/>.</summary>`,
|
|
@@ -119,6 +317,7 @@ function generateServiceFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
119
317
|
const emittedMethods = new Set<string>();
|
|
120
318
|
for (const op of operations) {
|
|
121
319
|
const plan = planOperation(op);
|
|
320
|
+
const methodStem = resolveCsMethodStem(op, mountName, ctx);
|
|
122
321
|
const method = resolveCsMethodName(op, mountName, ctx);
|
|
123
322
|
|
|
124
323
|
if (emittedMethods.has(method)) continue;
|
|
@@ -132,13 +331,18 @@ function generateServiceFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
132
331
|
// get a 422 from the API. Only emit the typed AuthenticateWith* wrappers.
|
|
133
332
|
if (!isUnionSplit) {
|
|
134
333
|
lines.push('');
|
|
135
|
-
const methodCode = generateMethod(svcTypeName, mountName, method, op, plan, ctx, resolvedOp);
|
|
334
|
+
const methodCode = generateMethod(svcTypeName, mountName, method, methodStem, op, plan, ctx, resolvedOp);
|
|
136
335
|
lines.push(methodCode);
|
|
137
336
|
|
|
337
|
+
if (!(resolvedOp?.urlBuilder ?? false) && method !== methodStem) {
|
|
338
|
+
lines.push('');
|
|
339
|
+
lines.push(generateCompatibilityMethod(mountName, method, methodStem, op, plan, ctx, resolvedOp));
|
|
340
|
+
}
|
|
341
|
+
|
|
138
342
|
// Generate auto-pagination method for paginated list operations
|
|
139
343
|
if (plan.isPaginated && op.pagination) {
|
|
140
344
|
lines.push('');
|
|
141
|
-
const autoPagingCode = generateAutoPagingMethod(mountName, method, op, plan, ctx, resolvedOp);
|
|
345
|
+
const autoPagingCode = generateAutoPagingMethod(mountName, method, methodStem, op, plan, ctx, resolvedOp);
|
|
142
346
|
lines.push(autoPagingCode);
|
|
143
347
|
}
|
|
144
348
|
}
|
|
@@ -148,7 +352,7 @@ function generateServiceFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
148
352
|
const wrapperLines = generateWrapperMethods(svcTypeName, resolvedOp!, ctx);
|
|
149
353
|
lines.push(...wrapperLines);
|
|
150
354
|
for (const w of resolvedOp!.wrappers!) {
|
|
151
|
-
emittedMethods.add(methodName(w.name));
|
|
355
|
+
emittedMethods.add(appendAsyncSuffix(methodName(w.name)));
|
|
152
356
|
}
|
|
153
357
|
}
|
|
154
358
|
}
|
|
@@ -176,6 +380,7 @@ function generateOptionsFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
176
380
|
optionsLines.push(' using STJS = System.Text.Json.Serialization;');
|
|
177
381
|
|
|
178
382
|
const emittedOptions = new Set<string>();
|
|
383
|
+
const emittedGroupTypes = new Set<string>();
|
|
179
384
|
for (const op of operations) {
|
|
180
385
|
const plan = planOperation(op);
|
|
181
386
|
const method = resolveCsMethodName(op, mountName, ctx);
|
|
@@ -190,7 +395,10 @@ function generateOptionsFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
190
395
|
const optionsClass = optionsClassName(mountName, method);
|
|
191
396
|
if (emittedOptions.has(optionsClass)) continue;
|
|
192
397
|
|
|
193
|
-
const
|
|
398
|
+
const groupedParams = collectGroupedParamNames(op);
|
|
399
|
+
const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
|
|
400
|
+
const hasVisibleQueryParams =
|
|
401
|
+
op.queryParams.filter((qp) => !hidden.has(qp.name) && !groupedParams.has(qp.name)).length > 0;
|
|
194
402
|
const hasBody = plan.hasBody && op.requestBody;
|
|
195
403
|
let hasVisibleBodyFields = false;
|
|
196
404
|
if (hasBody && op.requestBody?.kind === 'model') {
|
|
@@ -200,7 +408,7 @@ function generateOptionsFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
200
408
|
hasVisibleBodyFields = true;
|
|
201
409
|
}
|
|
202
410
|
|
|
203
|
-
if (!hasVisibleQueryParams && !hasVisibleBodyFields) continue;
|
|
411
|
+
if (!hasVisibleQueryParams && !hasVisibleBodyFields && !hasGroups) continue;
|
|
204
412
|
|
|
205
413
|
emittedOptions.add(optionsClass);
|
|
206
414
|
hasOptions = true;
|
|
@@ -225,6 +433,7 @@ function generateOptionsFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
225
433
|
if (bodyModel) {
|
|
226
434
|
for (const field of bodyModel.fields) {
|
|
227
435
|
if (hidden.has(field.name)) continue;
|
|
436
|
+
if (groupedParams.has(field.name)) continue;
|
|
228
437
|
const csField = fieldName(field.name);
|
|
229
438
|
if (emittedFields.has(csField)) continue;
|
|
230
439
|
emittedFields.add(csField);
|
|
@@ -263,10 +472,12 @@ function generateOptionsFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
263
472
|
}
|
|
264
473
|
}
|
|
265
474
|
|
|
266
|
-
// Query params (skip pagination fields for list options — they're in ListOptions base
|
|
475
|
+
// Query params (skip pagination fields for list options — they're in ListOptions base,
|
|
476
|
+
// and skip grouped params which get their own abstract class hierarchy)
|
|
267
477
|
const PAGINATION_FIELDS = new Set(['before', 'after', 'limit', 'order']);
|
|
268
478
|
for (const param of op.queryParams) {
|
|
269
479
|
if (hidden.has(param.name)) continue;
|
|
480
|
+
if (groupedParams.has(param.name)) continue;
|
|
270
481
|
if (isPaginated && PAGINATION_FIELDS.has(param.name)) continue;
|
|
271
482
|
const csField = fieldName(param.name);
|
|
272
483
|
if (emittedFields.has(csField)) continue;
|
|
@@ -311,8 +522,6 @@ function generateOptionsFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
311
522
|
const csField = fieldName(key);
|
|
312
523
|
if (emittedFields.has(csField)) continue;
|
|
313
524
|
emittedFields.add(csField);
|
|
314
|
-
optionsLines.push(` [JsonProperty("${key}")]`);
|
|
315
|
-
optionsLines.push(` [STJS.JsonPropertyName("${key}")]`);
|
|
316
525
|
optionsLines.push(` internal string ${csField} { get; set; } = default!;`);
|
|
317
526
|
optionsLines.push('');
|
|
318
527
|
}
|
|
@@ -320,13 +529,28 @@ function generateOptionsFile(mountName: string, operations: Operation[], ctx: Em
|
|
|
320
529
|
const csField = fieldName(key);
|
|
321
530
|
if (emittedFields.has(csField)) continue;
|
|
322
531
|
emittedFields.add(csField);
|
|
323
|
-
optionsLines.push(` [JsonProperty("${key}")]`);
|
|
324
|
-
optionsLines.push(` [STJS.JsonPropertyName("${key}")]`);
|
|
325
532
|
optionsLines.push(` internal string ${csField} { get; set; } = default!;`);
|
|
326
533
|
optionsLines.push('');
|
|
327
534
|
}
|
|
328
535
|
|
|
536
|
+
// Parameter group properties (serialized manually in the service method, not by JSON)
|
|
537
|
+
for (const group of op.parameterGroups ?? []) {
|
|
538
|
+
const baseName = groupBaseClassName(mountName, group.name);
|
|
539
|
+
const csField = fieldName(group.name);
|
|
540
|
+
optionsLines.push(' [JsonIgnore]');
|
|
541
|
+
optionsLines.push(' [STJS.JsonIgnore]');
|
|
542
|
+
const initializer = group.optional ? '' : ' = default!;';
|
|
543
|
+
const csType = group.optional ? `${baseName}?` : baseName;
|
|
544
|
+
optionsLines.push(` public ${csType} ${csField} { get; set; }${initializer}`);
|
|
545
|
+
optionsLines.push('');
|
|
546
|
+
}
|
|
547
|
+
|
|
329
548
|
optionsLines.push(' }');
|
|
549
|
+
|
|
550
|
+
// Emit parameter group abstract base + concrete variant classes
|
|
551
|
+
if (hasGroups) {
|
|
552
|
+
optionsLines.push(...generateParameterGroupTypes(mountName, op, ctx.spec.models, emittedGroupTypes));
|
|
553
|
+
}
|
|
330
554
|
}
|
|
331
555
|
|
|
332
556
|
optionsLines.push('}');
|
|
@@ -344,6 +568,7 @@ function generateMethod(
|
|
|
344
568
|
_serviceType: string,
|
|
345
569
|
mountName: string,
|
|
346
570
|
method: string,
|
|
571
|
+
methodStem: string,
|
|
347
572
|
op: Operation,
|
|
348
573
|
plan: OperationPlan,
|
|
349
574
|
ctx: EmitterContext,
|
|
@@ -354,7 +579,10 @@ function generateMethod(
|
|
|
354
579
|
const isDelete = plan.isDelete;
|
|
355
580
|
const hasBody = plan.hasBody && op.requestBody;
|
|
356
581
|
const hidden = buildHiddenParams(resolvedOp);
|
|
357
|
-
const
|
|
582
|
+
const groupedParams = collectGroupedParamNames(op);
|
|
583
|
+
const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
|
|
584
|
+
const hasVisibleQueryParams =
|
|
585
|
+
op.queryParams.filter((qp) => !hidden.has(qp.name) && !groupedParams.has(qp.name)).length > 0;
|
|
358
586
|
|
|
359
587
|
let hasVisibleBodyFields = false;
|
|
360
588
|
if (hasBody && op.requestBody?.kind === 'model') {
|
|
@@ -364,8 +592,8 @@ function generateMethod(
|
|
|
364
592
|
hasVisibleBodyFields = true;
|
|
365
593
|
}
|
|
366
594
|
|
|
367
|
-
const hasParams = hasVisibleBodyFields || hasVisibleQueryParams;
|
|
368
|
-
const optionsClass = hasParams ? optionsClassName(mountName,
|
|
595
|
+
const hasParams = hasVisibleBodyFields || hasVisibleQueryParams || hasGroups;
|
|
596
|
+
const optionsClass = hasParams ? optionsClassName(mountName, methodStem) : null;
|
|
369
597
|
const hasHidden = hasHiddenParams(resolvedOp);
|
|
370
598
|
|
|
371
599
|
// Per-operation Bearer token auth (e.g., SSO GetProfile uses access_token instead of API key)
|
|
@@ -388,7 +616,7 @@ function generateMethod(
|
|
|
388
616
|
} else if (isDelete) {
|
|
389
617
|
returnType = 'Task';
|
|
390
618
|
} else if (plan.responseModelName) {
|
|
391
|
-
const respType =
|
|
619
|
+
const respType = modelClassName(resolveModelName(plan.responseModelName));
|
|
392
620
|
if (!isPaginated && op.response?.kind === 'array') {
|
|
393
621
|
returnType = `Task<List<${respType}>>`;
|
|
394
622
|
} else {
|
|
@@ -420,7 +648,7 @@ function generateMethod(
|
|
|
420
648
|
const itemType = resolveListItemType(op.pagination.itemType, ctx);
|
|
421
649
|
lines.push(` /// <returns>A page of <see cref="${itemType}"/> results.</returns>`);
|
|
422
650
|
} else if (plan.responseModelName) {
|
|
423
|
-
const respType =
|
|
651
|
+
const respType = modelClassName(resolveModelName(plan.responseModelName));
|
|
424
652
|
lines.push(` /// <returns>The <see cref="${respType}"/> result.</returns>`);
|
|
425
653
|
}
|
|
426
654
|
if (op.deprecated) {
|
|
@@ -474,10 +702,11 @@ function generateMethod(
|
|
|
474
702
|
// Build path
|
|
475
703
|
const pathExpr = buildPathExpr(op);
|
|
476
704
|
|
|
477
|
-
// URL-builders
|
|
478
|
-
// form because the Service helpers
|
|
479
|
-
//
|
|
480
|
-
|
|
705
|
+
// URL-builders, bearer-override operations, and operations with parameter
|
|
706
|
+
// groups keep the inlined WorkOSRequest form because the Service helpers
|
|
707
|
+
// don't expose BuildRequestUri, AccessToken configuration, or manual
|
|
708
|
+
// query param injection. Everything else uses the helper one-liners.
|
|
709
|
+
const needsInlineRequest = isUrlBuilder || (hasBearerOverride && !!bearerParamName) || hasGroups;
|
|
481
710
|
const optionsArg = optionsClass ? 'options' : 'null';
|
|
482
711
|
|
|
483
712
|
if (needsInlineRequest) {
|
|
@@ -496,6 +725,17 @@ function generateMethod(
|
|
|
496
725
|
}
|
|
497
726
|
lines.push(' };');
|
|
498
727
|
|
|
728
|
+
// Serialize parameter group variants into query params (GET/DELETE)
|
|
729
|
+
// or body params (POST/PUT/PATCH) so sensitive fields like passwords
|
|
730
|
+
// never leak into the URL. DELETE is routed to query because the
|
|
731
|
+
// dotnet HTTP client only sends body content for non-GET/DELETE methods.
|
|
732
|
+
if (hasGroups) {
|
|
733
|
+
const groupTarget = hasBody && !isDelete ? 'body' : 'query';
|
|
734
|
+
lines.push('');
|
|
735
|
+
lines.push(...emitGroupSerialization(mountName, op, ' ', ctx.spec.models, groupTarget));
|
|
736
|
+
lines.push('');
|
|
737
|
+
}
|
|
738
|
+
|
|
499
739
|
if (isUrlBuilder) {
|
|
500
740
|
lines.push(' return this.Client.BuildRequestUri(request).ToString();');
|
|
501
741
|
} else if (returnType.startsWith('Task<')) {
|
|
@@ -526,6 +766,7 @@ function generateMethod(
|
|
|
526
766
|
function generateAutoPagingMethod(
|
|
527
767
|
mountName: string,
|
|
528
768
|
method: string,
|
|
769
|
+
methodStem: string,
|
|
529
770
|
op: Operation,
|
|
530
771
|
plan: OperationPlan,
|
|
531
772
|
ctx: EmitterContext,
|
|
@@ -533,7 +774,10 @@ function generateAutoPagingMethod(
|
|
|
533
774
|
): string {
|
|
534
775
|
const lines: string[] = [];
|
|
535
776
|
const hidden = buildHiddenParams(resolvedOp);
|
|
536
|
-
const
|
|
777
|
+
const groupedParams = collectGroupedParamNames(op);
|
|
778
|
+
const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
|
|
779
|
+
const hasVisibleQueryParams =
|
|
780
|
+
op.queryParams.filter((qp) => !hidden.has(qp.name) && !groupedParams.has(qp.name)).length > 0;
|
|
537
781
|
|
|
538
782
|
let hasVisibleBodyFields = false;
|
|
539
783
|
if (plan.hasBody && op.requestBody?.kind === 'model') {
|
|
@@ -541,8 +785,8 @@ function generateAutoPagingMethod(
|
|
|
541
785
|
if (bodyModel) hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
542
786
|
}
|
|
543
787
|
|
|
544
|
-
const hasParams = hasVisibleBodyFields || hasVisibleQueryParams;
|
|
545
|
-
const optionsClass = hasParams ? optionsClassName(mountName,
|
|
788
|
+
const hasParams = hasVisibleBodyFields || hasVisibleQueryParams || hasGroups;
|
|
789
|
+
const optionsClass = hasParams ? optionsClassName(mountName, methodStem) : null;
|
|
546
790
|
|
|
547
791
|
const itemType = resolveListItemType(op.pagination!.itemType, ctx);
|
|
548
792
|
|
|
@@ -572,7 +816,7 @@ function generateAutoPagingMethod(
|
|
|
572
816
|
params.push('RequestOptions? requestOptions = null');
|
|
573
817
|
params.push('CancellationToken cancellationToken = default');
|
|
574
818
|
|
|
575
|
-
lines.push(` public virtual IAsyncEnumerable<${itemType}> ${
|
|
819
|
+
lines.push(` public virtual IAsyncEnumerable<${itemType}> ${methodStem}AutoPagingAsync(${params.join(', ')})`);
|
|
576
820
|
lines.push(' {');
|
|
577
821
|
|
|
578
822
|
const pathExpr = buildPathExpr(op);
|
|
@@ -589,10 +833,15 @@ function resolveCsMethodName(op: Operation, mountName: string, ctx: EmitterConte
|
|
|
589
833
|
return resolveMethodName(op, { name: mountName, operations: [op] }, ctx);
|
|
590
834
|
}
|
|
591
835
|
|
|
836
|
+
export function resolveCsMethodStem(op: Operation, mountName: string, ctx: EmitterContext): string {
|
|
837
|
+
return resolveMethodStem(op, { name: mountName, operations: [op] }, ctx);
|
|
838
|
+
}
|
|
839
|
+
|
|
592
840
|
export function optionsClassName(mountName: string, method: string): string {
|
|
841
|
+
const methodStem = method.endsWith('Async') ? method.slice(0, -5) : method;
|
|
593
842
|
const prefix = className(mountName);
|
|
594
|
-
if (
|
|
595
|
-
return `${prefix}${
|
|
843
|
+
if (methodStem.startsWith(prefix)) return `${methodStem}Options`;
|
|
844
|
+
return `${prefix}${methodStem}Options`;
|
|
596
845
|
}
|
|
597
846
|
|
|
598
847
|
function buildPathExpr(op: Operation): string {
|
|
@@ -602,7 +851,7 @@ function buildPathExpr(op: Operation): string {
|
|
|
602
851
|
// Build C# string interpolation
|
|
603
852
|
let interpolated = op.path;
|
|
604
853
|
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
605
|
-
interpolated = interpolated.replace(`{${p.name}}`, `{${localName(p.name)}}`);
|
|
854
|
+
interpolated = interpolated.replace(`{${p.name}}`, `{Uri.EscapeDataString(${localName(p.name)})}`);
|
|
606
855
|
}
|
|
607
856
|
return `$"${interpolated}"`;
|
|
608
857
|
}
|
|
@@ -613,10 +862,82 @@ function resolveListItemType(itemType: import('@workos/oagen').TypeRef, ctx: Emi
|
|
|
613
862
|
if (model && isListWrapperModel(model)) {
|
|
614
863
|
const dataField = model.fields.find((f) => f.name === 'data');
|
|
615
864
|
if (dataField && dataField.type.kind === 'array' && dataField.type.items.kind === 'model') {
|
|
616
|
-
return
|
|
865
|
+
return modelClassName(resolveModelName(dataField.type.items.name));
|
|
617
866
|
}
|
|
618
867
|
}
|
|
619
|
-
return
|
|
868
|
+
return modelClassName(resolveModelName(itemType.name));
|
|
620
869
|
}
|
|
621
870
|
return mapTypeRef(itemType);
|
|
622
871
|
}
|
|
872
|
+
|
|
873
|
+
function generateCompatibilityMethod(
|
|
874
|
+
mountName: string,
|
|
875
|
+
asyncMethod: string,
|
|
876
|
+
methodStem: string,
|
|
877
|
+
op: Operation,
|
|
878
|
+
plan: OperationPlan,
|
|
879
|
+
ctx: EmitterContext,
|
|
880
|
+
resolvedOp?: ResolvedOperation,
|
|
881
|
+
): string {
|
|
882
|
+
const lines: string[] = [];
|
|
883
|
+
const hidden = buildHiddenParams(resolvedOp);
|
|
884
|
+
const groupedParams = collectGroupedParamNames(op);
|
|
885
|
+
const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
|
|
886
|
+
const hasVisibleQueryParams =
|
|
887
|
+
op.queryParams.filter((qp) => !hidden.has(qp.name) && !groupedParams.has(qp.name)).length > 0;
|
|
888
|
+
|
|
889
|
+
let hasVisibleBodyFields = false;
|
|
890
|
+
if (plan.hasBody && op.requestBody?.kind === 'model') {
|
|
891
|
+
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
892
|
+
if (bodyModel) hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
893
|
+
} else if (plan.hasBody && op.requestBody) {
|
|
894
|
+
hasVisibleBodyFields = true;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const hasParams = hasVisibleBodyFields || hasVisibleQueryParams || hasGroups;
|
|
898
|
+
const optionsClass = hasParams ? optionsClassName(mountName, methodStem) : null;
|
|
899
|
+
|
|
900
|
+
let returnType = 'Task';
|
|
901
|
+
if (plan.isPaginated && op.pagination) {
|
|
902
|
+
const itemType = resolveListItemType(op.pagination.itemType, ctx);
|
|
903
|
+
returnType = `Task<WorkOSList<${itemType}>>`;
|
|
904
|
+
} else if (plan.responseModelName) {
|
|
905
|
+
const respType = modelClassName(resolveModelName(plan.responseModelName));
|
|
906
|
+
returnType = !plan.isPaginated && op.response?.kind === 'array' ? `Task<List<${respType}>>` : `Task<${respType}>`;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const params: string[] = [];
|
|
910
|
+
const args: string[] = [];
|
|
911
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
912
|
+
const name = localName(p.name);
|
|
913
|
+
params.push(`string ${name}`);
|
|
914
|
+
args.push(name);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const hasBearerOverride = op.security?.some((s: any) => s.schemeName !== 'bearerAuth') ?? false;
|
|
918
|
+
if (hasBearerOverride) {
|
|
919
|
+
const bearerParamName = op.security!.find((s: any) => s.schemeName !== 'bearerAuth')!.schemeName;
|
|
920
|
+
const bearerLocal = localName(bearerParamName);
|
|
921
|
+
params.push(`string ${bearerLocal}`);
|
|
922
|
+
args.push(bearerLocal);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (optionsClass) {
|
|
926
|
+
const isRequired = hasVisibleBodyFields && !plan.isPaginated;
|
|
927
|
+
params.push(isRequired ? `${optionsClass} options` : `${optionsClass}? options = null`);
|
|
928
|
+
args.push('options');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
params.push('RequestOptions? requestOptions = null');
|
|
932
|
+
params.push('CancellationToken cancellationToken = default');
|
|
933
|
+
args.push('requestOptions');
|
|
934
|
+
args.push('cancellationToken');
|
|
935
|
+
|
|
936
|
+
lines.push(` /// <summary>Compatibility wrapper for <see cref="${asyncMethod}"/>.</summary>`);
|
|
937
|
+
lines.push(` public virtual ${returnType} ${methodStem}(${params.join(', ')})`);
|
|
938
|
+
lines.push(' {');
|
|
939
|
+
lines.push(` return this.${asyncMethod}(${args.join(', ')});`);
|
|
940
|
+
lines.push(' }');
|
|
941
|
+
|
|
942
|
+
return lines.join('\n');
|
|
943
|
+
}
|