@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.
Files changed (128) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/lint.yml +1 -1
  3. package/.github/workflows/release-please.yml +2 -2
  4. package/.github/workflows/release.yml +1 -1
  5. package/.husky/pre-push +11 -0
  6. package/.node-version +1 -1
  7. package/.release-please-manifest.json +1 -1
  8. package/CHANGELOG.md +15 -0
  9. package/README.md +35 -224
  10. package/dist/index.d.mts +12 -1
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +2 -12737
  13. package/dist/plugin-BSop9f9z.mjs +21471 -0
  14. package/dist/plugin-BSop9f9z.mjs.map +1 -0
  15. package/dist/plugin.d.mts +7 -0
  16. package/dist/plugin.d.mts.map +1 -0
  17. package/dist/plugin.mjs +2 -0
  18. package/docs/sdk-architecture/dotnet.md +336 -0
  19. package/oagen.config.ts +5 -343
  20. package/package.json +10 -34
  21. package/smoke/sdk-dotnet.ts +45 -12
  22. package/src/dotnet/client.ts +89 -0
  23. package/src/dotnet/enums.ts +323 -0
  24. package/src/dotnet/fixtures.ts +236 -0
  25. package/src/dotnet/index.ts +248 -0
  26. package/src/dotnet/manifest.ts +36 -0
  27. package/src/dotnet/models.ts +320 -0
  28. package/src/dotnet/naming.ts +368 -0
  29. package/src/dotnet/resources.ts +943 -0
  30. package/src/dotnet/tests.ts +713 -0
  31. package/src/dotnet/type-map.ts +228 -0
  32. package/src/dotnet/wrappers.ts +197 -0
  33. package/src/go/client.ts +35 -3
  34. package/src/go/enums.ts +4 -0
  35. package/src/go/index.ts +15 -7
  36. package/src/go/models.ts +6 -1
  37. package/src/go/naming.ts +5 -17
  38. package/src/go/resources.ts +534 -73
  39. package/src/go/tests.ts +39 -3
  40. package/src/go/type-map.ts +8 -3
  41. package/src/go/wrappers.ts +79 -21
  42. package/src/index.ts +15 -0
  43. package/src/kotlin/client.ts +58 -0
  44. package/src/kotlin/enums.ts +189 -0
  45. package/src/kotlin/index.ts +92 -0
  46. package/src/kotlin/manifest.ts +55 -0
  47. package/src/kotlin/models.ts +486 -0
  48. package/src/kotlin/naming.ts +229 -0
  49. package/src/kotlin/overrides.ts +25 -0
  50. package/src/kotlin/resources.ts +998 -0
  51. package/src/kotlin/tests.ts +1133 -0
  52. package/src/kotlin/type-map.ts +123 -0
  53. package/src/kotlin/wrappers.ts +168 -0
  54. package/src/node/client.ts +84 -7
  55. package/src/node/field-plan.ts +12 -14
  56. package/src/node/fixtures.ts +39 -3
  57. package/src/node/index.ts +1 -0
  58. package/src/node/models.ts +281 -37
  59. package/src/node/resources.ts +319 -95
  60. package/src/node/tests.ts +108 -29
  61. package/src/node/type-map.ts +1 -31
  62. package/src/node/utils.ts +96 -6
  63. package/src/node/wrappers.ts +31 -1
  64. package/src/php/client.ts +11 -3
  65. package/src/php/models.ts +0 -33
  66. package/src/php/naming.ts +2 -21
  67. package/src/php/resources.ts +275 -19
  68. package/src/php/tests.ts +118 -18
  69. package/src/php/type-map.ts +16 -2
  70. package/src/php/wrappers.ts +7 -2
  71. package/src/plugin.ts +50 -0
  72. package/src/python/client.ts +50 -32
  73. package/src/python/enums.ts +35 -10
  74. package/src/python/index.ts +35 -27
  75. package/src/python/models.ts +139 -2
  76. package/src/python/naming.ts +2 -22
  77. package/src/python/resources.ts +234 -17
  78. package/src/python/tests.ts +260 -16
  79. package/src/python/type-map.ts +16 -2
  80. package/src/ruby/client.ts +238 -0
  81. package/src/ruby/enums.ts +149 -0
  82. package/src/ruby/index.ts +93 -0
  83. package/src/ruby/manifest.ts +35 -0
  84. package/src/ruby/models.ts +360 -0
  85. package/src/ruby/naming.ts +187 -0
  86. package/src/ruby/rbi.ts +313 -0
  87. package/src/ruby/resources.ts +799 -0
  88. package/src/ruby/tests.ts +459 -0
  89. package/src/ruby/type-map.ts +97 -0
  90. package/src/ruby/wrappers.ts +161 -0
  91. package/src/shared/model-utils.ts +357 -16
  92. package/src/shared/naming-utils.ts +83 -0
  93. package/src/shared/non-spec-services.ts +13 -0
  94. package/src/shared/resolved-ops.ts +75 -1
  95. package/src/shared/wrapper-utils.ts +12 -1
  96. package/test/dotnet/client.test.ts +121 -0
  97. package/test/dotnet/enums.test.ts +193 -0
  98. package/test/dotnet/errors.test.ts +9 -0
  99. package/test/dotnet/manifest.test.ts +82 -0
  100. package/test/dotnet/models.test.ts +258 -0
  101. package/test/dotnet/resources.test.ts +387 -0
  102. package/test/dotnet/tests.test.ts +202 -0
  103. package/test/entrypoint.test.ts +89 -0
  104. package/test/go/client.test.ts +6 -6
  105. package/test/go/resources.test.ts +156 -7
  106. package/test/kotlin/models.test.ts +135 -0
  107. package/test/kotlin/resources.test.ts +210 -0
  108. package/test/kotlin/tests.test.ts +176 -0
  109. package/test/node/client.test.ts +74 -0
  110. package/test/node/models.test.ts +134 -1
  111. package/test/node/resources.test.ts +343 -34
  112. package/test/node/utils.test.ts +140 -0
  113. package/test/php/client.test.ts +2 -1
  114. package/test/php/models.test.ts +5 -4
  115. package/test/php/resources.test.ts +103 -0
  116. package/test/php/tests.test.ts +67 -0
  117. package/test/plugin.test.ts +50 -0
  118. package/test/python/client.test.ts +56 -0
  119. package/test/python/models.test.ts +99 -0
  120. package/test/python/resources.test.ts +294 -0
  121. package/test/python/tests.test.ts +91 -0
  122. package/test/ruby/client.test.ts +81 -0
  123. package/test/ruby/resources.test.ts +386 -0
  124. package/test/shared/resolved-ops.test.ts +122 -0
  125. package/tsdown.config.ts +1 -1
  126. package/dist/index.mjs.map +0 -1
  127. package/scripts/generate-php.js +0 -13
  128. 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
+ }