@workos/oagen-emitters 0.3.0 → 0.4.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/index.d.mts +4 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3288 -791
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/dotnet.md +336 -0
- package/oagen.config.ts +42 -12
- package/package.json +2 -2
- 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 +246 -0
- package/src/dotnet/manifest.ts +36 -0
- package/src/dotnet/models.ts +344 -0
- package/src/dotnet/naming.ts +330 -0
- package/src/dotnet/resources.ts +622 -0
- package/src/dotnet/tests.ts +693 -0
- package/src/dotnet/type-map.ts +201 -0
- package/src/dotnet/wrappers.ts +186 -0
- package/src/go/index.ts +5 -2
- package/src/go/naming.ts +5 -17
- package/src/index.ts +1 -0
- package/src/kotlin/client.ts +53 -0
- package/src/kotlin/enums.ts +162 -0
- package/src/kotlin/index.ts +92 -0
- package/src/kotlin/manifest.ts +55 -0
- package/src/kotlin/models.ts +395 -0
- package/src/kotlin/naming.ts +223 -0
- package/src/kotlin/overrides.ts +25 -0
- package/src/kotlin/resources.ts +667 -0
- package/src/kotlin/tests.ts +1019 -0
- package/src/kotlin/type-map.ts +123 -0
- package/src/kotlin/wrappers.ts +168 -0
- package/src/node/client.ts +50 -0
- package/src/node/index.ts +1 -0
- package/src/node/resources.ts +164 -44
- package/src/node/tests.ts +37 -7
- package/src/php/client.ts +11 -3
- package/src/php/naming.ts +2 -21
- package/src/php/resources.ts +81 -6
- package/src/php/tests.ts +93 -17
- package/src/php/wrappers.ts +1 -0
- package/src/python/client.ts +37 -29
- package/src/python/enums.ts +7 -7
- package/src/python/models.ts +1 -1
- package/src/python/naming.ts +2 -22
- package/src/shared/model-utils.ts +232 -15
- package/src/shared/naming-utils.ts +47 -0
- 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 +260 -0
- package/test/dotnet/resources.test.ts +255 -0
- package/test/dotnet/tests.test.ts +202 -0
- package/test/kotlin/models.test.ts +135 -0
- package/test/kotlin/tests.test.ts +176 -0
- package/test/node/client.test.ts +74 -0
- package/test/node/resources.test.ts +216 -15
- package/test/php/client.test.ts +2 -1
- package/test/php/resources.test.ts +38 -0
- package/test/php/tests.test.ts +67 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Service,
|
|
3
|
+
Operation,
|
|
4
|
+
OperationPlan,
|
|
5
|
+
EmitterContext,
|
|
6
|
+
GeneratedFile,
|
|
7
|
+
ResolvedOperation,
|
|
8
|
+
} from '@workos/oagen';
|
|
9
|
+
import { planOperation } from '@workos/oagen';
|
|
10
|
+
import { isListWrapperModel } from './models.js';
|
|
11
|
+
import { mapTypeRef, isValueTypeRef, isEnumRef, emitJsonPropertyAttributes } from './type-map.js';
|
|
12
|
+
import {
|
|
13
|
+
className,
|
|
14
|
+
fieldName,
|
|
15
|
+
methodName,
|
|
16
|
+
resolveClassName,
|
|
17
|
+
resolveMethodName,
|
|
18
|
+
serviceTypeName,
|
|
19
|
+
localName,
|
|
20
|
+
csLiteral,
|
|
21
|
+
clientFieldExpression,
|
|
22
|
+
httpMethodCs,
|
|
23
|
+
httpMethodHelperName,
|
|
24
|
+
escapeXml,
|
|
25
|
+
emitXmlDoc,
|
|
26
|
+
deprecationMessage,
|
|
27
|
+
escapeCsAttributeString,
|
|
28
|
+
humanize,
|
|
29
|
+
} from './naming.js';
|
|
30
|
+
import {
|
|
31
|
+
buildResolvedLookup,
|
|
32
|
+
lookupResolved,
|
|
33
|
+
groupByMount,
|
|
34
|
+
getOpDefaults,
|
|
35
|
+
getOpInferFromClient,
|
|
36
|
+
buildHiddenParams,
|
|
37
|
+
hasHiddenParams,
|
|
38
|
+
} from '../shared/resolved-ops.js';
|
|
39
|
+
import { generateWrapperMethods } from './wrappers.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Return path params sorted by their first occurrence in the URL template.
|
|
43
|
+
*/
|
|
44
|
+
export function sortPathParamsByTemplateOrder(op: Operation): typeof op.pathParams {
|
|
45
|
+
return [...op.pathParams].sort((a, b) => {
|
|
46
|
+
const posA = op.path.indexOf(`{${a.name}}`);
|
|
47
|
+
const posB = op.path.indexOf(`{${b.name}}`);
|
|
48
|
+
return posA - posB;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the resource class name for a service.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveResourceClassName(service: Service, ctx: EmitterContext): string {
|
|
56
|
+
return resolveClassName(service, ctx);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate C# service files from IR Service definitions.
|
|
61
|
+
* Each mount group becomes a single Service.cs file.
|
|
62
|
+
*/
|
|
63
|
+
export function generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
64
|
+
if (services.length === 0) return [];
|
|
65
|
+
|
|
66
|
+
const files: GeneratedFile[] = [];
|
|
67
|
+
const mountGroups = groupByMount(ctx);
|
|
68
|
+
|
|
69
|
+
const entries: Array<{ name: string; operations: Operation[] }> =
|
|
70
|
+
mountGroups.size > 0
|
|
71
|
+
? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
|
|
72
|
+
: services.map((s) => ({ name: resolveResourceClassName(s, ctx), operations: s.operations }));
|
|
73
|
+
|
|
74
|
+
for (const { name: mountName, operations } of entries) {
|
|
75
|
+
if (operations.length === 0) continue;
|
|
76
|
+
const serviceFile = generateServiceFile(mountName, operations, ctx);
|
|
77
|
+
if (serviceFile) files.push(serviceFile);
|
|
78
|
+
const optionsFile = generateOptionsFile(mountName, operations, ctx);
|
|
79
|
+
if (optionsFile) files.push(optionsFile);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return files;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function generateServiceFile(mountName: string, operations: Operation[], ctx: EmitterContext): GeneratedFile | null {
|
|
86
|
+
const lines: string[] = [];
|
|
87
|
+
const svcTypeName = serviceTypeName(mountName);
|
|
88
|
+
const csFile = `Services/${className(mountName)}/${svcTypeName}.cs`;
|
|
89
|
+
|
|
90
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
91
|
+
|
|
92
|
+
lines.push(`namespace ${ctx.namespacePascal}`);
|
|
93
|
+
lines.push('{');
|
|
94
|
+
lines.push(' using System.Collections.Generic;');
|
|
95
|
+
lines.push(' using System.Net.Http;');
|
|
96
|
+
lines.push(' using System.Threading;');
|
|
97
|
+
lines.push(' using System.Threading.Tasks;');
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push(
|
|
100
|
+
` /// <summary>Service that exposes the ${humanize(mountName)} API operations on <see cref="WorkOSClient"/>.</summary>`,
|
|
101
|
+
);
|
|
102
|
+
lines.push(` public class ${svcTypeName} : Service`);
|
|
103
|
+
lines.push(' {');
|
|
104
|
+
lines.push(` /// <summary>`);
|
|
105
|
+
lines.push(
|
|
106
|
+
` /// Initializes a new instance of the <see cref="${svcTypeName}"/> class for mocking. The service uses the singleton`,
|
|
107
|
+
);
|
|
108
|
+
lines.push(` /// client configured via <see cref="WorkOSConfiguration.WorkOSClient"/>.`);
|
|
109
|
+
lines.push(` /// </summary>`);
|
|
110
|
+
lines.push(` public ${svcTypeName}() { }`);
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push(` /// <summary>`);
|
|
113
|
+
lines.push(` /// Initializes a new instance of the <see cref="${svcTypeName}"/> class bound to the`);
|
|
114
|
+
lines.push(` /// supplied <paramref name="client"/>.`);
|
|
115
|
+
lines.push(` /// </summary>`);
|
|
116
|
+
lines.push(` /// <param name="client">The HTTP client used to make API requests.</param>`);
|
|
117
|
+
lines.push(` public ${svcTypeName}(WorkOSClient client) : base(client) { }`);
|
|
118
|
+
|
|
119
|
+
const emittedMethods = new Set<string>();
|
|
120
|
+
for (const op of operations) {
|
|
121
|
+
const plan = planOperation(op);
|
|
122
|
+
const method = resolveCsMethodName(op, mountName, ctx);
|
|
123
|
+
|
|
124
|
+
if (emittedMethods.has(method)) continue;
|
|
125
|
+
emittedMethods.add(method);
|
|
126
|
+
|
|
127
|
+
const resolvedOp = lookupResolved(op, resolvedLookup);
|
|
128
|
+
const isUnionSplit = (resolvedOp?.wrappers?.length ?? 0) > 0;
|
|
129
|
+
|
|
130
|
+
// For union-split operations (e.g. POST /user_management/authenticate), do
|
|
131
|
+
// NOT emit the raw method — its options class is empty and any caller will
|
|
132
|
+
// get a 422 from the API. Only emit the typed AuthenticateWith* wrappers.
|
|
133
|
+
if (!isUnionSplit) {
|
|
134
|
+
lines.push('');
|
|
135
|
+
const methodCode = generateMethod(svcTypeName, mountName, method, op, plan, ctx, resolvedOp);
|
|
136
|
+
lines.push(methodCode);
|
|
137
|
+
|
|
138
|
+
// Generate auto-pagination method for paginated list operations
|
|
139
|
+
if (plan.isPaginated && op.pagination) {
|
|
140
|
+
lines.push('');
|
|
141
|
+
const autoPagingCode = generateAutoPagingMethod(mountName, method, op, plan, ctx, resolvedOp);
|
|
142
|
+
lines.push(autoPagingCode);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Generate union split wrapper methods
|
|
147
|
+
if (isUnionSplit) {
|
|
148
|
+
const wrapperLines = generateWrapperMethods(svcTypeName, resolvedOp!, ctx);
|
|
149
|
+
lines.push(...wrapperLines);
|
|
150
|
+
for (const w of resolvedOp!.wrappers!) {
|
|
151
|
+
emittedMethods.add(methodName(w.name));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
lines.push(' }');
|
|
157
|
+
lines.push('}');
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
path: csFile,
|
|
161
|
+
content: lines.join('\n'),
|
|
162
|
+
overwriteExisting: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function generateOptionsFile(mountName: string, operations: Operation[], ctx: EmitterContext): GeneratedFile | null {
|
|
167
|
+
const resolvedLookup = buildResolvedLookup(ctx);
|
|
168
|
+
const optionsLines: string[] = [];
|
|
169
|
+
let hasOptions = false;
|
|
170
|
+
|
|
171
|
+
optionsLines.push(`namespace ${ctx.namespacePascal}`);
|
|
172
|
+
optionsLines.push('{');
|
|
173
|
+
optionsLines.push(' using System;');
|
|
174
|
+
optionsLines.push(' using System.Collections.Generic;');
|
|
175
|
+
optionsLines.push(' using Newtonsoft.Json;');
|
|
176
|
+
optionsLines.push(' using STJS = System.Text.Json.Serialization;');
|
|
177
|
+
|
|
178
|
+
const emittedOptions = new Set<string>();
|
|
179
|
+
for (const op of operations) {
|
|
180
|
+
const plan = planOperation(op);
|
|
181
|
+
const method = resolveCsMethodName(op, mountName, ctx);
|
|
182
|
+
const resolvedOp = lookupResolved(op, resolvedLookup);
|
|
183
|
+
const hidden = buildHiddenParams(resolvedOp);
|
|
184
|
+
|
|
185
|
+
// Union-split operations expose typed wrapper option classes
|
|
186
|
+
// (AuthenticateWith*Options) instead of a generic raw options class.
|
|
187
|
+
// Skip emitting an empty *CreateAuthenticateOptions placeholder.
|
|
188
|
+
if ((resolvedOp?.wrappers?.length ?? 0) > 0) continue;
|
|
189
|
+
|
|
190
|
+
const optionsClass = optionsClassName(mountName, method);
|
|
191
|
+
if (emittedOptions.has(optionsClass)) continue;
|
|
192
|
+
|
|
193
|
+
const hasVisibleQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name)).length > 0;
|
|
194
|
+
const hasBody = plan.hasBody && op.requestBody;
|
|
195
|
+
let hasVisibleBodyFields = false;
|
|
196
|
+
if (hasBody && op.requestBody?.kind === 'model') {
|
|
197
|
+
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
198
|
+
if (bodyModel) hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
199
|
+
} else if (hasBody) {
|
|
200
|
+
hasVisibleBodyFields = true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!hasVisibleQueryParams && !hasVisibleBodyFields) continue;
|
|
204
|
+
|
|
205
|
+
emittedOptions.add(optionsClass);
|
|
206
|
+
hasOptions = true;
|
|
207
|
+
|
|
208
|
+
// Determine base class: ListOptions for paginated list operations, BaseOptions otherwise
|
|
209
|
+
const isPaginated = plan.isPaginated;
|
|
210
|
+
const baseClass = isPaginated ? 'ListOptions' : 'BaseOptions';
|
|
211
|
+
|
|
212
|
+
optionsLines.push('');
|
|
213
|
+
const opSummary = op.description?.split('\n').find((l) => l.trim()) ?? `${method} on ${mountName}`;
|
|
214
|
+
optionsLines.push(
|
|
215
|
+
` /// <summary>Request options for <see cref="${className(mountName)}Service.${method}"/>: ${escapeXml(opSummary.trim())}</summary>`,
|
|
216
|
+
);
|
|
217
|
+
optionsLines.push(` public class ${optionsClass} : ${baseClass}`);
|
|
218
|
+
optionsLines.push(' {');
|
|
219
|
+
|
|
220
|
+
const emittedFields = new Set<string>();
|
|
221
|
+
|
|
222
|
+
// Body fields
|
|
223
|
+
if (hasBody && op.requestBody?.kind === 'model') {
|
|
224
|
+
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
225
|
+
if (bodyModel) {
|
|
226
|
+
for (const field of bodyModel.fields) {
|
|
227
|
+
if (hidden.has(field.name)) continue;
|
|
228
|
+
const csField = fieldName(field.name);
|
|
229
|
+
if (emittedFields.has(csField)) continue;
|
|
230
|
+
emittedFields.add(csField);
|
|
231
|
+
|
|
232
|
+
const isOptional = !field.required;
|
|
233
|
+
const baseType = mapTypeRef(field.type);
|
|
234
|
+
const isAlreadyNullable = baseType.endsWith('?');
|
|
235
|
+
let csType: string;
|
|
236
|
+
let initializer = '';
|
|
237
|
+
|
|
238
|
+
if (isOptional) {
|
|
239
|
+
if (isAlreadyNullable) {
|
|
240
|
+
csType = baseType;
|
|
241
|
+
} else if (isValueTypeRef(field.type)) {
|
|
242
|
+
csType = `${baseType}?`;
|
|
243
|
+
} else {
|
|
244
|
+
csType = `${baseType}?`;
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
csType = baseType;
|
|
248
|
+
if (!isAlreadyNullable && !isValueTypeRef(field.type)) {
|
|
249
|
+
initializer = ' = default!;';
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const isRequiredEnum = field.required && isEnumRef(field.type);
|
|
254
|
+
optionsLines.push(...emitXmlDoc(field.description, ' '));
|
|
255
|
+
if (field.deprecated) {
|
|
256
|
+
const msg = escapeCsAttributeString(deprecationMessage(field.description, 'field'));
|
|
257
|
+
optionsLines.push(` [System.Obsolete("${msg}")]`);
|
|
258
|
+
}
|
|
259
|
+
optionsLines.push(...emitJsonPropertyAttributes(field.name, { isRequiredEnum }));
|
|
260
|
+
optionsLines.push(` public ${csType} ${csField} { get; set; }${initializer}`);
|
|
261
|
+
optionsLines.push('');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Query params (skip pagination fields for list options — they're in ListOptions base)
|
|
267
|
+
const PAGINATION_FIELDS = new Set(['before', 'after', 'limit', 'order']);
|
|
268
|
+
for (const param of op.queryParams) {
|
|
269
|
+
if (hidden.has(param.name)) continue;
|
|
270
|
+
if (isPaginated && PAGINATION_FIELDS.has(param.name)) continue;
|
|
271
|
+
const csField = fieldName(param.name);
|
|
272
|
+
if (emittedFields.has(csField)) continue;
|
|
273
|
+
emittedFields.add(csField);
|
|
274
|
+
|
|
275
|
+
const isOptional = !param.required;
|
|
276
|
+
const baseType = mapTypeRef(param.type);
|
|
277
|
+
const isAlreadyNullable = baseType.endsWith('?');
|
|
278
|
+
let csType: string;
|
|
279
|
+
let initializer = '';
|
|
280
|
+
|
|
281
|
+
if (isOptional) {
|
|
282
|
+
if (isAlreadyNullable) {
|
|
283
|
+
csType = baseType;
|
|
284
|
+
} else if (isValueTypeRef(param.type)) {
|
|
285
|
+
csType = `${baseType}?`;
|
|
286
|
+
} else {
|
|
287
|
+
csType = `${baseType}?`;
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
csType = baseType;
|
|
291
|
+
if (!isAlreadyNullable && !isValueTypeRef(param.type)) {
|
|
292
|
+
initializer = ' = default!;';
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const isRequiredEnum = param.required && isEnumRef(param.type);
|
|
297
|
+
optionsLines.push(...emitXmlDoc(param.description, ' '));
|
|
298
|
+
if (param.deprecated) {
|
|
299
|
+
const msg = escapeCsAttributeString(deprecationMessage(param.description, 'parameter'));
|
|
300
|
+
optionsLines.push(` [System.Obsolete("${msg}")]`);
|
|
301
|
+
}
|
|
302
|
+
optionsLines.push(...emitJsonPropertyAttributes(param.name, { isRequiredEnum }));
|
|
303
|
+
optionsLines.push(` public ${csType} ${csField} { get; set; }${initializer}`);
|
|
304
|
+
optionsLines.push('');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Hidden fields that need to be set programmatically (e.g., grant_type, client_id)
|
|
308
|
+
const defaults = getOpDefaults(resolvedOp);
|
|
309
|
+
const inferFromClient = getOpInferFromClient(resolvedOp);
|
|
310
|
+
for (const key of Object.keys(defaults)) {
|
|
311
|
+
const csField = fieldName(key);
|
|
312
|
+
if (emittedFields.has(csField)) continue;
|
|
313
|
+
emittedFields.add(csField);
|
|
314
|
+
optionsLines.push(` [JsonProperty("${key}")]`);
|
|
315
|
+
optionsLines.push(` [STJS.JsonPropertyName("${key}")]`);
|
|
316
|
+
optionsLines.push(` internal string ${csField} { get; set; } = default!;`);
|
|
317
|
+
optionsLines.push('');
|
|
318
|
+
}
|
|
319
|
+
for (const key of inferFromClient) {
|
|
320
|
+
const csField = fieldName(key);
|
|
321
|
+
if (emittedFields.has(csField)) continue;
|
|
322
|
+
emittedFields.add(csField);
|
|
323
|
+
optionsLines.push(` [JsonProperty("${key}")]`);
|
|
324
|
+
optionsLines.push(` [STJS.JsonPropertyName("${key}")]`);
|
|
325
|
+
optionsLines.push(` internal string ${csField} { get; set; } = default!;`);
|
|
326
|
+
optionsLines.push('');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
optionsLines.push(' }');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
optionsLines.push('}');
|
|
333
|
+
|
|
334
|
+
if (!hasOptions) return null;
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
path: `Services/${className(mountName)}/_interfaces/${className(mountName)}Options.cs`,
|
|
338
|
+
content: optionsLines.join('\n'),
|
|
339
|
+
overwriteExisting: true,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function generateMethod(
|
|
344
|
+
_serviceType: string,
|
|
345
|
+
mountName: string,
|
|
346
|
+
method: string,
|
|
347
|
+
op: Operation,
|
|
348
|
+
plan: OperationPlan,
|
|
349
|
+
ctx: EmitterContext,
|
|
350
|
+
resolvedOp?: ResolvedOperation,
|
|
351
|
+
): string {
|
|
352
|
+
const lines: string[] = [];
|
|
353
|
+
const isPaginated = plan.isPaginated;
|
|
354
|
+
const isDelete = plan.isDelete;
|
|
355
|
+
const hasBody = plan.hasBody && op.requestBody;
|
|
356
|
+
const hidden = buildHiddenParams(resolvedOp);
|
|
357
|
+
const hasVisibleQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name)).length > 0;
|
|
358
|
+
|
|
359
|
+
let hasVisibleBodyFields = false;
|
|
360
|
+
if (hasBody && op.requestBody?.kind === 'model') {
|
|
361
|
+
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
362
|
+
if (bodyModel) hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
363
|
+
} else if (hasBody) {
|
|
364
|
+
hasVisibleBodyFields = true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const hasParams = hasVisibleBodyFields || hasVisibleQueryParams;
|
|
368
|
+
const optionsClass = hasParams ? optionsClassName(mountName, method) : null;
|
|
369
|
+
const hasHidden = hasHiddenParams(resolvedOp);
|
|
370
|
+
|
|
371
|
+
// Per-operation Bearer token auth (e.g., SSO GetProfile uses access_token instead of API key)
|
|
372
|
+
const hasBearerOverride = op.security?.some((s: any) => s.schemeName !== 'bearerAuth') ?? false;
|
|
373
|
+
const bearerParamName = hasBearerOverride
|
|
374
|
+
? op.security!.find((s: any) => s.schemeName !== 'bearerAuth')!.schemeName
|
|
375
|
+
: null;
|
|
376
|
+
|
|
377
|
+
// URL-builder operations (e.g., /sso/authorize redirect endpoints) build a URL
|
|
378
|
+
// string for the caller to redirect to instead of issuing an HTTP request.
|
|
379
|
+
const isUrlBuilder = resolvedOp?.urlBuilder ?? false;
|
|
380
|
+
|
|
381
|
+
// Return type
|
|
382
|
+
let returnType: string;
|
|
383
|
+
if (isUrlBuilder) {
|
|
384
|
+
returnType = 'string';
|
|
385
|
+
} else if (isPaginated && op.pagination) {
|
|
386
|
+
const itemType = resolveListItemType(op.pagination.itemType, ctx);
|
|
387
|
+
returnType = `Task<WorkOSList<${itemType}>>`;
|
|
388
|
+
} else if (isDelete) {
|
|
389
|
+
returnType = 'Task';
|
|
390
|
+
} else if (plan.responseModelName) {
|
|
391
|
+
const respType = className(plan.responseModelName);
|
|
392
|
+
if (!isPaginated && op.response?.kind === 'array') {
|
|
393
|
+
returnType = `Task<List<${respType}>>`;
|
|
394
|
+
} else {
|
|
395
|
+
returnType = `Task<${respType}>`;
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
returnType = 'Task';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// XML doc comment (full multi-line description from the spec)
|
|
402
|
+
lines.push(...emitXmlDoc(op.description, ' '));
|
|
403
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
404
|
+
const paramDesc = p.description ? escapeXml(p.description) : `The ${humanize(p.name)}.`;
|
|
405
|
+
lines.push(` /// <param name="${localName(p.name)}">${paramDesc}</param>`);
|
|
406
|
+
}
|
|
407
|
+
if (hasBearerOverride && bearerParamName) {
|
|
408
|
+
lines.push(` /// <param name="${localName(bearerParamName)}">The bearer token for authentication.</param>`);
|
|
409
|
+
}
|
|
410
|
+
if (optionsClass) {
|
|
411
|
+
lines.push(` /// <param name="options">Request options.</param>`);
|
|
412
|
+
}
|
|
413
|
+
if (!isUrlBuilder) {
|
|
414
|
+
lines.push(` /// <param name="requestOptions">Per-request configuration overrides.</param>`);
|
|
415
|
+
lines.push(` /// <param name="cancellationToken">Cancellation token.</param>`);
|
|
416
|
+
}
|
|
417
|
+
if (isUrlBuilder) {
|
|
418
|
+
lines.push(` /// <returns>The fully-qualified URL for the caller to redirect to.</returns>`);
|
|
419
|
+
} else if (isPaginated && op.pagination) {
|
|
420
|
+
const itemType = resolveListItemType(op.pagination.itemType, ctx);
|
|
421
|
+
lines.push(` /// <returns>A page of <see cref="${itemType}"/> results.</returns>`);
|
|
422
|
+
} else if (plan.responseModelName) {
|
|
423
|
+
const respType = className(plan.responseModelName);
|
|
424
|
+
lines.push(` /// <returns>The <see cref="${respType}"/> result.</returns>`);
|
|
425
|
+
}
|
|
426
|
+
if (op.deprecated) {
|
|
427
|
+
const msg = escapeCsAttributeString(deprecationMessage(op.description, 'operation'));
|
|
428
|
+
lines.push(` [System.Obsolete("${msg}")]`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Method signature
|
|
432
|
+
const params: string[] = [];
|
|
433
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
434
|
+
params.push(`string ${localName(p.name)}`);
|
|
435
|
+
}
|
|
436
|
+
if (hasBearerOverride && bearerParamName) {
|
|
437
|
+
params.push(`string ${localName(bearerParamName)}`);
|
|
438
|
+
}
|
|
439
|
+
if (optionsClass) {
|
|
440
|
+
const isRequired = hasVisibleBodyFields && !isPaginated;
|
|
441
|
+
params.push(isRequired ? `${optionsClass} options` : `${optionsClass}? options = null`);
|
|
442
|
+
}
|
|
443
|
+
if (!isUrlBuilder) {
|
|
444
|
+
params.push('RequestOptions? requestOptions = null');
|
|
445
|
+
params.push('CancellationToken cancellationToken = default');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const asyncKeyword = isUrlBuilder ? '' : 'async ';
|
|
449
|
+
lines.push(` public virtual ${asyncKeyword}${returnType} ${method}(${params.join(', ')})`);
|
|
450
|
+
lines.push(' {');
|
|
451
|
+
|
|
452
|
+
// Inject hidden params
|
|
453
|
+
if (hasHidden && optionsClass) {
|
|
454
|
+
const isOptionalParam = !hasVisibleBodyFields || isPaginated;
|
|
455
|
+
if (isOptionalParam) {
|
|
456
|
+
lines.push(` options ??= new ${optionsClass}();`);
|
|
457
|
+
}
|
|
458
|
+
const defaults = getOpDefaults(resolvedOp);
|
|
459
|
+
const inferFromClient = getOpInferFromClient(resolvedOp);
|
|
460
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
461
|
+
lines.push(` options.${fieldName(key)} = ${csLiteral(value as string | number | boolean)};`);
|
|
462
|
+
}
|
|
463
|
+
for (const field of inferFromClient) {
|
|
464
|
+
if (field === 'client_id') {
|
|
465
|
+
lines.push(` options.${fieldName(field)} = this.Client.RequireClientId();`);
|
|
466
|
+
} else {
|
|
467
|
+
lines.push(
|
|
468
|
+
` options.${fieldName(field)} = this.Client.${clientFieldExpression(field)} ?? string.Empty;`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Build path
|
|
475
|
+
const pathExpr = buildPathExpr(op);
|
|
476
|
+
|
|
477
|
+
// URL-builders and bearer-override operations keep the inlined WorkOSRequest
|
|
478
|
+
// form because the Service helpers don't expose BuildRequestUri or
|
|
479
|
+
// AccessToken configuration. Everything else uses the helper one-liners.
|
|
480
|
+
const needsInlineRequest = isUrlBuilder || (hasBearerOverride && !!bearerParamName);
|
|
481
|
+
const optionsArg = optionsClass ? 'options' : 'null';
|
|
482
|
+
|
|
483
|
+
if (needsInlineRequest) {
|
|
484
|
+
lines.push(' var request = new WorkOSRequest');
|
|
485
|
+
lines.push(' {');
|
|
486
|
+
lines.push(` Method = HttpMethod.${httpMethodCs(op.httpMethod)},`);
|
|
487
|
+
lines.push(` Path = ${pathExpr},`);
|
|
488
|
+
if (optionsClass) {
|
|
489
|
+
lines.push(' Options = options,');
|
|
490
|
+
}
|
|
491
|
+
if (hasBearerOverride && bearerParamName) {
|
|
492
|
+
lines.push(` AccessToken = ${localName(bearerParamName)},`);
|
|
493
|
+
}
|
|
494
|
+
if (!isUrlBuilder) {
|
|
495
|
+
lines.push(` RequestOptions = requestOptions,`);
|
|
496
|
+
}
|
|
497
|
+
lines.push(' };');
|
|
498
|
+
|
|
499
|
+
if (isUrlBuilder) {
|
|
500
|
+
lines.push(' return this.Client.BuildRequestUri(request).ToString();');
|
|
501
|
+
} else if (returnType.startsWith('Task<')) {
|
|
502
|
+
const innerType = returnType.slice(5, -1);
|
|
503
|
+
lines.push(` return await this.Client.MakeAPIRequest<${innerType}>(request, cancellationToken);`);
|
|
504
|
+
} else {
|
|
505
|
+
lines.push(' await this.Client.MakeRawAPIRequest(request, cancellationToken);');
|
|
506
|
+
}
|
|
507
|
+
} else if (isDelete) {
|
|
508
|
+
lines.push(` await this.DeleteAsync(${pathExpr}, ${optionsArg}, requestOptions, cancellationToken);`);
|
|
509
|
+
} else if (returnType.startsWith('Task<')) {
|
|
510
|
+
const innerType = returnType.slice(5, -1);
|
|
511
|
+
const helper = httpMethodHelperName(op.httpMethod);
|
|
512
|
+
lines.push(
|
|
513
|
+
` return await this.${helper}<${innerType}>(${pathExpr}, ${optionsArg}, requestOptions, cancellationToken);`,
|
|
514
|
+
);
|
|
515
|
+
} else {
|
|
516
|
+
const helper = httpMethodHelperName(op.httpMethod);
|
|
517
|
+
lines.push(
|
|
518
|
+
` await this.${helper}<object>(${pathExpr}, ${optionsArg}, requestOptions, cancellationToken);`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
lines.push(' }');
|
|
523
|
+
return lines.join('\n');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function generateAutoPagingMethod(
|
|
527
|
+
mountName: string,
|
|
528
|
+
method: string,
|
|
529
|
+
op: Operation,
|
|
530
|
+
plan: OperationPlan,
|
|
531
|
+
ctx: EmitterContext,
|
|
532
|
+
resolvedOp?: ResolvedOperation,
|
|
533
|
+
): string {
|
|
534
|
+
const lines: string[] = [];
|
|
535
|
+
const hidden = buildHiddenParams(resolvedOp);
|
|
536
|
+
const hasVisibleQueryParams = op.queryParams.filter((qp) => !hidden.has(qp.name)).length > 0;
|
|
537
|
+
|
|
538
|
+
let hasVisibleBodyFields = false;
|
|
539
|
+
if (plan.hasBody && op.requestBody?.kind === 'model') {
|
|
540
|
+
const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
|
|
541
|
+
if (bodyModel) hasVisibleBodyFields = bodyModel.fields.some((f) => !hidden.has(f.name));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const hasParams = hasVisibleBodyFields || hasVisibleQueryParams;
|
|
545
|
+
const optionsClass = hasParams ? optionsClassName(mountName, method) : null;
|
|
546
|
+
|
|
547
|
+
const itemType = resolveListItemType(op.pagination!.itemType, ctx);
|
|
548
|
+
|
|
549
|
+
// XML doc
|
|
550
|
+
lines.push(
|
|
551
|
+
` /// <summary>Auto-paging variant of <see cref="${method}"/>. Yields individual items across all pages.</summary>`,
|
|
552
|
+
);
|
|
553
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
554
|
+
const paramDesc = p.description ? escapeXml(p.description) : `The ${humanize(p.name)}.`;
|
|
555
|
+
lines.push(` /// <param name="${localName(p.name)}">${paramDesc}</param>`);
|
|
556
|
+
}
|
|
557
|
+
if (optionsClass) {
|
|
558
|
+
lines.push(` /// <param name="options">Request options.</param>`);
|
|
559
|
+
}
|
|
560
|
+
lines.push(` /// <param name="requestOptions">Per-request configuration overrides.</param>`);
|
|
561
|
+
lines.push(` /// <param name="cancellationToken">Cancellation token.</param>`);
|
|
562
|
+
lines.push(` /// <returns>An async sequence of <see cref="${itemType}"/> items.</returns>`);
|
|
563
|
+
|
|
564
|
+
// Signature
|
|
565
|
+
const params: string[] = [];
|
|
566
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
567
|
+
params.push(`string ${localName(p.name)}`);
|
|
568
|
+
}
|
|
569
|
+
if (optionsClass) {
|
|
570
|
+
params.push(`${optionsClass}? options = null`);
|
|
571
|
+
}
|
|
572
|
+
params.push('RequestOptions? requestOptions = null');
|
|
573
|
+
params.push('CancellationToken cancellationToken = default');
|
|
574
|
+
|
|
575
|
+
lines.push(` public virtual IAsyncEnumerable<${itemType}> ${method}AutoPagingAsync(${params.join(', ')})`);
|
|
576
|
+
lines.push(' {');
|
|
577
|
+
|
|
578
|
+
const pathExpr = buildPathExpr(op);
|
|
579
|
+
const optionsArg = optionsClass ? 'options' : 'null';
|
|
580
|
+
lines.push(
|
|
581
|
+
` return this.ListAutoPagingAsync<${itemType}>(${pathExpr}, ${optionsArg}, requestOptions, cancellationToken);`,
|
|
582
|
+
);
|
|
583
|
+
lines.push(' }');
|
|
584
|
+
|
|
585
|
+
return lines.join('\n');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function resolveCsMethodName(op: Operation, mountName: string, ctx: EmitterContext): string {
|
|
589
|
+
return resolveMethodName(op, { name: mountName, operations: [op] }, ctx);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function optionsClassName(mountName: string, method: string): string {
|
|
593
|
+
const prefix = className(mountName);
|
|
594
|
+
if (method.startsWith(prefix)) return `${method}Options`;
|
|
595
|
+
return `${prefix}${method}Options`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function buildPathExpr(op: Operation): string {
|
|
599
|
+
if (op.pathParams.length === 0) {
|
|
600
|
+
return `"${op.path}"`;
|
|
601
|
+
}
|
|
602
|
+
// Build C# string interpolation
|
|
603
|
+
let interpolated = op.path;
|
|
604
|
+
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
605
|
+
interpolated = interpolated.replace(`{${p.name}}`, `{${localName(p.name)}}`);
|
|
606
|
+
}
|
|
607
|
+
return `$"${interpolated}"`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function resolveListItemType(itemType: import('@workos/oagen').TypeRef, ctx: EmitterContext): string {
|
|
611
|
+
if (itemType.kind === 'model') {
|
|
612
|
+
const model = ctx.spec.models.find((m) => m.name === itemType.name);
|
|
613
|
+
if (model && isListWrapperModel(model)) {
|
|
614
|
+
const dataField = model.fields.find((f) => f.name === 'data');
|
|
615
|
+
if (dataField && dataField.type.kind === 'array' && dataField.type.items.kind === 'model') {
|
|
616
|
+
return className(dataField.type.items.name);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return className(itemType.name);
|
|
620
|
+
}
|
|
621
|
+
return mapTypeRef(itemType);
|
|
622
|
+
}
|