@workos/oagen-emitters 0.3.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 +15 -0
- package/README.md +35 -224
- package/dist/index.d.mts +12 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -12737
- 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 +336 -0
- package/oagen.config.ts +5 -343
- package/package.json +10 -34
- package/smoke/sdk-dotnet.ts +45 -12
- package/src/dotnet/client.ts +89 -0
- package/src/dotnet/enums.ts +323 -0
- package/src/dotnet/fixtures.ts +236 -0
- package/src/dotnet/index.ts +248 -0
- package/src/dotnet/manifest.ts +36 -0
- package/src/dotnet/models.ts +320 -0
- package/src/dotnet/naming.ts +368 -0
- package/src/dotnet/resources.ts +943 -0
- package/src/dotnet/tests.ts +713 -0
- package/src/dotnet/type-map.ts +228 -0
- package/src/dotnet/wrappers.ts +197 -0
- package/src/go/client.ts +35 -3
- package/src/go/enums.ts +4 -0
- package/src/go/index.ts +15 -7
- package/src/go/models.ts +6 -1
- package/src/go/naming.ts +5 -17
- 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 +15 -0
- package/src/kotlin/client.ts +58 -0
- package/src/kotlin/enums.ts +189 -0
- package/src/kotlin/index.ts +92 -0
- package/src/kotlin/manifest.ts +55 -0
- package/src/kotlin/models.ts +486 -0
- package/src/kotlin/naming.ts +229 -0
- package/src/kotlin/overrides.ts +25 -0
- package/src/kotlin/resources.ts +998 -0
- package/src/kotlin/tests.ts +1133 -0
- package/src/kotlin/type-map.ts +123 -0
- package/src/kotlin/wrappers.ts +168 -0
- package/src/node/client.ts +84 -7
- package/src/node/field-plan.ts +12 -14
- package/src/node/fixtures.ts +39 -3
- package/src/node/index.ts +1 -0
- package/src/node/models.ts +281 -37
- package/src/node/resources.ts +319 -95
- package/src/node/tests.ts +108 -29
- 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/client.ts +11 -3
- package/src/php/models.ts +0 -33
- package/src/php/naming.ts +2 -21
- package/src/php/resources.ts +275 -19
- package/src/php/tests.ts +118 -18
- package/src/php/type-map.ts +16 -2
- package/src/php/wrappers.ts +7 -2
- package/src/plugin.ts +50 -0
- package/src/python/client.ts +50 -32
- package/src/python/enums.ts +35 -10
- package/src/python/index.ts +35 -27
- package/src/python/models.ts +139 -2
- package/src/python/naming.ts +2 -22
- 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 +357 -16
- package/src/shared/naming-utils.ts +83 -0
- package/src/shared/non-spec-services.ts +13 -0
- package/src/shared/resolved-ops.ts +75 -1
- package/src/shared/wrapper-utils.ts +12 -1
- package/test/dotnet/client.test.ts +121 -0
- package/test/dotnet/enums.test.ts +193 -0
- package/test/dotnet/errors.test.ts +9 -0
- package/test/dotnet/manifest.test.ts +82 -0
- package/test/dotnet/models.test.ts +258 -0
- package/test/dotnet/resources.test.ts +387 -0
- package/test/dotnet/tests.test.ts +202 -0
- 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 +135 -0
- package/test/kotlin/resources.test.ts +210 -0
- package/test/kotlin/tests.test.ts +176 -0
- package/test/node/client.test.ts +74 -0
- package/test/node/models.test.ts +134 -1
- package/test/node/resources.test.ts +343 -34
- package/test/node/utils.test.ts +140 -0
- package/test/php/client.test.ts +2 -1
- package/test/php/models.test.ts +5 -4
- package/test/php/resources.test.ts +103 -0
- package/test/php/tests.test.ts +67 -0
- 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
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Service,
|
|
3
|
+
Operation,
|
|
4
|
+
OperationPlan,
|
|
5
|
+
EmitterContext,
|
|
6
|
+
GeneratedFile,
|
|
7
|
+
ResolvedOperation,
|
|
8
|
+
Model,
|
|
9
|
+
TypeRef,
|
|
10
|
+
} from '@workos/oagen';
|
|
11
|
+
import { planOperation } from '@workos/oagen';
|
|
12
|
+
import { isListWrapperModel } from './models.js';
|
|
13
|
+
import { mapTypeRef, isValueTypeRef, isEnumRef, emitJsonPropertyAttributes, resolveModelName } from './type-map.js';
|
|
14
|
+
import {
|
|
15
|
+
appendAsyncSuffix,
|
|
16
|
+
className,
|
|
17
|
+
fieldName,
|
|
18
|
+
methodName,
|
|
19
|
+
resolveClassName,
|
|
20
|
+
resolveMethodName,
|
|
21
|
+
resolveMethodStem,
|
|
22
|
+
serviceTypeName,
|
|
23
|
+
localName,
|
|
24
|
+
csLiteral,
|
|
25
|
+
clientFieldExpression,
|
|
26
|
+
httpMethodCs,
|
|
27
|
+
httpMethodHelperName,
|
|
28
|
+
escapeXml,
|
|
29
|
+
emitXmlDoc,
|
|
30
|
+
deprecationMessage,
|
|
31
|
+
escapeCsAttributeString,
|
|
32
|
+
humanize,
|
|
33
|
+
modelClassName,
|
|
34
|
+
} from './naming.js';
|
|
35
|
+
import {
|
|
36
|
+
buildResolvedLookup,
|
|
37
|
+
lookupResolved,
|
|
38
|
+
groupByMount,
|
|
39
|
+
getOpDefaults,
|
|
40
|
+
getOpInferFromClient,
|
|
41
|
+
buildHiddenParams,
|
|
42
|
+
hasHiddenParams,
|
|
43
|
+
collectGroupedParamNames,
|
|
44
|
+
collectBodyFieldTypes,
|
|
45
|
+
} from '../shared/resolved-ops.js';
|
|
46
|
+
import { generateWrapperMethods } from './wrappers.js';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Return path params sorted by their first occurrence in the URL template.
|
|
50
|
+
*/
|
|
51
|
+
export function sortPathParamsByTemplateOrder(op: Operation): typeof op.pathParams {
|
|
52
|
+
return [...op.pathParams].sort((a, b) => {
|
|
53
|
+
const posA = op.path.indexOf(`{${a.name}}`);
|
|
54
|
+
const posB = op.path.indexOf(`{${b.name}}`);
|
|
55
|
+
return posA - posB;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the resource class name for a service.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveResourceClassName(service: Service, ctx: EmitterContext): string {
|
|
63
|
+
return resolveClassName(service, ctx);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate C# service files from IR Service definitions.
|
|
68
|
+
* Each mount group becomes a single Service.cs file.
|
|
69
|
+
*/
|
|
70
|
+
export function generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
71
|
+
if (services.length === 0) return [];
|
|
72
|
+
|
|
73
|
+
const files: GeneratedFile[] = [];
|
|
74
|
+
const mountGroups = groupByMount(ctx);
|
|
75
|
+
|
|
76
|
+
const entries: Array<{ name: string; operations: Operation[] }> =
|
|
77
|
+
mountGroups.size > 0
|
|
78
|
+
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
79
|
+
: services.map((s) => ({ name: resolveResourceClassName(s, ctx), operations: s.operations }));
|
|
80
|
+
|
|
81
|
+
for (const { name: mountName, operations } of entries) {
|
|
82
|
+
if (operations.length === 0) continue;
|
|
83
|
+
const serviceFile = generateServiceFile(mountName, operations, ctx);
|
|
84
|
+
if (serviceFile) files.push(serviceFile);
|
|
85
|
+
const optionsFile = generateOptionsFile(mountName, operations, ctx);
|
|
86
|
+
if (optionsFile) files.push(optionsFile);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return files;
|
|
90
|
+
}
|
|
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
|
+
|
|
279
|
+
function generateServiceFile(mountName: string, operations: Operation[], ctx: EmitterContext): GeneratedFile | null {
|
|
280
|
+
const lines: string[] = [];
|
|
281
|
+
const svcTypeName = serviceTypeName(mountName);
|
|
282
|
+
const csFile = `Services/${className(mountName)}/${svcTypeName}.cs`;
|
|
283
|
+
|
|
284
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
285
|
+
|
|
286
|
+
lines.push(`namespace ${ctx.namespacePascal}`);
|
|
287
|
+
lines.push('{');
|
|
288
|
+
lines.push(' using System;');
|
|
289
|
+
lines.push(' using System.Collections.Generic;');
|
|
290
|
+
lines.push(' using System.Net.Http;');
|
|
291
|
+
lines.push(' using System.Threading;');
|
|
292
|
+
lines.push(' using System.Threading.Tasks;');
|
|
293
|
+
if (groupsNeedJsonConvert(operations, ctx.spec.models)) {
|
|
294
|
+
lines.push(' using Newtonsoft.Json;');
|
|
295
|
+
}
|
|
296
|
+
lines.push('');
|
|
297
|
+
lines.push(
|
|
298
|
+
` /// <summary>Service that exposes the ${humanize(mountName)} API operations on <see cref="WorkOSClient"/>.</summary>`,
|
|
299
|
+
);
|
|
300
|
+
lines.push(` public class ${svcTypeName} : Service`);
|
|
301
|
+
lines.push(' {');
|
|
302
|
+
lines.push(` /// <summary>`);
|
|
303
|
+
lines.push(
|
|
304
|
+
` /// Initializes a new instance of the <see cref="${svcTypeName}"/> class for mocking. The service uses the singleton`,
|
|
305
|
+
);
|
|
306
|
+
lines.push(` /// client configured via <see cref="WorkOSConfiguration.WorkOSClient"/>.`);
|
|
307
|
+
lines.push(` /// </summary>`);
|
|
308
|
+
lines.push(` public ${svcTypeName}() { }`);
|
|
309
|
+
lines.push('');
|
|
310
|
+
lines.push(` /// <summary>`);
|
|
311
|
+
lines.push(` /// Initializes a new instance of the <see cref="${svcTypeName}"/> class bound to the`);
|
|
312
|
+
lines.push(` /// supplied <paramref name="client"/>.`);
|
|
313
|
+
lines.push(` /// </summary>`);
|
|
314
|
+
lines.push(` /// <param name="client">The HTTP client used to make API requests.</param>`);
|
|
315
|
+
lines.push(` public ${svcTypeName}(WorkOSClient client) : base(client) { }`);
|
|
316
|
+
|
|
317
|
+
const emittedMethods = new Set<string>();
|
|
318
|
+
for (const op of operations) {
|
|
319
|
+
const plan = planOperation(op);
|
|
320
|
+
const methodStem = resolveCsMethodStem(op, mountName, ctx);
|
|
321
|
+
const method = resolveCsMethodName(op, mountName, ctx);
|
|
322
|
+
|
|
323
|
+
if (emittedMethods.has(method)) continue;
|
|
324
|
+
emittedMethods.add(method);
|
|
325
|
+
|
|
326
|
+
const resolvedOp = lookupResolved(op, resolvedLookup);
|
|
327
|
+
const isUnionSplit = (resolvedOp?.wrappers?.length ?? 0) > 0;
|
|
328
|
+
|
|
329
|
+
// For union-split operations (e.g. POST /user_management/authenticate), do
|
|
330
|
+
// NOT emit the raw method — its options class is empty and any caller will
|
|
331
|
+
// get a 422 from the API. Only emit the typed AuthenticateWith* wrappers.
|
|
332
|
+
if (!isUnionSplit) {
|
|
333
|
+
lines.push('');
|
|
334
|
+
const methodCode = generateMethod(svcTypeName, mountName, method, methodStem, op, plan, ctx, resolvedOp);
|
|
335
|
+
lines.push(methodCode);
|
|
336
|
+
|
|
337
|
+
if (!(resolvedOp?.urlBuilder ?? false) && method !== methodStem) {
|
|
338
|
+
lines.push('');
|
|
339
|
+
lines.push(generateCompatibilityMethod(mountName, method, methodStem, op, plan, ctx, resolvedOp));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Generate auto-pagination method for paginated list operations
|
|
343
|
+
if (plan.isPaginated && op.pagination) {
|
|
344
|
+
lines.push('');
|
|
345
|
+
const autoPagingCode = generateAutoPagingMethod(mountName, method, methodStem, op, plan, ctx, resolvedOp);
|
|
346
|
+
lines.push(autoPagingCode);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Generate union split wrapper methods
|
|
351
|
+
if (isUnionSplit) {
|
|
352
|
+
const wrapperLines = generateWrapperMethods(svcTypeName, resolvedOp!, ctx);
|
|
353
|
+
lines.push(...wrapperLines);
|
|
354
|
+
for (const w of resolvedOp!.wrappers!) {
|
|
355
|
+
emittedMethods.add(appendAsyncSuffix(methodName(w.name)));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
lines.push(' }');
|
|
361
|
+
lines.push('}');
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
path: csFile,
|
|
365
|
+
content: lines.join('\n'),
|
|
366
|
+
overwriteExisting: true,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function generateOptionsFile(mountName: string, operations: Operation[], ctx: EmitterContext): GeneratedFile | null {
|
|
371
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
372
|
+
const optionsLines: string[] = [];
|
|
373
|
+
let hasOptions = false;
|
|
374
|
+
|
|
375
|
+
optionsLines.push(`namespace ${ctx.namespacePascal}`);
|
|
376
|
+
optionsLines.push('{');
|
|
377
|
+
optionsLines.push(' using System;');
|
|
378
|
+
optionsLines.push(' using System.Collections.Generic;');
|
|
379
|
+
optionsLines.push(' using Newtonsoft.Json;');
|
|
380
|
+
optionsLines.push(' using STJS = System.Text.Json.Serialization;');
|
|
381
|
+
|
|
382
|
+
const emittedOptions = new Set<string>();
|
|
383
|
+
const emittedGroupTypes = new Set<string>();
|
|
384
|
+
for (const op of operations) {
|
|
385
|
+
const plan = planOperation(op);
|
|
386
|
+
const method = resolveCsMethodName(op, mountName, ctx);
|
|
387
|
+
const resolvedOp = lookupResolved(op, resolvedLookup);
|
|
388
|
+
const hidden = buildHiddenParams(resolvedOp);
|
|
389
|
+
|
|
390
|
+
// Union-split operations expose typed wrapper option classes
|
|
391
|
+
// (AuthenticateWith*Options) instead of a generic raw options class.
|
|
392
|
+
// Skip emitting an empty *CreateAuthenticateOptions placeholder.
|
|
393
|
+
if ((resolvedOp?.wrappers?.length ?? 0) > 0) continue;
|
|
394
|
+
|
|
395
|
+
const optionsClass = optionsClassName(mountName, method);
|
|
396
|
+
if (emittedOptions.has(optionsClass)) continue;
|
|
397
|
+
|
|
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;
|
|
402
|
+
const hasBody = plan.hasBody && op.requestBody;
|
|
403
|
+
let hasVisibleBodyFields = false;
|
|
404
|
+
if (hasBody && op.requestBody?.kind === 'model') {
|
|
405
|
+
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
406
|
+
if (bodyModel) hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
407
|
+
} else if (hasBody) {
|
|
408
|
+
hasVisibleBodyFields = true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!hasVisibleQueryParams && !hasVisibleBodyFields && !hasGroups) continue;
|
|
412
|
+
|
|
413
|
+
emittedOptions.add(optionsClass);
|
|
414
|
+
hasOptions = true;
|
|
415
|
+
|
|
416
|
+
// Determine base class: ListOptions for paginated list operations, BaseOptions otherwise
|
|
417
|
+
const isPaginated = plan.isPaginated;
|
|
418
|
+
const baseClass = isPaginated ? 'ListOptions' : 'BaseOptions';
|
|
419
|
+
|
|
420
|
+
optionsLines.push('');
|
|
421
|
+
const opSummary = op.description?.split('\n').find((l) => l.trim()) ?? `${method} on ${mountName}`;
|
|
422
|
+
optionsLines.push(
|
|
423
|
+
` /// <summary>Request options for <see cref="${className(mountName)}Service.${method}"/>: ${escapeXml(opSummary.trim())}</summary>`,
|
|
424
|
+
);
|
|
425
|
+
optionsLines.push(` public class ${optionsClass} : ${baseClass}`);
|
|
426
|
+
optionsLines.push(' {');
|
|
427
|
+
|
|
428
|
+
const emittedFields = new Set<string>();
|
|
429
|
+
|
|
430
|
+
// Body fields
|
|
431
|
+
if (hasBody && op.requestBody?.kind === 'model') {
|
|
432
|
+
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
433
|
+
if (bodyModel) {
|
|
434
|
+
for (const field of bodyModel.fields) {
|
|
435
|
+
if (hidden.has(field.name)) continue;
|
|
436
|
+
if (groupedParams.has(field.name)) continue;
|
|
437
|
+
const csField = fieldName(field.name);
|
|
438
|
+
if (emittedFields.has(csField)) continue;
|
|
439
|
+
emittedFields.add(csField);
|
|
440
|
+
|
|
441
|
+
const isOptional = !field.required;
|
|
442
|
+
const baseType = mapTypeRef(field.type);
|
|
443
|
+
const isAlreadyNullable = baseType.endsWith('?');
|
|
444
|
+
let csType: string;
|
|
445
|
+
let initializer = '';
|
|
446
|
+
|
|
447
|
+
if (isOptional) {
|
|
448
|
+
if (isAlreadyNullable) {
|
|
449
|
+
csType = baseType;
|
|
450
|
+
} else if (isValueTypeRef(field.type)) {
|
|
451
|
+
csType = `${baseType}?`;
|
|
452
|
+
} else {
|
|
453
|
+
csType = `${baseType}?`;
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
csType = baseType;
|
|
457
|
+
if (!isAlreadyNullable && !isValueTypeRef(field.type)) {
|
|
458
|
+
initializer = ' = default!;';
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const isRequiredEnum = field.required && isEnumRef(field.type);
|
|
463
|
+
optionsLines.push(...emitXmlDoc(field.description, ' '));
|
|
464
|
+
if (field.deprecated) {
|
|
465
|
+
const msg = escapeCsAttributeString(deprecationMessage(field.description, 'field'));
|
|
466
|
+
optionsLines.push(` [System.Obsolete("${msg}")]`);
|
|
467
|
+
}
|
|
468
|
+
optionsLines.push(...emitJsonPropertyAttributes(field.name, { isRequiredEnum }));
|
|
469
|
+
optionsLines.push(` public ${csType} ${csField} { get; set; }${initializer}`);
|
|
470
|
+
optionsLines.push('');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
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)
|
|
477
|
+
const PAGINATION_FIELDS = new Set(['before', 'after', 'limit', 'order']);
|
|
478
|
+
for (const param of op.queryParams) {
|
|
479
|
+
if (hidden.has(param.name)) continue;
|
|
480
|
+
if (groupedParams.has(param.name)) continue;
|
|
481
|
+
if (isPaginated && PAGINATION_FIELDS.has(param.name)) continue;
|
|
482
|
+
const csField = fieldName(param.name);
|
|
483
|
+
if (emittedFields.has(csField)) continue;
|
|
484
|
+
emittedFields.add(csField);
|
|
485
|
+
|
|
486
|
+
const isOptional = !param.required;
|
|
487
|
+
const baseType = mapTypeRef(param.type);
|
|
488
|
+
const isAlreadyNullable = baseType.endsWith('?');
|
|
489
|
+
let csType: string;
|
|
490
|
+
let initializer = '';
|
|
491
|
+
|
|
492
|
+
if (isOptional) {
|
|
493
|
+
if (isAlreadyNullable) {
|
|
494
|
+
csType = baseType;
|
|
495
|
+
} else if (isValueTypeRef(param.type)) {
|
|
496
|
+
csType = `${baseType}?`;
|
|
497
|
+
} else {
|
|
498
|
+
csType = `${baseType}?`;
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
csType = baseType;
|
|
502
|
+
if (!isAlreadyNullable && !isValueTypeRef(param.type)) {
|
|
503
|
+
initializer = ' = default!;';
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const isRequiredEnum = param.required && isEnumRef(param.type);
|
|
508
|
+
optionsLines.push(...emitXmlDoc(param.description, ' '));
|
|
509
|
+
if (param.deprecated) {
|
|
510
|
+
const msg = escapeCsAttributeString(deprecationMessage(param.description, 'parameter'));
|
|
511
|
+
optionsLines.push(` [System.Obsolete("${msg}")]`);
|
|
512
|
+
}
|
|
513
|
+
optionsLines.push(...emitJsonPropertyAttributes(param.name, { isRequiredEnum }));
|
|
514
|
+
optionsLines.push(` public ${csType} ${csField} { get; set; }${initializer}`);
|
|
515
|
+
optionsLines.push('');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Hidden fields that need to be set programmatically (e.g., grant_type, client_id)
|
|
519
|
+
const defaults = getOpDefaults(resolvedOp);
|
|
520
|
+
const inferFromClient = getOpInferFromClient(resolvedOp);
|
|
521
|
+
for (const key of Object.keys(defaults)) {
|
|
522
|
+
const csField = fieldName(key);
|
|
523
|
+
if (emittedFields.has(csField)) continue;
|
|
524
|
+
emittedFields.add(csField);
|
|
525
|
+
optionsLines.push(` internal string ${csField} { get; set; } = default!;`);
|
|
526
|
+
optionsLines.push('');
|
|
527
|
+
}
|
|
528
|
+
for (const key of inferFromClient) {
|
|
529
|
+
const csField = fieldName(key);
|
|
530
|
+
if (emittedFields.has(csField)) continue;
|
|
531
|
+
emittedFields.add(csField);
|
|
532
|
+
optionsLines.push(` internal string ${csField} { get; set; } = default!;`);
|
|
533
|
+
optionsLines.push('');
|
|
534
|
+
}
|
|
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
|
+
|
|
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
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
optionsLines.push('}');
|
|
557
|
+
|
|
558
|
+
if (!hasOptions) return null;
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
path: `Services/${className(mountName)}/_interfaces/${className(mountName)}Options.cs`,
|
|
562
|
+
content: optionsLines.join('\n'),
|
|
563
|
+
overwriteExisting: true,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function generateMethod(
|
|
568
|
+
_serviceType: string,
|
|
569
|
+
mountName: string,
|
|
570
|
+
method: string,
|
|
571
|
+
methodStem: string,
|
|
572
|
+
op: Operation,
|
|
573
|
+
plan: OperationPlan,
|
|
574
|
+
ctx: EmitterContext,
|
|
575
|
+
resolvedOp?: ResolvedOperation,
|
|
576
|
+
): string {
|
|
577
|
+
const lines: string[] = [];
|
|
578
|
+
const isPaginated = plan.isPaginated;
|
|
579
|
+
const isDelete = plan.isDelete;
|
|
580
|
+
const hasBody = plan.hasBody && op.requestBody;
|
|
581
|
+
const hidden = buildHiddenParams(resolvedOp);
|
|
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;
|
|
586
|
+
|
|
587
|
+
let hasVisibleBodyFields = false;
|
|
588
|
+
if (hasBody && op.requestBody?.kind === 'model') {
|
|
589
|
+
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
590
|
+
if (bodyModel) hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
591
|
+
} else if (hasBody) {
|
|
592
|
+
hasVisibleBodyFields = true;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const hasParams = hasVisibleBodyFields || hasVisibleQueryParams || hasGroups;
|
|
596
|
+
const optionsClass = hasParams ? optionsClassName(mountName, methodStem) : null;
|
|
597
|
+
const hasHidden = hasHiddenParams(resolvedOp);
|
|
598
|
+
|
|
599
|
+
// Per-operation Bearer token auth (e.g., SSO GetProfile uses access_token instead of API key)
|
|
600
|
+
const hasBearerOverride = op.security?.some((s: any) => s.schemeName !== 'bearerAuth') ?? false;
|
|
601
|
+
const bearerParamName = hasBearerOverride
|
|
602
|
+
? op.security!.find((s: any) => s.schemeName !== 'bearerAuth')!.schemeName
|
|
603
|
+
: null;
|
|
604
|
+
|
|
605
|
+
// URL-builder operations (e.g., /sso/authorize redirect endpoints) build a URL
|
|
606
|
+
// string for the caller to redirect to instead of issuing an HTTP request.
|
|
607
|
+
const isUrlBuilder = resolvedOp?.urlBuilder ?? false;
|
|
608
|
+
|
|
609
|
+
// Return type
|
|
610
|
+
let returnType: string;
|
|
611
|
+
if (isUrlBuilder) {
|
|
612
|
+
returnType = 'string';
|
|
613
|
+
} else if (isPaginated && op.pagination) {
|
|
614
|
+
const itemType = resolveListItemType(op.pagination.itemType, ctx);
|
|
615
|
+
returnType = `Task<WorkOSList<${itemType}>>`;
|
|
616
|
+
} else if (isDelete) {
|
|
617
|
+
returnType = 'Task';
|
|
618
|
+
} else if (plan.responseModelName) {
|
|
619
|
+
const respType = modelClassName(resolveModelName(plan.responseModelName));
|
|
620
|
+
if (!isPaginated && op.response?.kind === 'array') {
|
|
621
|
+
returnType = `Task<List<${respType}>>`;
|
|
622
|
+
} else {
|
|
623
|
+
returnType = `Task<${respType}>`;
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
returnType = 'Task';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// XML doc comment (full multi-line description from the spec)
|
|
630
|
+
lines.push(...emitXmlDoc(op.description, ' '));
|
|
631
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
632
|
+
const paramDesc = p.description ? escapeXml(p.description) : `The ${humanize(p.name)}.`;
|
|
633
|
+
lines.push(` /// <param name="${localName(p.name)}">${paramDesc}</param>`);
|
|
634
|
+
}
|
|
635
|
+
if (hasBearerOverride && bearerParamName) {
|
|
636
|
+
lines.push(` /// <param name="${localName(bearerParamName)}">The bearer token for authentication.</param>`);
|
|
637
|
+
}
|
|
638
|
+
if (optionsClass) {
|
|
639
|
+
lines.push(` /// <param name="options">Request options.</param>`);
|
|
640
|
+
}
|
|
641
|
+
if (!isUrlBuilder) {
|
|
642
|
+
lines.push(` /// <param name="requestOptions">Per-request configuration overrides.</param>`);
|
|
643
|
+
lines.push(` /// <param name="cancellationToken">Cancellation token.</param>`);
|
|
644
|
+
}
|
|
645
|
+
if (isUrlBuilder) {
|
|
646
|
+
lines.push(` /// <returns>The fully-qualified URL for the caller to redirect to.</returns>`);
|
|
647
|
+
} else if (isPaginated && op.pagination) {
|
|
648
|
+
const itemType = resolveListItemType(op.pagination.itemType, ctx);
|
|
649
|
+
lines.push(` /// <returns>A page of <see cref="${itemType}"/> results.</returns>`);
|
|
650
|
+
} else if (plan.responseModelName) {
|
|
651
|
+
const respType = modelClassName(resolveModelName(plan.responseModelName));
|
|
652
|
+
lines.push(` /// <returns>The <see cref="${respType}"/> result.</returns>`);
|
|
653
|
+
}
|
|
654
|
+
if (op.deprecated) {
|
|
655
|
+
const msg = escapeCsAttributeString(deprecationMessage(op.description, 'operation'));
|
|
656
|
+
lines.push(` [System.Obsolete("${msg}")]`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Method signature
|
|
660
|
+
const params: string[] = [];
|
|
661
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
662
|
+
params.push(`string ${localName(p.name)}`);
|
|
663
|
+
}
|
|
664
|
+
if (hasBearerOverride && bearerParamName) {
|
|
665
|
+
params.push(`string ${localName(bearerParamName)}`);
|
|
666
|
+
}
|
|
667
|
+
if (optionsClass) {
|
|
668
|
+
const isRequired = hasVisibleBodyFields && !isPaginated;
|
|
669
|
+
params.push(isRequired ? `${optionsClass} options` : `${optionsClass}? options = null`);
|
|
670
|
+
}
|
|
671
|
+
if (!isUrlBuilder) {
|
|
672
|
+
params.push('RequestOptions? requestOptions = null');
|
|
673
|
+
params.push('CancellationToken cancellationToken = default');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const asyncKeyword = isUrlBuilder ? '' : 'async ';
|
|
677
|
+
lines.push(` public virtual ${asyncKeyword}${returnType} ${method}(${params.join(', ')})`);
|
|
678
|
+
lines.push(' {');
|
|
679
|
+
|
|
680
|
+
// Inject hidden params
|
|
681
|
+
if (hasHidden && optionsClass) {
|
|
682
|
+
const isOptionalParam = !hasVisibleBodyFields || isPaginated;
|
|
683
|
+
if (isOptionalParam) {
|
|
684
|
+
lines.push(` options ??= new ${optionsClass}();`);
|
|
685
|
+
}
|
|
686
|
+
const defaults = getOpDefaults(resolvedOp);
|
|
687
|
+
const inferFromClient = getOpInferFromClient(resolvedOp);
|
|
688
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
689
|
+
lines.push(` options.${fieldName(key)} = ${csLiteral(value as string | number | boolean)};`);
|
|
690
|
+
}
|
|
691
|
+
for (const field of inferFromClient) {
|
|
692
|
+
if (field === 'client_id') {
|
|
693
|
+
lines.push(` options.${fieldName(field)} = this.Client.RequireClientId();`);
|
|
694
|
+
} else {
|
|
695
|
+
lines.push(
|
|
696
|
+
` options.${fieldName(field)} = this.Client.${clientFieldExpression(field)} ?? string.Empty;`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Build path
|
|
703
|
+
const pathExpr = buildPathExpr(op);
|
|
704
|
+
|
|
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;
|
|
710
|
+
const optionsArg = optionsClass ? 'options' : 'null';
|
|
711
|
+
|
|
712
|
+
if (needsInlineRequest) {
|
|
713
|
+
lines.push(' var request = new WorkOSRequest');
|
|
714
|
+
lines.push(' {');
|
|
715
|
+
lines.push(` Method = HttpMethod.${httpMethodCs(op.httpMethod)},`);
|
|
716
|
+
lines.push(` Path = ${pathExpr},`);
|
|
717
|
+
if (optionsClass) {
|
|
718
|
+
lines.push(' Options = options,');
|
|
719
|
+
}
|
|
720
|
+
if (hasBearerOverride && bearerParamName) {
|
|
721
|
+
lines.push(` AccessToken = ${localName(bearerParamName)},`);
|
|
722
|
+
}
|
|
723
|
+
if (!isUrlBuilder) {
|
|
724
|
+
lines.push(` RequestOptions = requestOptions,`);
|
|
725
|
+
}
|
|
726
|
+
lines.push(' };');
|
|
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
|
+
|
|
739
|
+
if (isUrlBuilder) {
|
|
740
|
+
lines.push(' return this.Client.BuildRequestUri(request).ToString();');
|
|
741
|
+
} else if (returnType.startsWith('Task<')) {
|
|
742
|
+
const innerType = returnType.slice(5, -1);
|
|
743
|
+
lines.push(` return await this.Client.MakeAPIRequest<${innerType}>(request, cancellationToken);`);
|
|
744
|
+
} else {
|
|
745
|
+
lines.push(' await this.Client.MakeRawAPIRequest(request, cancellationToken);');
|
|
746
|
+
}
|
|
747
|
+
} else if (isDelete) {
|
|
748
|
+
lines.push(` await this.DeleteAsync(${pathExpr}, ${optionsArg}, requestOptions, cancellationToken);`);
|
|
749
|
+
} else if (returnType.startsWith('Task<')) {
|
|
750
|
+
const innerType = returnType.slice(5, -1);
|
|
751
|
+
const helper = httpMethodHelperName(op.httpMethod);
|
|
752
|
+
lines.push(
|
|
753
|
+
` return await this.${helper}<${innerType}>(${pathExpr}, ${optionsArg}, requestOptions, cancellationToken);`,
|
|
754
|
+
);
|
|
755
|
+
} else {
|
|
756
|
+
const helper = httpMethodHelperName(op.httpMethod);
|
|
757
|
+
lines.push(
|
|
758
|
+
` await this.${helper}<object>(${pathExpr}, ${optionsArg}, requestOptions, cancellationToken);`,
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
lines.push(' }');
|
|
763
|
+
return lines.join('\n');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function generateAutoPagingMethod(
|
|
767
|
+
mountName: string,
|
|
768
|
+
method: string,
|
|
769
|
+
methodStem: string,
|
|
770
|
+
op: Operation,
|
|
771
|
+
plan: OperationPlan,
|
|
772
|
+
ctx: EmitterContext,
|
|
773
|
+
resolvedOp?: ResolvedOperation,
|
|
774
|
+
): string {
|
|
775
|
+
const lines: string[] = [];
|
|
776
|
+
const hidden = buildHiddenParams(resolvedOp);
|
|
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;
|
|
781
|
+
|
|
782
|
+
let hasVisibleBodyFields = false;
|
|
783
|
+
if (plan.hasBody && op.requestBody?.kind === 'model') {
|
|
784
|
+
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
785
|
+
if (bodyModel) hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const hasParams = hasVisibleBodyFields || hasVisibleQueryParams || hasGroups;
|
|
789
|
+
const optionsClass = hasParams ? optionsClassName(mountName, methodStem) : null;
|
|
790
|
+
|
|
791
|
+
const itemType = resolveListItemType(op.pagination!.itemType, ctx);
|
|
792
|
+
|
|
793
|
+
// XML doc
|
|
794
|
+
lines.push(
|
|
795
|
+
` /// <summary>Auto-paging variant of <see cref="${method}"/>. Yields individual items across all pages.</summary>`,
|
|
796
|
+
);
|
|
797
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
798
|
+
const paramDesc = p.description ? escapeXml(p.description) : `The ${humanize(p.name)}.`;
|
|
799
|
+
lines.push(` /// <param name="${localName(p.name)}">${paramDesc}</param>`);
|
|
800
|
+
}
|
|
801
|
+
if (optionsClass) {
|
|
802
|
+
lines.push(` /// <param name="options">Request options.</param>`);
|
|
803
|
+
}
|
|
804
|
+
lines.push(` /// <param name="requestOptions">Per-request configuration overrides.</param>`);
|
|
805
|
+
lines.push(` /// <param name="cancellationToken">Cancellation token.</param>`);
|
|
806
|
+
lines.push(` /// <returns>An async sequence of <see cref="${itemType}"/> items.</returns>`);
|
|
807
|
+
|
|
808
|
+
// Signature
|
|
809
|
+
const params: string[] = [];
|
|
810
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
811
|
+
params.push(`string ${localName(p.name)}`);
|
|
812
|
+
}
|
|
813
|
+
if (optionsClass) {
|
|
814
|
+
params.push(`${optionsClass}? options = null`);
|
|
815
|
+
}
|
|
816
|
+
params.push('RequestOptions? requestOptions = null');
|
|
817
|
+
params.push('CancellationToken cancellationToken = default');
|
|
818
|
+
|
|
819
|
+
lines.push(` public virtual IAsyncEnumerable<${itemType}> ${methodStem}AutoPagingAsync(${params.join(', ')})`);
|
|
820
|
+
lines.push(' {');
|
|
821
|
+
|
|
822
|
+
const pathExpr = buildPathExpr(op);
|
|
823
|
+
const optionsArg = optionsClass ? 'options' : 'null';
|
|
824
|
+
lines.push(
|
|
825
|
+
` return this.ListAutoPagingAsync<${itemType}>(${pathExpr}, ${optionsArg}, requestOptions, cancellationToken);`,
|
|
826
|
+
);
|
|
827
|
+
lines.push(' }');
|
|
828
|
+
|
|
829
|
+
return lines.join('\n');
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function resolveCsMethodName(op: Operation, mountName: string, ctx: EmitterContext): string {
|
|
833
|
+
return resolveMethodName(op, { name: mountName, operations: [op] }, ctx);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export function resolveCsMethodStem(op: Operation, mountName: string, ctx: EmitterContext): string {
|
|
837
|
+
return resolveMethodStem(op, { name: mountName, operations: [op] }, ctx);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function optionsClassName(mountName: string, method: string): string {
|
|
841
|
+
const methodStem = method.endsWith('Async') ? method.slice(0, -5) : method;
|
|
842
|
+
const prefix = className(mountName);
|
|
843
|
+
if (methodStem.startsWith(prefix)) return `${methodStem}Options`;
|
|
844
|
+
return `${prefix}${methodStem}Options`;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function buildPathExpr(op: Operation): string {
|
|
848
|
+
if (op.pathParams.length === 0) {
|
|
849
|
+
return `"${op.path}"`;
|
|
850
|
+
}
|
|
851
|
+
// Build C# string interpolation
|
|
852
|
+
let interpolated = op.path;
|
|
853
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
854
|
+
interpolated = interpolated.replace(`{${p.name}}`, `{Uri.EscapeDataString(${localName(p.name)})}`);
|
|
855
|
+
}
|
|
856
|
+
return `$"${interpolated}"`;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function resolveListItemType(itemType: import('@workos/oagen').TypeRef, ctx: EmitterContext): string {
|
|
860
|
+
if (itemType.kind === 'model') {
|
|
861
|
+
const model = ctx.spec.models.find((m) => m.name === itemType.name);
|
|
862
|
+
if (model && isListWrapperModel(model)) {
|
|
863
|
+
const dataField = model.fields.find((f) => f.name === 'data');
|
|
864
|
+
if (dataField && dataField.type.kind === 'array' && dataField.type.items.kind === 'model') {
|
|
865
|
+
return modelClassName(resolveModelName(dataField.type.items.name));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return modelClassName(resolveModelName(itemType.name));
|
|
869
|
+
}
|
|
870
|
+
return mapTypeRef(itemType);
|
|
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
|
+
}
|