@workos/oagen-emitters 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lint.yml +1 -1
- package/.github/workflows/release-please.yml +2 -2
- package/.github/workflows/release.yml +1 -1
- package/.husky/pre-push +11 -0
- package/.node-version +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +35 -224
- package/dist/index.d.mts +9 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -15234
- package/dist/plugin-BSop9f9z.mjs +21471 -0
- package/dist/plugin-BSop9f9z.mjs.map +1 -0
- package/dist/plugin.d.mts +7 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +2 -0
- package/docs/sdk-architecture/dotnet.md +5 -5
- package/oagen.config.ts +5 -373
- package/package.json +10 -34
- package/src/dotnet/index.ts +6 -4
- package/src/dotnet/models.ts +58 -82
- package/src/dotnet/naming.ts +44 -6
- package/src/dotnet/resources.ts +350 -29
- package/src/dotnet/tests.ts +44 -24
- package/src/dotnet/type-map.ts +44 -17
- package/src/dotnet/wrappers.ts +21 -10
- package/src/go/client.ts +35 -3
- package/src/go/enums.ts +4 -0
- package/src/go/index.ts +10 -5
- package/src/go/models.ts +6 -1
- package/src/go/resources.ts +534 -73
- package/src/go/tests.ts +39 -3
- package/src/go/type-map.ts +8 -3
- package/src/go/wrappers.ts +79 -21
- package/src/index.ts +14 -0
- package/src/kotlin/client.ts +7 -2
- package/src/kotlin/enums.ts +30 -3
- package/src/kotlin/models.ts +97 -6
- package/src/kotlin/naming.ts +7 -1
- package/src/kotlin/resources.ts +370 -39
- package/src/kotlin/tests.ts +120 -6
- package/src/node/client.ts +38 -11
- package/src/node/field-plan.ts +12 -14
- package/src/node/fixtures.ts +39 -3
- package/src/node/models.ts +281 -37
- package/src/node/resources.ts +156 -52
- package/src/node/tests.ts +76 -27
- package/src/node/type-map.ts +1 -31
- package/src/node/utils.ts +96 -6
- package/src/node/wrappers.ts +31 -1
- package/src/php/models.ts +0 -33
- package/src/php/resources.ts +199 -18
- package/src/php/tests.ts +26 -2
- package/src/php/type-map.ts +16 -2
- package/src/php/wrappers.ts +6 -2
- package/src/plugin.ts +50 -0
- package/src/python/client.ts +13 -3
- package/src/python/enums.ts +28 -3
- package/src/python/index.ts +35 -27
- package/src/python/models.ts +138 -1
- package/src/python/resources.ts +234 -17
- package/src/python/tests.ts +260 -16
- package/src/python/type-map.ts +16 -2
- package/src/ruby/client.ts +238 -0
- package/src/ruby/enums.ts +149 -0
- package/src/ruby/index.ts +93 -0
- package/src/ruby/manifest.ts +35 -0
- package/src/ruby/models.ts +360 -0
- package/src/ruby/naming.ts +187 -0
- package/src/ruby/rbi.ts +313 -0
- package/src/ruby/resources.ts +799 -0
- package/src/ruby/tests.ts +459 -0
- package/src/ruby/type-map.ts +97 -0
- package/src/ruby/wrappers.ts +161 -0
- package/src/shared/model-utils.ts +131 -7
- package/src/shared/naming-utils.ts +36 -0
- package/src/shared/non-spec-services.ts +13 -0
- package/src/shared/resolved-ops.ts +75 -1
- package/test/dotnet/client.test.ts +2 -2
- package/test/dotnet/models.test.ts +7 -9
- package/test/dotnet/resources.test.ts +135 -3
- package/test/dotnet/tests.test.ts +5 -5
- package/test/entrypoint.test.ts +89 -0
- package/test/go/client.test.ts +6 -6
- package/test/go/resources.test.ts +156 -7
- package/test/kotlin/models.test.ts +1 -1
- package/test/kotlin/resources.test.ts +210 -0
- package/test/node/models.test.ts +134 -1
- package/test/node/resources.test.ts +134 -26
- package/test/node/utils.test.ts +140 -0
- package/test/php/models.test.ts +5 -4
- package/test/php/resources.test.ts +66 -1
- package/test/plugin.test.ts +50 -0
- package/test/python/client.test.ts +56 -0
- package/test/python/models.test.ts +99 -0
- package/test/python/resources.test.ts +294 -0
- package/test/python/tests.test.ts +91 -0
- package/test/ruby/client.test.ts +81 -0
- package/test/ruby/resources.test.ts +386 -0
- package/test/shared/resolved-ops.test.ts +122 -0
- package/tsdown.config.ts +1 -1
- package/dist/index.mjs.map +0 -1
- package/scripts/generate-php.js +0 -13
- package/scripts/git-push-with-published-oagen.sh +0 -21
package/src/kotlin/resources.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
ktLiteral,
|
|
23
23
|
clientFieldExpression,
|
|
24
24
|
escapeReserved,
|
|
25
|
+
humanize,
|
|
25
26
|
} from './naming.js';
|
|
26
27
|
import {
|
|
27
28
|
buildResolvedLookup,
|
|
@@ -30,6 +31,8 @@ import {
|
|
|
30
31
|
buildHiddenParams,
|
|
31
32
|
getOpDefaults,
|
|
32
33
|
getOpInferFromClient,
|
|
34
|
+
collectGroupedParamNames,
|
|
35
|
+
collectBodyFieldTypes,
|
|
33
36
|
} from '../shared/resolved-ops.js';
|
|
34
37
|
import { generateWrapperMethods } from './wrappers.js';
|
|
35
38
|
import { resolveWrapperParams } from '../shared/wrapper-utils.js';
|
|
@@ -83,6 +86,15 @@ function generateApiClass(
|
|
|
83
86
|
|
|
84
87
|
const body: string[] = [];
|
|
85
88
|
const seenMethods = new Set<string>();
|
|
89
|
+
const hasAuthenticateHelper = operations.some(
|
|
90
|
+
(op) => op.path === '/user_management/authenticate' && op.httpMethod.toUpperCase() === 'POST',
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (hasAuthenticateHelper) {
|
|
94
|
+
imports.add('com.workos.common.http.bodyOf');
|
|
95
|
+
imports.add('com.workos.models.AuthenticateResponse');
|
|
96
|
+
body.push(...generateAuthenticateHelper());
|
|
97
|
+
}
|
|
86
98
|
|
|
87
99
|
for (const op of operations) {
|
|
88
100
|
if (isHandwrittenOverride(op)) continue;
|
|
@@ -118,13 +130,43 @@ function generateApiClass(
|
|
|
118
130
|
|
|
119
131
|
if (body.length === 0) return null;
|
|
120
132
|
|
|
121
|
-
//
|
|
122
|
-
|
|
133
|
+
// Emit sealed classes for parameter groups before the API class.
|
|
134
|
+
// Parameter-group IR can lose body field type fidelity; prefer the request
|
|
135
|
+
// body model's field type when available.
|
|
136
|
+
const bodyFieldTypes = new Map<string, TypeRef>();
|
|
137
|
+
for (const op of operations) {
|
|
138
|
+
for (const [name, type] of collectBodyFieldTypes(op, ctx.spec.models)) {
|
|
139
|
+
bodyFieldTypes.set(name, type);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const sealedLines: string[] = [];
|
|
143
|
+
const emittedSealedClasses = new Set<string>();
|
|
144
|
+
for (const op of operations) {
|
|
145
|
+
if ((op.parameterGroups?.length ?? 0) > 0) {
|
|
146
|
+
for (const group of op.parameterGroups ?? []) {
|
|
147
|
+
// Register imports for types used in parameter group sealed classes.
|
|
148
|
+
// The body field type override may introduce enum/model types that
|
|
149
|
+
// the original IR parameter didn't reference.
|
|
150
|
+
for (const variant of group.variants) {
|
|
151
|
+
for (const p of variant.parameters) {
|
|
152
|
+
const effectiveType = bodyFieldTypes.get(p.name) ?? p.type;
|
|
153
|
+
registerTypeImports(effectiveType, imports, ctx);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (emittedSealedClasses.has(group.name)) continue;
|
|
157
|
+
emittedSealedClasses.add(group.name);
|
|
158
|
+
for (const line of generateSealedClass(group, bodyFieldTypes)) sealedLines.push(line);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Drop unused imports by peeking at the body text and sealed class text.
|
|
164
|
+
const allText = body.join('\n') + '\n' + sealedLines.join('\n');
|
|
123
165
|
const filteredImports = [...imports].filter((imp) => {
|
|
124
166
|
const simple = imp.slice(imp.lastIndexOf('.') + 1);
|
|
125
167
|
// Skip the import if the class body never references the simple name.
|
|
126
|
-
if (simple === 'WorkOS' || simple === '
|
|
127
|
-
return new RegExp(`\\b${simple}\\b`).test(
|
|
168
|
+
if (simple === 'WorkOS' || simple === 'RequestOptions') return true;
|
|
169
|
+
return new RegExp(`\\b${simple}\\b`).test(allText);
|
|
128
170
|
});
|
|
129
171
|
|
|
130
172
|
const lines: string[] = [];
|
|
@@ -132,6 +174,8 @@ function generateApiClass(
|
|
|
132
174
|
lines.push('');
|
|
133
175
|
for (const imp of filteredImports.sort()) lines.push(`import ${imp}`);
|
|
134
176
|
lines.push('');
|
|
177
|
+
for (const line of sealedLines) lines.push(line);
|
|
178
|
+
|
|
135
179
|
const serviceDescription = resolveServiceDescription(ctx, mountName, operations);
|
|
136
180
|
if (serviceDescription) {
|
|
137
181
|
const docLines = serviceDescription.trim().split('\n');
|
|
@@ -197,9 +241,13 @@ function renderMethod(
|
|
|
197
241
|
|
|
198
242
|
const httpMethod = op.httpMethod.toUpperCase();
|
|
199
243
|
const pathParams = sortPathParamsByTemplateOrder(op);
|
|
200
|
-
const
|
|
244
|
+
const groupedParamNames = collectGroupedParamNames(op);
|
|
245
|
+
const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
|
|
246
|
+
const queryParams = op.queryParams.filter((p) => !hidden.has(p.name) && !groupedParamNames.has(p.name));
|
|
201
247
|
const bodyModel = resolveBodyModel(op, ctx);
|
|
202
|
-
const bodyFields = bodyModel
|
|
248
|
+
const bodyFields = bodyModel
|
|
249
|
+
? bodyModel.fields.filter((f) => !hidden.has(f.name) && !groupedParamNames.has(f.name))
|
|
250
|
+
: [];
|
|
203
251
|
|
|
204
252
|
// Track imports we need
|
|
205
253
|
for (const p of [...pathParams, ...queryParams]) registerTypeImports(p.type, imports, ctx);
|
|
@@ -222,11 +270,21 @@ function renderMethod(
|
|
|
222
270
|
const uniqueQuery = queryParams.filter((qp) => !paramNames.has(propertyName(qp.name)));
|
|
223
271
|
for (const qp of uniqueQuery) paramNames.add(propertyName(qp.name));
|
|
224
272
|
|
|
273
|
+
const sharedQueryBodyParams = new Set(
|
|
274
|
+
uniqueQuery
|
|
275
|
+
.filter((qp) => bodyFields.some((bf) => bf.name === qp.name && mapTypeRef(qp.type) === mapTypeRef(bf.type)))
|
|
276
|
+
.map((qp) => qp.name),
|
|
277
|
+
);
|
|
278
|
+
|
|
225
279
|
// Map body field wire name → Kotlin parameter name. When the natural name
|
|
226
280
|
// collides with a path/query, prefix with `body` (e.g. slug → bodySlug).
|
|
227
281
|
const bodyParamNames = new Map<string, string>();
|
|
228
282
|
for (const bf of bodyFields) {
|
|
229
283
|
const natural = propertyName(bf.name);
|
|
284
|
+
if (sharedQueryBodyParams.has(bf.name)) {
|
|
285
|
+
bodyParamNames.set(bf.name, natural);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
230
288
|
if (paramNames.has(natural)) {
|
|
231
289
|
const renamed = `body${natural.charAt(0).toUpperCase()}${natural.slice(1)}`;
|
|
232
290
|
bodyParamNames.set(bf.name, renamed);
|
|
@@ -237,12 +295,25 @@ function renderMethod(
|
|
|
237
295
|
}
|
|
238
296
|
}
|
|
239
297
|
|
|
298
|
+
const groupParamNames = assignGroupParameterNames(op, paramNames);
|
|
299
|
+
|
|
240
300
|
const params: string[] = [];
|
|
241
301
|
for (const pp of pathParams) params.push(` ${propertyName(pp.name)}: String`);
|
|
242
302
|
|
|
243
303
|
const sortedQuery = [...uniqueQuery].sort((a, b) => (a.required === b.required ? 0 : a.required ? -1 : 1));
|
|
244
304
|
for (const qp of sortedQuery) {
|
|
245
|
-
params.push(renderParam(qp.name, qp.type, qp.required));
|
|
305
|
+
params.push(renderParam(qp.name, qp.type, qp.required, method.startsWith('list') && qp.name === 'limit'));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Parameter group params (sealed class types)
|
|
309
|
+
for (const group of op.parameterGroups ?? []) {
|
|
310
|
+
const sealedName = sealedGroupName(group.name);
|
|
311
|
+
const prop = groupParamNames.get(group.name)!;
|
|
312
|
+
if (group.optional) {
|
|
313
|
+
params.push(` ${prop}: ${sealedName}? = null`);
|
|
314
|
+
} else {
|
|
315
|
+
params.push(` ${prop}: ${sealedName}`);
|
|
316
|
+
}
|
|
246
317
|
}
|
|
247
318
|
|
|
248
319
|
// PATCH operations use PatchField<T> for optional body fields so callers
|
|
@@ -251,6 +322,7 @@ function renderMethod(
|
|
|
251
322
|
|
|
252
323
|
const sortedBodyFields = [...bodyFields].sort((a, b) => (a.required === b.required ? 0 : a.required ? -1 : 1));
|
|
253
324
|
for (const bf of sortedBodyFields) {
|
|
325
|
+
if (sharedQueryBodyParams.has(bf.name)) continue;
|
|
254
326
|
if (isPatch && !bf.required) {
|
|
255
327
|
const baseType = mapTypeRef(bf.type);
|
|
256
328
|
imports.add('com.workos.common.http.PatchField');
|
|
@@ -304,36 +376,75 @@ function renderMethod(
|
|
|
304
376
|
const appendDefaultsAsQuery = !hasBody && (Object.keys(defaults).length > 0 || inferFromClient.length > 0);
|
|
305
377
|
const pathExpr = buildPathExpression(op.path, pathParams);
|
|
306
378
|
|
|
379
|
+
if (
|
|
380
|
+
op.path === '/user_management/authenticate' &&
|
|
381
|
+
httpMethod === 'POST' &&
|
|
382
|
+
plan.responseModelName === 'AuthenticateResponse'
|
|
383
|
+
) {
|
|
384
|
+
imports.add('com.workos.models.AuthenticateResponse');
|
|
385
|
+
const grantType = defaults.grant_type ?? 'authorization_code';
|
|
386
|
+
const entryLines = sortedBodyFields
|
|
387
|
+
.filter((bf) => bf.name !== 'grant_type' && bf.name !== 'client_id' && bf.name !== 'client_secret')
|
|
388
|
+
.map((bf) => ` ${ktLiteral(bf.name)} to ${bodyParamNames.get(bf.name)!}`);
|
|
389
|
+
lines.push(` return authenticate(`);
|
|
390
|
+
lines.push(` grantType = ${ktLiteral(grantType)},`);
|
|
391
|
+
lines.push(` requestOptions = requestOptions,`);
|
|
392
|
+
for (let i = 0; i < entryLines.length; i++) {
|
|
393
|
+
const suffix = i === entryLines.length - 1 ? '' : ',';
|
|
394
|
+
lines.push(`${entryLines[i]}${suffix}`);
|
|
395
|
+
}
|
|
396
|
+
lines.push(` )`);
|
|
397
|
+
lines.push(' }');
|
|
398
|
+
return lines.join('\n');
|
|
399
|
+
}
|
|
400
|
+
|
|
307
401
|
if (isPaginated) {
|
|
308
402
|
// Nested helper function + requestPage call; 'after' is owned by the
|
|
309
403
|
// cursor logic so we skip it in the generic query loop.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
lines.push(` method = ${ktLiteral(httpMethod)},`);
|
|
318
|
-
lines.push(` path = ${pathExpr},`);
|
|
319
|
-
lines.push(` queryParams = params,`);
|
|
320
|
-
lines.push(` requestOptions = requestOptions`);
|
|
321
|
-
lines.push(` )`);
|
|
322
|
-
lines.push(` }`);
|
|
404
|
+
// 'after' and 'before' are owned by the cursor logic. 'before' is only
|
|
405
|
+
// included on the first page — re-sending it on follow-up pages (where
|
|
406
|
+
// afterCursor is set by the pagination engine) is nonsensical and can
|
|
407
|
+
// cause empty or looping results from the server.
|
|
408
|
+
imports.add('com.workos.common.http.addIfNotNull');
|
|
409
|
+
imports.add('com.workos.common.http.addJoinedIfNotNull');
|
|
410
|
+
imports.add('com.workos.common.http.addEach');
|
|
323
411
|
const itemClass = className(paginatedItemName!);
|
|
324
412
|
lines.push(` val itemType = object : TypeReference<${itemClass}>() {}`);
|
|
325
|
-
lines.push(
|
|
326
|
-
|
|
327
|
-
);
|
|
413
|
+
lines.push(` return workos.baseClient.requestPage(`);
|
|
414
|
+
lines.push(` method = ${ktLiteral(httpMethod)},`);
|
|
415
|
+
lines.push(` path = ${pathExpr},`);
|
|
416
|
+
lines.push(` itemType = itemType,`);
|
|
417
|
+
lines.push(` requestOptions = requestOptions,`);
|
|
418
|
+
lines.push(` before = ${pickNamedQueryParam(sortedQuery, 'before')},`);
|
|
419
|
+
lines.push(` after = ${pickNamedQueryParam(sortedQuery, 'after')}`);
|
|
420
|
+
lines.push(` ) {`);
|
|
421
|
+
lines.push(` val params = this`);
|
|
422
|
+
for (const qp of sortedQuery.filter((p) => p.name !== 'after' && p.name !== 'before')) {
|
|
423
|
+
for (const ln of emitQueryParam(qp, ' ')) lines.push(ln);
|
|
424
|
+
}
|
|
425
|
+
for (const group of op.parameterGroups ?? []) {
|
|
426
|
+
for (const ln of emitGroupQueryDispatch(group, groupParamNames.get(group.name)!, ' ')) lines.push(ln);
|
|
427
|
+
}
|
|
428
|
+
lines.push(` }`);
|
|
328
429
|
} else {
|
|
329
430
|
// Only emit the `params` local when the method actually contributes
|
|
330
431
|
// query parameters (spec-declared query, or defaults/inferFromClient
|
|
331
432
|
// for GET/DELETE without a body). `RequestConfig.queryParams` defaults
|
|
332
433
|
// to `emptyList()` when omitted, so we avoid dead local declarations.
|
|
333
|
-
|
|
434
|
+
// Groups go to the body for POST/PUT/PATCH (hasBody), query otherwise.
|
|
435
|
+
const groupsGoToQuery = hasGroups && !hasBody;
|
|
436
|
+
const emitsQueryParams = sortedQuery.length > 0 || appendDefaultsAsQuery || groupsGoToQuery;
|
|
334
437
|
if (emitsQueryParams) {
|
|
438
|
+
imports.add('com.workos.common.http.addIfNotNull');
|
|
439
|
+
imports.add('com.workos.common.http.addJoinedIfNotNull');
|
|
440
|
+
imports.add('com.workos.common.http.addEach');
|
|
335
441
|
lines.push(` val params = mutableListOf<Pair<String, String>>()`);
|
|
336
442
|
for (const qp of sortedQuery) for (const ln of emitQueryParam(qp, ' ')) lines.push(ln);
|
|
443
|
+
if (groupsGoToQuery) {
|
|
444
|
+
for (const group of op.parameterGroups ?? []) {
|
|
445
|
+
for (const ln of emitGroupQueryDispatch(group, groupParamNames.get(group.name)!, ' ')) lines.push(ln);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
337
448
|
if (appendDefaultsAsQuery) {
|
|
338
449
|
for (const [k, v] of Object.entries(defaults)) lines.push(` params += ${ktLiteral(k)} to ${ktLiteral(v)}`);
|
|
339
450
|
// Client-inferred fields may be nullable (e.g. clientId). Skip when
|
|
@@ -374,6 +485,15 @@ function renderMethod(
|
|
|
374
485
|
// Empty body (POST/PUT/PATCH still require one for OkHttp).
|
|
375
486
|
lines.push(` val body = linkedMapOf<String, Any?>()`);
|
|
376
487
|
}
|
|
488
|
+
// Parameter group values go into the body for POST/PUT/PATCH so
|
|
489
|
+
// sensitive fields (passwords, role slugs) never leak into the URL.
|
|
490
|
+
if (hasGroups) {
|
|
491
|
+
for (const group of op.parameterGroups ?? []) {
|
|
492
|
+
for (const ln of emitGroupBodyDispatch(group, groupParamNames.get(group.name)!, ' ')) {
|
|
493
|
+
lines.push(ln);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
377
497
|
lines.push(` val config =`);
|
|
378
498
|
lines.push(` RequestConfig(`);
|
|
379
499
|
lines.push(` method = ${ktLiteral(httpMethod)},`);
|
|
@@ -449,12 +569,12 @@ function resolvePaginatedItemName(name: string | null, ctx?: EmitterContext): st
|
|
|
449
569
|
return name;
|
|
450
570
|
}
|
|
451
571
|
|
|
452
|
-
function renderParam(name: string, type: TypeRef, required: boolean): string {
|
|
453
|
-
return renderParamNamed(propertyName(name), type, required);
|
|
572
|
+
function renderParam(name: string, type: TypeRef, required: boolean, forceInt = false): string {
|
|
573
|
+
return renderParamNamed(propertyName(name), type, required, forceInt);
|
|
454
574
|
}
|
|
455
575
|
|
|
456
|
-
function renderParamNamed(kotlinName: string, type: TypeRef, required: boolean): string {
|
|
457
|
-
const mapped = required ? mapTypeRef(type) : mapTypeRefOptional(type);
|
|
576
|
+
function renderParamNamed(kotlinName: string, type: TypeRef, required: boolean, forceInt = false): string {
|
|
577
|
+
const mapped = forceInt ? (required ? 'Int' : 'Int?') : required ? mapTypeRef(type) : mapTypeRefOptional(type);
|
|
458
578
|
return required ? ` ${kotlinName}: ${mapped}` : ` ${kotlinName}: ${mapped} = null`;
|
|
459
579
|
}
|
|
460
580
|
|
|
@@ -485,19 +605,25 @@ function buildMethodKdoc(
|
|
|
485
605
|
// @param entry even without a description so the deprecation note is
|
|
486
606
|
// surfaced in the docs.
|
|
487
607
|
const paramDocs: string[] = [];
|
|
608
|
+
const seenParamDocs = new Set<string>();
|
|
609
|
+
const pushParamDoc = (name: string, description: string | undefined, deprecated?: boolean) => {
|
|
610
|
+
if (seenParamDocs.has(name)) return;
|
|
611
|
+
seenParamDocs.add(name);
|
|
612
|
+
paramDocs.push(formatParamDoc(name, description, deprecated));
|
|
613
|
+
};
|
|
488
614
|
for (const pp of pathParams) {
|
|
489
615
|
if (pp.description?.trim() || pp.deprecated) {
|
|
490
|
-
|
|
616
|
+
pushParamDoc(propertyName(pp.name), pp.description, pp.deprecated);
|
|
491
617
|
}
|
|
492
618
|
}
|
|
493
619
|
for (const qp of queryParams) {
|
|
494
620
|
if (qp.description?.trim() || qp.deprecated) {
|
|
495
|
-
|
|
621
|
+
pushParamDoc(propertyName(qp.name), qp.description, qp.deprecated);
|
|
496
622
|
}
|
|
497
623
|
}
|
|
498
624
|
for (const bf of bodyFields) {
|
|
499
625
|
if (bf.description?.trim() || bf.deprecated) {
|
|
500
|
-
|
|
626
|
+
pushParamDoc(bodyParamNames.get(bf.name)!, bf.description, bf.deprecated);
|
|
501
627
|
}
|
|
502
628
|
}
|
|
503
629
|
|
|
@@ -544,17 +670,20 @@ function unwrapArray(t: TypeRef): TypeRef | null {
|
|
|
544
670
|
|
|
545
671
|
/**
|
|
546
672
|
* Serialize a single value expression for a query parameter. For enums we
|
|
547
|
-
* use `.value` so the wire name is used; for
|
|
673
|
+
* use `.value` so the wire name is used; for strings the value is already
|
|
674
|
+
* the right type; for everything else `.toString()`.
|
|
548
675
|
*/
|
|
549
676
|
function valueExprForQuery(type: TypeRef): string {
|
|
550
677
|
const inner = type.kind === 'nullable' ? type.inner : type;
|
|
551
678
|
if (inner.kind === 'enum') return 'it.value';
|
|
679
|
+
if (inner.kind === 'primitive' && inner.type === 'string') return 'it';
|
|
552
680
|
return 'it.toString()';
|
|
553
681
|
}
|
|
554
682
|
|
|
555
683
|
function emitQueryParam(p: Parameter, indent: string): string[] {
|
|
556
684
|
const prop = propertyName(p.name);
|
|
557
685
|
const rendered = queryParamToString(p.type, prop);
|
|
686
|
+
const inner = p.type.kind === 'nullable' ? p.type.inner : p.type;
|
|
558
687
|
const arrayItem = unwrapArray(p.type);
|
|
559
688
|
if (arrayItem) {
|
|
560
689
|
// Honor `style: form, explode: false` → comma-joined. Default (explode:true
|
|
@@ -564,24 +693,26 @@ function emitQueryParam(p: Parameter, indent: string): string[] {
|
|
|
564
693
|
const itemExpr = valueExprForQuery(arrayItem);
|
|
565
694
|
if (!explode) {
|
|
566
695
|
if (p.required) {
|
|
567
|
-
return [`${indent}params
|
|
696
|
+
return [`${indent}params.addJoinedIfNotNull(${ktLiteral(p.name)}, ${prop}.map { ${itemExpr} })`];
|
|
568
697
|
}
|
|
569
|
-
return [
|
|
570
|
-
`${indent}if (${prop} != null) params += ${ktLiteral(p.name)} to ${prop}.joinToString(",") { ${itemExpr} }`,
|
|
571
|
-
];
|
|
698
|
+
return [`${indent}params.addJoinedIfNotNull(${ktLiteral(p.name)}, ${prop}?.map { ${itemExpr} })`];
|
|
572
699
|
}
|
|
573
700
|
if (p.required) {
|
|
574
|
-
return [`${indent}
|
|
701
|
+
return [`${indent}params.addEach(${ktLiteral(p.name)}, ${prop}.map { ${itemExpr} })`];
|
|
575
702
|
}
|
|
576
|
-
return [`${indent}
|
|
703
|
+
return [`${indent}${prop}?.let { params.addEach(${ktLiteral(p.name)}, it.map { ${itemExpr} }) }`];
|
|
577
704
|
}
|
|
578
705
|
if (p.required) return [`${indent}params += ${ktLiteral(p.name)} to ${rendered}`];
|
|
579
|
-
|
|
706
|
+
if (inner.kind === 'primitive' && inner.type === 'string') {
|
|
707
|
+
return [`${indent}params.addIfNotNull(${ktLiteral(p.name)}, ${prop})`];
|
|
708
|
+
}
|
|
709
|
+
return [`${indent}${prop}?.let { params += ${ktLiteral(p.name)} to ${queryParamToString(inner, 'it')} }`];
|
|
580
710
|
}
|
|
581
711
|
|
|
582
712
|
function queryParamToString(type: TypeRef, varName: string): string {
|
|
583
713
|
if (type.kind === 'enum') return `${varName}.value`;
|
|
584
714
|
if (type.kind === 'nullable') return queryParamToString(type.inner, varName);
|
|
715
|
+
if (type.kind === 'primitive' && type.type === 'string') return varName;
|
|
585
716
|
return `${varName}.toString()`;
|
|
586
717
|
}
|
|
587
718
|
|
|
@@ -620,6 +751,32 @@ function pickNamedQueryParam(sorted: Parameter[], name: string): string {
|
|
|
620
751
|
return match ? propertyName(match.name) : 'null';
|
|
621
752
|
}
|
|
622
753
|
|
|
754
|
+
function generateAuthenticateHelper(): string[] {
|
|
755
|
+
return [
|
|
756
|
+
' private fun authenticate(',
|
|
757
|
+
' grantType: String,',
|
|
758
|
+
' requestOptions: RequestOptions?,',
|
|
759
|
+
' vararg entries: Pair<String, Any?>',
|
|
760
|
+
' ): AuthenticateResponse {',
|
|
761
|
+
' val body =',
|
|
762
|
+
' bodyOf(',
|
|
763
|
+
' *entries,',
|
|
764
|
+
' "grant_type" to grantType,',
|
|
765
|
+
' "client_id" to workos.clientId,',
|
|
766
|
+
' "client_secret" to workos.apiKey',
|
|
767
|
+
' )',
|
|
768
|
+
' val config =',
|
|
769
|
+
' RequestConfig(',
|
|
770
|
+
' method = "POST",',
|
|
771
|
+
' path = "/user_management/authenticate",',
|
|
772
|
+
' body = body,',
|
|
773
|
+
' requestOptions = requestOptions',
|
|
774
|
+
' )',
|
|
775
|
+
' return workos.baseClient.request(config, AuthenticateResponse::class.java)',
|
|
776
|
+
' }',
|
|
777
|
+
];
|
|
778
|
+
}
|
|
779
|
+
|
|
623
780
|
function resolveBodyModel(op: Operation, ctx: EmitterContext): Model | null {
|
|
624
781
|
const body = op.requestBody;
|
|
625
782
|
if (!body) return null;
|
|
@@ -665,3 +822,177 @@ export function sortPathParamsByTemplateOrder(op: Operation): Parameter[] {
|
|
|
665
822
|
function escapeKdoc(s: string): string {
|
|
666
823
|
return s.replace(/\*\//g, '*\u200b/');
|
|
667
824
|
}
|
|
825
|
+
|
|
826
|
+
// ---------------------------------------------------------------------------
|
|
827
|
+
// Mutually-exclusive parameter group support
|
|
828
|
+
// ---------------------------------------------------------------------------
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Derive a short Kotlin property name for a parameter within a variant,
|
|
832
|
+
* stripping the group name prefix to avoid stuttering.
|
|
833
|
+
*/
|
|
834
|
+
function deriveShortPropertyName(paramName: string, groupName: string): string {
|
|
835
|
+
const prefix = groupName + '_';
|
|
836
|
+
const stripped = paramName.startsWith(prefix) ? paramName.slice(prefix.length) : paramName;
|
|
837
|
+
return propertyName(stripped);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Generate sealed class definitions for all parameter groups in an operation.
|
|
842
|
+
*
|
|
843
|
+
* [bodyFieldTypes] is a fallback map from wire field name → TypeRef built from
|
|
844
|
+
* the body model. When the oagen core resolves parameter group variants it
|
|
845
|
+
* sometimes loses array/object types, falling back to a primitive string.
|
|
846
|
+
* Cross-referencing the body model corrects that.
|
|
847
|
+
*/
|
|
848
|
+
function generateSealedClass(
|
|
849
|
+
group: import('@workos/oagen').ParameterGroup,
|
|
850
|
+
bodyFieldTypes?: Map<string, TypeRef>,
|
|
851
|
+
): string[] {
|
|
852
|
+
const lines: string[] = [];
|
|
853
|
+
const sealedName = sealedGroupName(group.name);
|
|
854
|
+
lines.push(`/** Mutually exclusive ${humanize(group.name)} parameter variants. */`);
|
|
855
|
+
lines.push(`sealed class ${sealedName} {`);
|
|
856
|
+
for (let vi = 0; vi < group.variants.length; vi++) {
|
|
857
|
+
const variant = group.variants[vi];
|
|
858
|
+
const variantName = className(variant.name);
|
|
859
|
+
const fields = variant.parameters.map((p) => {
|
|
860
|
+
const prop = deriveShortPropertyName(p.name, group.name);
|
|
861
|
+
// Prefer the body model's field type when available — the IR parameter
|
|
862
|
+
// group may have lost array/object type info for body fields.
|
|
863
|
+
const effectiveType = bodyFieldTypes?.get(p.name) ?? p.type;
|
|
864
|
+
return { decl: `val ${prop}: ${mapTypeRef(effectiveType)}`, name: p.name };
|
|
865
|
+
});
|
|
866
|
+
// ktlint requires blank line before each declaration inside a sealed class
|
|
867
|
+
if (vi > 0) lines.push('');
|
|
868
|
+
// ktlint class-signature rule requires multi-line constructors
|
|
869
|
+
lines.push(` /** Variant: ${humanize(variant.name)}. */`);
|
|
870
|
+
lines.push(` data class ${variantName}(`);
|
|
871
|
+
for (let i = 0; i < fields.length; i++) {
|
|
872
|
+
const comma = i < fields.length - 1 ? ',' : '';
|
|
873
|
+
lines.push(` /** The ${humanize(fields[i].name)}. */`);
|
|
874
|
+
lines.push(` ${fields[i].decl}${comma}`);
|
|
875
|
+
}
|
|
876
|
+
lines.push(` ) : ${sealedName}()`);
|
|
877
|
+
}
|
|
878
|
+
lines.push('}');
|
|
879
|
+
lines.push('');
|
|
880
|
+
return lines;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Emit `when` dispatch that serializes a parameter group into query params. */
|
|
884
|
+
function emitGroupQueryDispatch(group: import('@workos/oagen').ParameterGroup, prop: string, indent: string): string[] {
|
|
885
|
+
const sealedName = sealedGroupName(group.name);
|
|
886
|
+
const lines: string[] = [];
|
|
887
|
+
|
|
888
|
+
if (group.optional) {
|
|
889
|
+
lines.push(`${indent}if (${prop} != null) {`);
|
|
890
|
+
emitWhenBlock(lines, group, sealedName, prop, `${indent} `);
|
|
891
|
+
lines.push(`${indent}}`);
|
|
892
|
+
} else {
|
|
893
|
+
emitWhenBlock(lines, group, sealedName, prop, indent);
|
|
894
|
+
}
|
|
895
|
+
return lines;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function assignGroupParameterNames(op: Operation, occupiedNames: Set<string>): Map<string, string> {
|
|
899
|
+
const names = new Map<string, string>();
|
|
900
|
+
for (const group of op.parameterGroups ?? []) {
|
|
901
|
+
const natural = propertyName(sealedGroupName(group.name));
|
|
902
|
+
const assigned = reserveUniqueGroupParameterName(natural, occupiedNames);
|
|
903
|
+
names.set(group.name, assigned);
|
|
904
|
+
}
|
|
905
|
+
return names;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function reserveUniqueGroupParameterName(base: string, occupiedNames: Set<string>): string {
|
|
909
|
+
if (!occupiedNames.has(base)) {
|
|
910
|
+
occupiedNames.add(base);
|
|
911
|
+
return base;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const capitalized = `${base.charAt(0).toUpperCase()}${base.slice(1)}`;
|
|
915
|
+
const prefixed = `group${capitalized}`;
|
|
916
|
+
if (!occupiedNames.has(prefixed)) {
|
|
917
|
+
occupiedNames.add(prefixed);
|
|
918
|
+
return prefixed;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
let index = 2;
|
|
922
|
+
while (occupiedNames.has(`${prefixed}${index}`)) index += 1;
|
|
923
|
+
const fallback = `${prefixed}${index}`;
|
|
924
|
+
occupiedNames.add(fallback);
|
|
925
|
+
return fallback;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function emitWhenBlock(
|
|
929
|
+
lines: string[],
|
|
930
|
+
group: import('@workos/oagen').ParameterGroup,
|
|
931
|
+
sealedName: string,
|
|
932
|
+
prop: string,
|
|
933
|
+
indent: string,
|
|
934
|
+
): void {
|
|
935
|
+
lines.push(`${indent}when (${prop}) {`);
|
|
936
|
+
for (const variant of group.variants) {
|
|
937
|
+
const variantName = className(variant.name);
|
|
938
|
+
const entries = variant.parameters.map((p) => {
|
|
939
|
+
const fieldProp = deriveShortPropertyName(p.name, group.name);
|
|
940
|
+
return `params += ${ktLiteral(p.name)} to ${prop}.${fieldProp}`;
|
|
941
|
+
});
|
|
942
|
+
if (entries.length === 1) {
|
|
943
|
+
lines.push(`${indent} is ${sealedName}.${variantName} -> ${entries[0]}`);
|
|
944
|
+
} else {
|
|
945
|
+
lines.push(`${indent} is ${sealedName}.${variantName} -> {`);
|
|
946
|
+
for (const e of entries) lines.push(`${indent} ${e}`);
|
|
947
|
+
lines.push(`${indent} }`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
lines.push(`${indent}}`);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** Emit `when` dispatch that serializes a parameter group into the request body map. */
|
|
954
|
+
function emitGroupBodyDispatch(group: import('@workos/oagen').ParameterGroup, prop: string, indent: string): string[] {
|
|
955
|
+
const sealedName = sealedGroupName(group.name);
|
|
956
|
+
const lines: string[] = [];
|
|
957
|
+
|
|
958
|
+
if (group.optional) {
|
|
959
|
+
lines.push(`${indent}if (${prop} != null) {`);
|
|
960
|
+
emitBodyWhenBlock(lines, group, sealedName, prop, `${indent} `);
|
|
961
|
+
lines.push(`${indent}}`);
|
|
962
|
+
} else {
|
|
963
|
+
emitBodyWhenBlock(lines, group, sealedName, prop, indent);
|
|
964
|
+
}
|
|
965
|
+
return lines;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function sealedGroupName(name: string): string {
|
|
969
|
+
const resolved = className(name);
|
|
970
|
+
if (resolved === 'Password') return 'CreateUserPassword';
|
|
971
|
+
if (resolved === 'Role') return 'CreateUserRole';
|
|
972
|
+
return resolved;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function emitBodyWhenBlock(
|
|
976
|
+
lines: string[],
|
|
977
|
+
group: import('@workos/oagen').ParameterGroup,
|
|
978
|
+
sealedName: string,
|
|
979
|
+
prop: string,
|
|
980
|
+
indent: string,
|
|
981
|
+
): void {
|
|
982
|
+
lines.push(`${indent}when (${prop}) {`);
|
|
983
|
+
for (const variant of group.variants) {
|
|
984
|
+
const variantName = className(variant.name);
|
|
985
|
+
const entries = variant.parameters.map((p) => {
|
|
986
|
+
const fieldProp = deriveShortPropertyName(p.name, group.name);
|
|
987
|
+
return `body[${ktLiteral(p.name)}] = ${prop}.${fieldProp}`;
|
|
988
|
+
});
|
|
989
|
+
if (entries.length === 1) {
|
|
990
|
+
lines.push(`${indent} is ${sealedName}.${variantName} -> ${entries[0]}`);
|
|
991
|
+
} else {
|
|
992
|
+
lines.push(`${indent} is ${sealedName}.${variantName} -> {`);
|
|
993
|
+
for (const e of entries) lines.push(`${indent} ${e}`);
|
|
994
|
+
lines.push(`${indent} }`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
lines.push(`${indent}}`);
|
|
998
|
+
}
|