@workos/oagen-emitters 0.8.1 → 0.9.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 +19 -0
- package/dist/index.mjs +1 -1
- package/dist/{plugin-DOE0FqrZ.mjs → plugin-Dh9JSScr.mjs} +586 -86
- package/dist/plugin-Dh9JSScr.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +4 -4
- package/src/kotlin/client.ts +12 -6
- package/src/kotlin/enums.ts +12 -1
- package/src/kotlin/index.ts +8 -6
- package/src/kotlin/models.ts +99 -4
- package/src/kotlin/naming.ts +58 -4
- package/src/kotlin/resources.ts +436 -53
- package/src/kotlin/suspend.ts +96 -0
- package/src/kotlin/tests.ts +33 -5
- package/src/kotlin/wrappers.ts +104 -21
- package/test/kotlin/resources.test.ts +94 -1
- package/dist/plugin-DOE0FqrZ.mjs.map +0 -1
package/src/kotlin/resources.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
clientFieldExpression,
|
|
24
24
|
escapeReserved,
|
|
25
25
|
humanize,
|
|
26
|
+
maybeShortenEnumParamDescription,
|
|
26
27
|
} from './naming.js';
|
|
27
28
|
import {
|
|
28
29
|
buildResolvedLookup,
|
|
@@ -38,9 +39,50 @@ import { generateWrapperMethods } from './wrappers.js';
|
|
|
38
39
|
import { resolveWrapperParams } from '../shared/wrapper-utils.js';
|
|
39
40
|
import { isHandwrittenOverride } from './overrides.js';
|
|
40
41
|
import { buildKotlinPathExpression, KOTLIN_PATH_ENCODE_IMPORT } from './path-expression.js';
|
|
42
|
+
import { emitSuspendVariant, SUSPEND_IMPORTS, type SuspendParam } from './suspend.js';
|
|
41
43
|
|
|
42
44
|
const KOTLIN_SRC_PREFIX = 'src/main/kotlin/';
|
|
43
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Some specs leave query params / fields typed as plain `string` even though
|
|
48
|
+
* the description (or the field name) makes clear they carry an ISO-8601
|
|
49
|
+
* timestamp. Detecting that here lets us emit `OffsetDateTime` so callers
|
|
50
|
+
* don't have to format the wire string themselves.
|
|
51
|
+
*/
|
|
52
|
+
const ISO_8601_DESCRIPTION_RE = /\bISO[-_ ]?8601\b/i;
|
|
53
|
+
|
|
54
|
+
function looksLikeIso8601String(description: string | undefined): boolean {
|
|
55
|
+
if (!description) return false;
|
|
56
|
+
return ISO_8601_DESCRIPTION_RE.test(description);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Promote a string `TypeRef` to a `format: date-time` primitive when the
|
|
61
|
+
* accompanying description identifies it as an ISO-8601 timestamp. Leaves
|
|
62
|
+
* non-string types untouched.
|
|
63
|
+
*/
|
|
64
|
+
function promoteIso8601TypeRef(type: TypeRef, description: string | undefined): TypeRef {
|
|
65
|
+
if (!looksLikeIso8601String(description)) return type;
|
|
66
|
+
const promote = (t: TypeRef): TypeRef => {
|
|
67
|
+
if (t.kind === 'primitive' && t.type === 'string' && !t.format) {
|
|
68
|
+
return { kind: 'primitive', type: 'string', format: 'date-time' };
|
|
69
|
+
}
|
|
70
|
+
if (t.kind === 'nullable') return { kind: 'nullable', inner: promote(t.inner) };
|
|
71
|
+
return t;
|
|
72
|
+
};
|
|
73
|
+
return promote(type);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function promoteParameterType(p: Parameter): Parameter {
|
|
77
|
+
const promoted = promoteIso8601TypeRef(p.type, p.description);
|
|
78
|
+
return promoted === p.type ? p : { ...p, type: promoted };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function promoteFieldType(f: Field): Field {
|
|
82
|
+
const promoted = promoteIso8601TypeRef(f.type, f.description);
|
|
83
|
+
return promoted === f.type ? f : { ...f, type: promoted };
|
|
84
|
+
}
|
|
85
|
+
|
|
44
86
|
/**
|
|
45
87
|
* Generate one API class per mount group. Methods map 1:1 to IR operations.
|
|
46
88
|
* Path params, query params, and body fields are flattened into the method
|
|
@@ -84,6 +126,9 @@ function generateApiClass(
|
|
|
84
126
|
imports.add('com.workos.common.http.Page');
|
|
85
127
|
imports.add('com.workos.common.http.RequestConfig');
|
|
86
128
|
imports.add('com.workos.common.http.RequestOptions');
|
|
129
|
+
// Every emitted method gains a `suspend` overload that delegates to the
|
|
130
|
+
// blocking version under `withContext(Dispatchers.IO)`.
|
|
131
|
+
for (const imp of SUSPEND_IMPORTS) imports.add(imp);
|
|
87
132
|
|
|
88
133
|
const body: string[] = [];
|
|
89
134
|
const seenMethods = new Set<string>();
|
|
@@ -183,17 +228,29 @@ function generateApiClass(
|
|
|
183
228
|
for (const line of sealedLines) lines.push(line);
|
|
184
229
|
|
|
185
230
|
const serviceDescription = resolveServiceDescription(ctx, mountName, operations);
|
|
231
|
+
// Every blocking method on this class also has a `suspend` overload — the
|
|
232
|
+
// suspend variant delegates to the blocking one via
|
|
233
|
+
// `withContext(Dispatchers.IO)` and is safe to call from any coroutine
|
|
234
|
+
// dispatcher. The service-level KDoc surfaces this so callers don't have to
|
|
235
|
+
// discover it per-method.
|
|
236
|
+
const suspendNote =
|
|
237
|
+
'Every operation on this class is available in two flavors: a blocking variant ' +
|
|
238
|
+
'(`<methodName>`) and a coroutine-aware variant (`<methodName>Suspend`). ' +
|
|
239
|
+
'The `Suspend` variants delegate to the blocking ones under `withContext(Dispatchers.IO)`, ' +
|
|
240
|
+
'so they are safe to call from any coroutine dispatcher (including `Dispatchers.Main`).';
|
|
186
241
|
if (serviceDescription) {
|
|
187
242
|
const docLines = serviceDescription.trim().split('\n');
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
lines.push(' */');
|
|
194
|
-
}
|
|
243
|
+
lines.push('/**');
|
|
244
|
+
for (const l of docLines) lines.push(l ? ` * ${escapeKdoc(l)}` : ' *');
|
|
245
|
+
lines.push(' *');
|
|
246
|
+
lines.push(` * ${escapeKdoc(suspendNote)}`);
|
|
247
|
+
lines.push(' */');
|
|
195
248
|
} else {
|
|
196
|
-
lines.push(
|
|
249
|
+
lines.push('/**');
|
|
250
|
+
lines.push(` * API accessor for ${mountName}.`);
|
|
251
|
+
lines.push(' *');
|
|
252
|
+
lines.push(` * ${escapeKdoc(suspendNote)}`);
|
|
253
|
+
lines.push(' */');
|
|
197
254
|
}
|
|
198
255
|
// ktlint requires constructor-property parameters on their own line.
|
|
199
256
|
// The property is `internal` so hand-maintained extension files in the
|
|
@@ -249,10 +306,12 @@ function renderMethod(
|
|
|
249
306
|
const pathParams = sortPathParamsByTemplateOrder(op);
|
|
250
307
|
const groupedParamNames = collectGroupedParamNames(op);
|
|
251
308
|
const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
|
|
252
|
-
const queryParams = op.queryParams
|
|
309
|
+
const queryParams = op.queryParams
|
|
310
|
+
.filter((p) => !hidden.has(p.name) && !groupedParamNames.has(p.name))
|
|
311
|
+
.map(promoteParameterType);
|
|
253
312
|
const bodyModel = resolveBodyModel(op, ctx);
|
|
254
313
|
const bodyFields = bodyModel
|
|
255
|
-
? bodyModel.fields.filter((f) => !hidden.has(f.name) && !groupedParamNames.has(f.name))
|
|
314
|
+
? bodyModel.fields.filter((f) => !hidden.has(f.name) && !groupedParamNames.has(f.name)).map(promoteFieldType)
|
|
256
315
|
: [];
|
|
257
316
|
|
|
258
317
|
// Track imports we need
|
|
@@ -304,11 +363,21 @@ function renderMethod(
|
|
|
304
363
|
const groupParamNames = assignGroupParameterNames(op, paramNames);
|
|
305
364
|
|
|
306
365
|
const params: string[] = [];
|
|
307
|
-
|
|
366
|
+
// Mirrors `params` but tracks the bare Kotlin parameter name so the suspend
|
|
367
|
+
// overload (emitted alongside the blocking version) can forward arguments.
|
|
368
|
+
const suspendParams: SuspendParam[] = [];
|
|
369
|
+
const pushParam = (decl: string, name: string) => {
|
|
370
|
+
params.push(decl);
|
|
371
|
+
suspendParams.push({ decl, name });
|
|
372
|
+
};
|
|
373
|
+
for (const pp of pathParams) pushParam(` ${propertyName(pp.name)}: String`, propertyName(pp.name));
|
|
308
374
|
|
|
309
375
|
const sortedQuery = [...uniqueQuery].sort((a, b) => (a.required === b.required ? 0 : a.required ? -1 : 1));
|
|
310
376
|
for (const qp of sortedQuery) {
|
|
311
|
-
|
|
377
|
+
pushParam(
|
|
378
|
+
renderParam(qp.name, qp.type, qp.required, method.startsWith('list') && qp.name === 'limit'),
|
|
379
|
+
propertyName(qp.name),
|
|
380
|
+
);
|
|
312
381
|
}
|
|
313
382
|
|
|
314
383
|
// Parameter group params (sealed class types)
|
|
@@ -316,9 +385,9 @@ function renderMethod(
|
|
|
316
385
|
const sealedName = sealedGroupName(group.name);
|
|
317
386
|
const prop = groupParamNames.get(group.name)!;
|
|
318
387
|
if (group.optional) {
|
|
319
|
-
|
|
388
|
+
pushParam(` ${prop}: ${sealedName}? = null`, prop);
|
|
320
389
|
} else {
|
|
321
|
-
|
|
390
|
+
pushParam(` ${prop}: ${sealedName}`, prop);
|
|
322
391
|
}
|
|
323
392
|
}
|
|
324
393
|
|
|
@@ -332,14 +401,17 @@ function renderMethod(
|
|
|
332
401
|
if (isPatch && !bf.required) {
|
|
333
402
|
const baseType = mapTypeRef(bf.type);
|
|
334
403
|
imports.add('com.workos.common.http.PatchField');
|
|
335
|
-
|
|
404
|
+
pushParam(
|
|
405
|
+
` ${bodyParamNames.get(bf.name)!}: PatchField<${baseType}> = PatchField.Absent`,
|
|
406
|
+
bodyParamNames.get(bf.name)!,
|
|
407
|
+
);
|
|
336
408
|
} else {
|
|
337
|
-
|
|
409
|
+
pushParam(renderParamNamed(bodyParamNames.get(bf.name)!, bf.type, bf.required), bodyParamNames.get(bf.name)!);
|
|
338
410
|
}
|
|
339
411
|
}
|
|
340
412
|
|
|
341
413
|
// Per-request options trailer (always optional)
|
|
342
|
-
|
|
414
|
+
pushParam(' requestOptions: RequestOptions? = null', 'requestOptions');
|
|
343
415
|
|
|
344
416
|
const returnType = resolveReturnType(plan, imports, ctx);
|
|
345
417
|
const isPaginated = plan.isPaginated && paginatedItemName !== null;
|
|
@@ -403,6 +475,7 @@ function renderMethod(
|
|
|
403
475
|
}
|
|
404
476
|
lines.push(` )`);
|
|
405
477
|
lines.push(' }');
|
|
478
|
+
appendSuspendVariantLines(lines, method, suspendParams, returnType, op.deprecated);
|
|
406
479
|
return lines.join('\n');
|
|
407
480
|
}
|
|
408
481
|
|
|
@@ -426,12 +499,13 @@ function renderMethod(
|
|
|
426
499
|
lines.push(` before = ${pickNamedQueryParam(sortedQuery, 'before')},`);
|
|
427
500
|
lines.push(` after = ${pickNamedQueryParam(sortedQuery, 'after')}`);
|
|
428
501
|
lines.push(` ) {`);
|
|
429
|
-
lines.push(` val params = this`);
|
|
430
502
|
for (const qp of sortedQuery.filter((p) => p.name !== 'after' && p.name !== 'before')) {
|
|
431
|
-
for (const ln of emitQueryParam(qp, ' ')) lines.push(ln);
|
|
503
|
+
for (const ln of emitQueryParam(qp, ' ', true)) lines.push(ln);
|
|
432
504
|
}
|
|
433
505
|
for (const group of op.parameterGroups ?? []) {
|
|
434
|
-
for (const ln of emitGroupQueryDispatch(group, groupParamNames.get(group.name)!, ' '))
|
|
506
|
+
for (const ln of emitGroupQueryDispatch(group, groupParamNames.get(group.name)!, ' ', true)) {
|
|
507
|
+
lines.push(ln);
|
|
508
|
+
}
|
|
435
509
|
}
|
|
436
510
|
lines.push(` }`);
|
|
437
511
|
} else {
|
|
@@ -537,9 +611,227 @@ function renderMethod(
|
|
|
537
611
|
}
|
|
538
612
|
|
|
539
613
|
lines.push(' }');
|
|
614
|
+
appendSuspendVariantLines(lines, method, suspendParams, returnType, op.deprecated);
|
|
615
|
+
|
|
616
|
+
// Java-friendly overloads: for each variant of every sealed-class parameter
|
|
617
|
+
// group, emit an additional overload that accepts the variant's flat fields
|
|
618
|
+
// directly (no `new ResourceTarget.ById(...)` boilerplate from Java).
|
|
619
|
+
appendJavaFriendlyVariantOverloads(lines, {
|
|
620
|
+
method,
|
|
621
|
+
op,
|
|
622
|
+
canonicalParams: suspendParams,
|
|
623
|
+
groupParamNames,
|
|
624
|
+
returnType,
|
|
625
|
+
deprecated: op.deprecated,
|
|
626
|
+
});
|
|
627
|
+
|
|
540
628
|
return lines.join('\n');
|
|
541
629
|
}
|
|
542
630
|
|
|
631
|
+
interface JavaOverloadCtx {
|
|
632
|
+
method: string;
|
|
633
|
+
op: Operation;
|
|
634
|
+
/**
|
|
635
|
+
* The exact parameter declarations of the canonical method, in order. The
|
|
636
|
+
* Java-friendly overload reuses these verbatim for non-variant params so
|
|
637
|
+
* type/nullability/default exactly match the canonical method (avoiding
|
|
638
|
+
* subtle drift like `Long?` vs `Int?` or `String?` vs `String`).
|
|
639
|
+
*/
|
|
640
|
+
canonicalParams: SuspendParam[];
|
|
641
|
+
groupParamNames: Map<string, string>;
|
|
642
|
+
returnType: string;
|
|
643
|
+
deprecated: boolean | undefined;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* For each variant of every sealed-class parameter group on `op`, append a
|
|
648
|
+
* Java-discoverable overload that takes the variant's flat fields directly
|
|
649
|
+
* and forwards to the canonical (sealed-class) method.
|
|
650
|
+
*
|
|
651
|
+
* Implementation scope: when an operation has multiple parameter groups, we
|
|
652
|
+
* emit overloads for each variant of each group while keeping the *other*
|
|
653
|
+
* groups' sealed-class params unchanged. This avoids combinatorial explosion
|
|
654
|
+
* for ops with several groups while still flattening the common case.
|
|
655
|
+
*
|
|
656
|
+
* Method-name derivation handles the most common variant shapes (`ById`,
|
|
657
|
+
* `ByExternalId`, etc.). Other shapes (e.g. `Plaintext`, `Imported`) are
|
|
658
|
+
* skipped with a comment so unusual unions don't compile-fail the SDK; if
|
|
659
|
+
* one becomes important we can add an explicit case here.
|
|
660
|
+
*/
|
|
661
|
+
function appendJavaFriendlyVariantOverloads(lines: string[], ctx: JavaOverloadCtx): void {
|
|
662
|
+
const groups = ctx.op.parameterGroups ?? [];
|
|
663
|
+
if (groups.length === 0) return;
|
|
664
|
+
|
|
665
|
+
for (const group of groups) {
|
|
666
|
+
for (const variant of group.variants) {
|
|
667
|
+
const overloadMethodName = deriveOverloadMethodName(ctx.method, variant.name);
|
|
668
|
+
if (overloadMethodName === null) {
|
|
669
|
+
// Variant shape doesn't fit a clean Java-friendly method name; skip.
|
|
670
|
+
// TODO: extend deriveOverloadMethodName for less-common shapes
|
|
671
|
+
// (e.g. password variants) once we have a need for them.
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
emitOneJavaOverload(lines, ctx, group, variant, overloadMethodName);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function deriveOverloadMethodName(baseMethod: string, variantName: string): string | null {
|
|
680
|
+
const v = className(variantName);
|
|
681
|
+
if (v === 'ById') return baseMethod;
|
|
682
|
+
if (/^By[A-Z]/.test(v)) {
|
|
683
|
+
// Don't double-stack a By-suffix: if the operation method already ends
|
|
684
|
+
// with the variant suffix (case-insensitive), reuse it as-is. This keeps
|
|
685
|
+
// names like `updateResourceByExternalId` from becoming
|
|
686
|
+
// `updateResourceByExternalIdByExternalId`.
|
|
687
|
+
if (baseMethod.toLowerCase().endsWith(v.toLowerCase())) {
|
|
688
|
+
return baseMethod;
|
|
689
|
+
}
|
|
690
|
+
return `${baseMethod}${v}`;
|
|
691
|
+
}
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function emitOneJavaOverload(
|
|
696
|
+
lines: string[],
|
|
697
|
+
ctx: JavaOverloadCtx,
|
|
698
|
+
group: import('@workos/oagen').ParameterGroup,
|
|
699
|
+
variant: import('@workos/oagen').ParameterGroup['variants'][number],
|
|
700
|
+
overloadMethodName: string,
|
|
701
|
+
): void {
|
|
702
|
+
const sealedName = sealedGroupName(group.name);
|
|
703
|
+
const variantClass = className(variant.name);
|
|
704
|
+
const targetGroupProp = ctx.groupParamNames.get(group.name)!;
|
|
705
|
+
|
|
706
|
+
// Reuse the canonical method's exact parameter decls. We swap out only the
|
|
707
|
+
// target group's slot (replacing the sealed param with variant fields) and
|
|
708
|
+
// leave path/query/body params untouched so types/nullability/defaults
|
|
709
|
+
// line up exactly with what the canonical method accepts.
|
|
710
|
+
type ParamSpec = { decl: string; name: string };
|
|
711
|
+
const decls: ParamSpec[] = [];
|
|
712
|
+
|
|
713
|
+
// Track names already in use so variant field names that collide with
|
|
714
|
+
// existing op params can be deterministically disambiguated.
|
|
715
|
+
const existingNames = new Set<string>();
|
|
716
|
+
for (const cp of ctx.canonicalParams) {
|
|
717
|
+
if (cp.name !== targetGroupProp) existingNames.add(cp.name);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Compute variant field decls (with collision-aware names). For a colliding
|
|
721
|
+
// field, prefix it with the group's property name (e.g. `externalId` →
|
|
722
|
+
// `parentResourceExternalId`) so the overload signature is unambiguous and
|
|
723
|
+
// doesn't shadow the operation's own path/query/body params.
|
|
724
|
+
const variantFieldDecls: ParamSpec[] = [];
|
|
725
|
+
const variantArgPairs: { sealedField: string; localName: string }[] = [];
|
|
726
|
+
for (const p of variant.parameters) {
|
|
727
|
+
const sealedField = deriveShortPropertyName(p.name, group.name);
|
|
728
|
+
let localName = sealedField;
|
|
729
|
+
if (existingNames.has(localName)) {
|
|
730
|
+
const groupPrefix = propertyName(group.name);
|
|
731
|
+
const camel = `${groupPrefix}${localName.charAt(0).toUpperCase()}${localName.slice(1)}`;
|
|
732
|
+
localName = camel;
|
|
733
|
+
}
|
|
734
|
+
existingNames.add(localName);
|
|
735
|
+
// Variant fields are always required when their variant is selected — the
|
|
736
|
+
// sealed-class data class declares them non-nullable (see
|
|
737
|
+
// [generateSealedClass]). Mirror that here so the overload's flat
|
|
738
|
+
// parameter types match the variant-constructor argument types exactly.
|
|
739
|
+
// Also: prefer the body model's field type via [bodyFieldTypes] when the
|
|
740
|
+
// parameter-group IR has lost type fidelity. Inside this scope we don't
|
|
741
|
+
// have bodyFieldTypes; the canonical method's type rendering for variant
|
|
742
|
+
// fields hasn't been needed elsewhere, so fall back to p.type — which is
|
|
743
|
+
// correct for non-body groups (path/query) and for body groups whose
|
|
744
|
+
// types weren't degraded.
|
|
745
|
+
const decl = renderParamNamed(localName, p.type, true);
|
|
746
|
+
variantFieldDecls.push({ decl, name: localName });
|
|
747
|
+
variantArgPairs.push({ sealedField, localName });
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Walk canonical params; substitute the target group slot with the variant's
|
|
751
|
+
// flat fields, copy everything else unchanged.
|
|
752
|
+
for (const cp of ctx.canonicalParams) {
|
|
753
|
+
if (cp.name === targetGroupProp) {
|
|
754
|
+
for (const v of variantFieldDecls) decls.push(v);
|
|
755
|
+
} else {
|
|
756
|
+
decls.push({ decl: cp.decl, name: cp.name });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const returnClause = ctx.returnType === 'Unit' ? '' : `: ${ctx.returnType}`;
|
|
761
|
+
const variantArgs = variantArgPairs.map((p) => `${p.sealedField} = ${p.localName}`).join(', ');
|
|
762
|
+
const sealedConstruct = `${sealedName}.${variantClass}(${variantArgs})`;
|
|
763
|
+
|
|
764
|
+
// Build the call to the canonical method by walking canonical params again:
|
|
765
|
+
// the target group slot is set to the inline-constructed variant; everything
|
|
766
|
+
// else passes through by its canonical name.
|
|
767
|
+
const forwardArgs: string[] = [];
|
|
768
|
+
for (const cp of ctx.canonicalParams) {
|
|
769
|
+
if (cp.name === targetGroupProp) {
|
|
770
|
+
forwardArgs.push(`${cp.name} = ${sealedConstruct}`);
|
|
771
|
+
} else {
|
|
772
|
+
forwardArgs.push(`${cp.name} = ${cp.name}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// KDoc.
|
|
777
|
+
lines.push('');
|
|
778
|
+
lines.push(' /**');
|
|
779
|
+
lines.push(` * Java-friendly overload — equivalent to`);
|
|
780
|
+
lines.push(` * \`${ctx.method}(${sealedName}.${variantClass}(...))\` from Kotlin.`);
|
|
781
|
+
lines.push(` *`);
|
|
782
|
+
lines.push(` * Accepts the discriminating fields directly so Java callers don't`);
|
|
783
|
+
lines.push(` * need to construct \`${sealedName}.${variantClass}\` explicitly.`);
|
|
784
|
+
lines.push(' */');
|
|
785
|
+
if (ctx.deprecated) lines.push(' @Deprecated("Deprecated operation")');
|
|
786
|
+
// Note: no `@JvmOverloads` here. The synthetic Java overloads `@JvmOverloads`
|
|
787
|
+
// generates (one per trailing optional, with the optional dropped) collide
|
|
788
|
+
// with the canonical method's `@JvmOverloads` permutations whenever the
|
|
789
|
+
// overload reuses the canonical method name (e.g. `ById` variants).
|
|
790
|
+
// Java callers pay a small ergonomic cost — they must pass `null` for
|
|
791
|
+
// optional trailing params (typically just `requestOptions`) — but the
|
|
792
|
+
// overload's main purpose (no sealed-class construction) still applies.
|
|
793
|
+
|
|
794
|
+
// Emit the function signature.
|
|
795
|
+
if (decls.length === 1) {
|
|
796
|
+
const single = decls[0].decl.replace(/^\s+/, '');
|
|
797
|
+
lines.push(` fun ${escapeReserved(overloadMethodName)}(${single})${returnClause} = ${ctx.method}(`);
|
|
798
|
+
} else {
|
|
799
|
+
lines.push(` fun ${escapeReserved(overloadMethodName)}(`);
|
|
800
|
+
for (let i = 0; i < decls.length; i++) {
|
|
801
|
+
const sep = i === decls.length - 1 ? '' : ',';
|
|
802
|
+
lines.push(`${decls[i].decl}${sep}`);
|
|
803
|
+
}
|
|
804
|
+
lines.push(` )${returnClause} = ${ctx.method}(`);
|
|
805
|
+
}
|
|
806
|
+
for (let i = 0; i < forwardArgs.length; i++) {
|
|
807
|
+
const sep = i === forwardArgs.length - 1 ? '' : ',';
|
|
808
|
+
lines.push(` ${forwardArgs[i]}${sep}`);
|
|
809
|
+
}
|
|
810
|
+
lines.push(' )');
|
|
811
|
+
|
|
812
|
+
// Emit a coroutine-friendly suspend variant of the overload too.
|
|
813
|
+
appendSuspendVariantLines(
|
|
814
|
+
lines,
|
|
815
|
+
overloadMethodName,
|
|
816
|
+
decls.map((d) => ({ decl: d.decl, name: d.name })),
|
|
817
|
+
ctx.returnType,
|
|
818
|
+
ctx.deprecated,
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function appendSuspendVariantLines(
|
|
823
|
+
lines: string[],
|
|
824
|
+
method: string,
|
|
825
|
+
suspendParams: SuspendParam[],
|
|
826
|
+
returnType: string,
|
|
827
|
+
deprecated: boolean | undefined,
|
|
828
|
+
): void {
|
|
829
|
+
lines.push('');
|
|
830
|
+
for (const ln of emitSuspendVariant({ methodName: method, params: suspendParams, returnType, deprecated })) {
|
|
831
|
+
lines.push(ln);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
543
835
|
function resolveReturnType(plan: ReturnType<typeof planOperation>, imports: Set<string>, ctx?: EmitterContext): string {
|
|
544
836
|
const itemName = plan.isPaginated
|
|
545
837
|
? (resolvePaginatedItemName(plan.paginatedItemModelName, ctx) ?? plan.paginatedItemModelName)
|
|
@@ -609,31 +901,38 @@ function buildMethodKdoc(
|
|
|
609
901
|
}
|
|
610
902
|
|
|
611
903
|
// @param lines. Use the Kotlin-visible parameter name (body collisions get
|
|
612
|
-
// renamed, e.g. slug → bodySlug).
|
|
613
|
-
//
|
|
614
|
-
//
|
|
904
|
+
// renamed, e.g. slug → bodySlug). Every parameter gets an `@param` line —
|
|
905
|
+
// Dokka does not flag missing `@param` blocks (only fully undocumented
|
|
906
|
+
// declarations), so we have to enforce coverage at emit time. Spec-provided
|
|
907
|
+
// descriptions are preferred; missing descriptions get a templated fallback
|
|
908
|
+
// derived from the parameter name. The fallback is intentionally a little
|
|
909
|
+
// ugly — it nudges callers to add real descriptions to the spec.
|
|
615
910
|
const paramDocs: string[] = [];
|
|
616
911
|
const seenParamDocs = new Set<string>();
|
|
617
|
-
const pushParamDoc = (
|
|
912
|
+
const pushParamDoc = (
|
|
913
|
+
name: string,
|
|
914
|
+
sourceName: string,
|
|
915
|
+
description: string | undefined,
|
|
916
|
+
deprecated?: boolean,
|
|
917
|
+
type?: TypeRef,
|
|
918
|
+
) => {
|
|
618
919
|
if (seenParamDocs.has(name)) return;
|
|
619
920
|
seenParamDocs.add(name);
|
|
620
|
-
paramDocs.push(formatParamDoc(name, description, deprecated));
|
|
921
|
+
paramDocs.push(formatParamDoc(name, description, deprecated, sourceName, type));
|
|
621
922
|
};
|
|
622
923
|
for (const pp of pathParams) {
|
|
623
|
-
|
|
624
|
-
pushParamDoc(propertyName(pp.name), pp.description, pp.deprecated);
|
|
625
|
-
}
|
|
924
|
+
pushParamDoc(propertyName(pp.name), pp.name, pp.description, pp.deprecated, pp.type);
|
|
626
925
|
}
|
|
627
926
|
for (const qp of queryParams) {
|
|
628
|
-
|
|
629
|
-
pushParamDoc(propertyName(qp.name), qp.description, qp.deprecated);
|
|
630
|
-
}
|
|
927
|
+
pushParamDoc(propertyName(qp.name), qp.name, qp.description, qp.deprecated, qp.type);
|
|
631
928
|
}
|
|
632
929
|
for (const bf of bodyFields) {
|
|
633
|
-
|
|
634
|
-
pushParamDoc(bodyParamNames.get(bf.name)!, bf.description, bf.deprecated);
|
|
635
|
-
}
|
|
930
|
+
pushParamDoc(bodyParamNames.get(bf.name)!, bf.name, bf.description, bf.deprecated, bf.type);
|
|
636
931
|
}
|
|
932
|
+
// Always document the trailing `requestOptions` parameter with a stable,
|
|
933
|
+
// canned phrasing so generated SDKs are consistent and Dokka's coverage
|
|
934
|
+
// reporting has nothing to flag.
|
|
935
|
+
pushParamDoc('requestOptions', 'request_options', REQUEST_OPTIONS_PARAM_DESCRIPTION);
|
|
637
936
|
|
|
638
937
|
const returnDoc = plan.isPaginated
|
|
639
938
|
? '@return a [com.workos.common.http.Page] of results'
|
|
@@ -658,12 +957,35 @@ function buildMethodKdoc(
|
|
|
658
957
|
return out;
|
|
659
958
|
}
|
|
660
959
|
|
|
661
|
-
|
|
960
|
+
/**
|
|
961
|
+
* Stable, canned description for the trailing `requestOptions` parameter that
|
|
962
|
+
* every generated method exposes. Kept as a constant so the same phrasing
|
|
963
|
+
* appears across resource methods, wrapper methods, and union-split helpers.
|
|
964
|
+
*/
|
|
965
|
+
const REQUEST_OPTIONS_PARAM_DESCRIPTION = 'per-request overrides (idempotency key, API key, headers, timeout)';
|
|
966
|
+
|
|
967
|
+
function formatParamDoc(
|
|
968
|
+
kotlinName: string,
|
|
969
|
+
description: string | undefined,
|
|
970
|
+
deprecated?: boolean,
|
|
971
|
+
sourceName?: string,
|
|
972
|
+
type?: TypeRef,
|
|
973
|
+
): string {
|
|
662
974
|
const firstLine = description?.split('\n').find((l) => l.trim()) ?? '';
|
|
663
|
-
const
|
|
975
|
+
const specText = firstLine.trim();
|
|
664
976
|
const deprecationNote = deprecated ? '**Deprecated.**' : '';
|
|
977
|
+
// Fall back to a templated description derived from the parameter name when
|
|
978
|
+
// the spec didn't provide one. Dokka has no `-Xdoclint:missing` analogue,
|
|
979
|
+
// so emitting a placeholder is the only way to guarantee `@param` coverage.
|
|
980
|
+
const fallback = `the ${humanize(sourceName ?? kotlinName)} of the request.`;
|
|
981
|
+
let text = specText || fallback;
|
|
982
|
+
// For long enum-typed parameter descriptions (notably PaginationOrder),
|
|
983
|
+
// replace the verbose per-method copy with a short summary that defers to
|
|
984
|
+
// the enum's own KDoc — see [maybeShortenEnumParamDescription].
|
|
985
|
+
const shortened = maybeShortenEnumParamDescription(type, text);
|
|
986
|
+
if (shortened) text = shortened.description;
|
|
665
987
|
const parts = [deprecationNote, text].filter(Boolean).join(' ');
|
|
666
|
-
return `@param ${kotlinName}
|
|
988
|
+
return `@param ${kotlinName} ${escapeKdoc(parts)}`;
|
|
667
989
|
}
|
|
668
990
|
|
|
669
991
|
/**
|
|
@@ -684,43 +1006,66 @@ function unwrapArray(t: TypeRef): TypeRef | null {
|
|
|
684
1006
|
function valueExprForQuery(type: TypeRef): string {
|
|
685
1007
|
const inner = type.kind === 'nullable' ? type.inner : type;
|
|
686
1008
|
if (inner.kind === 'enum') return 'it.value';
|
|
687
|
-
if (inner.kind === 'primitive' && inner.type === 'string')
|
|
1009
|
+
if (inner.kind === 'primitive' && inner.type === 'string') {
|
|
1010
|
+
return inner.format === 'date-time' ? 'it.toString()' : 'it';
|
|
1011
|
+
}
|
|
688
1012
|
return 'it.toString()';
|
|
689
1013
|
}
|
|
690
1014
|
|
|
691
|
-
function emitQueryParam(p: Parameter, indent: string): string[] {
|
|
1015
|
+
function emitQueryParam(p: Parameter, indent: string, receiverMode = false): string[] {
|
|
692
1016
|
const prop = propertyName(p.name);
|
|
693
1017
|
const rendered = queryParamToString(p.type, prop);
|
|
694
1018
|
const inner = p.type.kind === 'nullable' ? p.type.inner : p.type;
|
|
695
1019
|
const arrayItem = unwrapArray(p.type);
|
|
1020
|
+
// In receiver-lambda mode (`requestPage { ... }`) the surrounding closure is
|
|
1021
|
+
// an extension on `MutableList<Pair<String, String>>`, so we elide the
|
|
1022
|
+
// explicit `params.` qualifier (extension functions resolve via implicit
|
|
1023
|
+
// receiver) and route `+=` through `add(pair)` to keep ktlint happy.
|
|
1024
|
+
const callPrefix = receiverMode ? '' : 'params.';
|
|
1025
|
+
const addPair = (pair: string) => (receiverMode ? `add(${pair})` : `params += ${pair}`);
|
|
696
1026
|
if (arrayItem) {
|
|
697
1027
|
// Honor `style: form, explode: false` → comma-joined. Default (explode:true
|
|
698
1028
|
// or unspecified for form) → repeated keys. `p.explode ?? true` matches
|
|
699
1029
|
// the OpenAPI default for query parameters when `style` is form.
|
|
700
1030
|
const explode = p.explode ?? true;
|
|
701
1031
|
const itemExpr = valueExprForQuery(arrayItem);
|
|
1032
|
+
// `it` is the loop variable in the trivial mapping case — `xs.map { it }`
|
|
1033
|
+
// is the identity function, so emit the collection directly when the per-
|
|
1034
|
+
// item expression doesn't transform the value.
|
|
1035
|
+
const isIdentity = itemExpr === 'it';
|
|
702
1036
|
if (!explode) {
|
|
703
1037
|
if (p.required) {
|
|
704
|
-
|
|
1038
|
+
const arg = isIdentity ? prop : `${prop}.map { ${itemExpr} }`;
|
|
1039
|
+
return [`${indent}${callPrefix}addJoinedIfNotNull(${ktLiteral(p.name)}, ${arg})`];
|
|
705
1040
|
}
|
|
706
|
-
|
|
1041
|
+
const arg = isIdentity ? prop : `${prop}?.map { ${itemExpr} }`;
|
|
1042
|
+
return [`${indent}${callPrefix}addJoinedIfNotNull(${ktLiteral(p.name)}, ${arg})`];
|
|
707
1043
|
}
|
|
708
1044
|
if (p.required) {
|
|
709
|
-
|
|
1045
|
+
const arg = isIdentity ? prop : `${prop}.map { ${itemExpr} }`;
|
|
1046
|
+
return [`${indent}${callPrefix}addEach(${ktLiteral(p.name)}, ${arg})`];
|
|
710
1047
|
}
|
|
711
|
-
|
|
1048
|
+
if (isIdentity) {
|
|
1049
|
+
return [`${indent}${prop}?.let { ${callPrefix}addEach(${ktLiteral(p.name)}, it) }`];
|
|
1050
|
+
}
|
|
1051
|
+
return [`${indent}${prop}?.let { ${callPrefix}addEach(${ktLiteral(p.name)}, it.map { ${itemExpr} }) }`];
|
|
712
1052
|
}
|
|
713
|
-
if (p.required) return [`${indent}
|
|
714
|
-
if (inner.kind === 'primitive' && inner.type === 'string') {
|
|
715
|
-
return [`${indent}
|
|
1053
|
+
if (p.required) return [`${indent}${addPair(`${ktLiteral(p.name)} to ${rendered}`)}`];
|
|
1054
|
+
if (inner.kind === 'primitive' && inner.type === 'string' && inner.format !== 'date-time') {
|
|
1055
|
+
return [`${indent}${callPrefix}addIfNotNull(${ktLiteral(p.name)}, ${prop})`];
|
|
716
1056
|
}
|
|
717
|
-
return [`${indent}${prop}?.let {
|
|
1057
|
+
return [`${indent}${prop}?.let { ${addPair(`${ktLiteral(p.name)} to ${queryParamToString(inner, 'it')}`)} }`];
|
|
718
1058
|
}
|
|
719
1059
|
|
|
720
1060
|
function queryParamToString(type: TypeRef, varName: string): string {
|
|
721
1061
|
if (type.kind === 'enum') return `${varName}.value`;
|
|
722
1062
|
if (type.kind === 'nullable') return queryParamToString(type.inner, varName);
|
|
723
|
-
|
|
1063
|
+
// Plain `string` is already the wire type. ISO-8601 strings get promoted to
|
|
1064
|
+
// `OffsetDateTime`, so we need an explicit `.toString()` to serialize them
|
|
1065
|
+
// as the spec-required ISO-8601 representation.
|
|
1066
|
+
if (type.kind === 'primitive' && type.type === 'string') {
|
|
1067
|
+
return type.format === 'date-time' ? `${varName}.toString()` : varName;
|
|
1068
|
+
}
|
|
724
1069
|
return `${varName}.toString()`;
|
|
725
1070
|
}
|
|
726
1071
|
|
|
@@ -840,7 +1185,38 @@ function generateSealedClass(
|
|
|
840
1185
|
): string[] {
|
|
841
1186
|
const lines: string[] = [];
|
|
842
1187
|
const sealedName = sealedGroupName(group.name);
|
|
843
|
-
|
|
1188
|
+
|
|
1189
|
+
// KDoc with Kotlin + Java construction examples. Pick the first variant as
|
|
1190
|
+
// a worked example so callers see the variant constructor in both languages
|
|
1191
|
+
// without us having to enumerate every shape.
|
|
1192
|
+
const example = group.variants[0];
|
|
1193
|
+
if (example) {
|
|
1194
|
+
const exampleVariant = className(example.name);
|
|
1195
|
+
const exampleArgs = example.parameters
|
|
1196
|
+
.map((p) => `${deriveShortPropertyName(p.name, group.name)} = "..."`)
|
|
1197
|
+
.join(', ');
|
|
1198
|
+
lines.push('/**');
|
|
1199
|
+
lines.push(` * Mutually exclusive ${humanize(group.name)} parameter variants.`);
|
|
1200
|
+
lines.push(' *');
|
|
1201
|
+
lines.push(' * Usage from Kotlin:');
|
|
1202
|
+
lines.push(' * ```kotlin');
|
|
1203
|
+
lines.push(` * val target: ${sealedName} = ${sealedName}.${exampleVariant}(${exampleArgs})`);
|
|
1204
|
+
lines.push(' * ```');
|
|
1205
|
+
lines.push(' *');
|
|
1206
|
+
lines.push(' * Usage from Java:');
|
|
1207
|
+
lines.push(' * ```java');
|
|
1208
|
+
lines.push(
|
|
1209
|
+
` * ${sealedName} target = new ${sealedName}.${exampleVariant}(${example.parameters.map(() => '"..."').join(', ')});`,
|
|
1210
|
+
);
|
|
1211
|
+
lines.push(' * ```');
|
|
1212
|
+
lines.push(' *');
|
|
1213
|
+
lines.push(
|
|
1214
|
+
` * Java callers may also use the per-variant overloads on the surrounding API class to skip variant construction entirely.`,
|
|
1215
|
+
);
|
|
1216
|
+
lines.push(' */');
|
|
1217
|
+
} else {
|
|
1218
|
+
lines.push(`/** Mutually exclusive ${humanize(group.name)} parameter variants. */`);
|
|
1219
|
+
}
|
|
844
1220
|
lines.push(`sealed class ${sealedName} {`);
|
|
845
1221
|
for (let vi = 0; vi < group.variants.length; vi++) {
|
|
846
1222
|
const variant = group.variants[vi];
|
|
@@ -870,16 +1246,21 @@ function generateSealedClass(
|
|
|
870
1246
|
}
|
|
871
1247
|
|
|
872
1248
|
/** Emit `when` dispatch that serializes a parameter group into query params. */
|
|
873
|
-
function emitGroupQueryDispatch(
|
|
1249
|
+
function emitGroupQueryDispatch(
|
|
1250
|
+
group: import('@workos/oagen').ParameterGroup,
|
|
1251
|
+
prop: string,
|
|
1252
|
+
indent: string,
|
|
1253
|
+
receiverMode = false,
|
|
1254
|
+
): string[] {
|
|
874
1255
|
const sealedName = sealedGroupName(group.name);
|
|
875
1256
|
const lines: string[] = [];
|
|
876
1257
|
|
|
877
1258
|
if (group.optional) {
|
|
878
1259
|
lines.push(`${indent}if (${prop} != null) {`);
|
|
879
|
-
emitWhenBlock(lines, group, sealedName, prop, `${indent}
|
|
1260
|
+
emitWhenBlock(lines, group, sealedName, prop, `${indent} `, receiverMode);
|
|
880
1261
|
lines.push(`${indent}}`);
|
|
881
1262
|
} else {
|
|
882
|
-
emitWhenBlock(lines, group, sealedName, prop, indent);
|
|
1263
|
+
emitWhenBlock(lines, group, sealedName, prop, indent, receiverMode);
|
|
883
1264
|
}
|
|
884
1265
|
return lines;
|
|
885
1266
|
}
|
|
@@ -920,13 +1301,15 @@ function emitWhenBlock(
|
|
|
920
1301
|
sealedName: string,
|
|
921
1302
|
prop: string,
|
|
922
1303
|
indent: string,
|
|
1304
|
+
receiverMode = false,
|
|
923
1305
|
): void {
|
|
924
1306
|
lines.push(`${indent}when (${prop}) {`);
|
|
925
1307
|
for (const variant of group.variants) {
|
|
926
1308
|
const variantName = className(variant.name);
|
|
927
1309
|
const entries = variant.parameters.map((p) => {
|
|
928
1310
|
const fieldProp = deriveShortPropertyName(p.name, group.name);
|
|
929
|
-
|
|
1311
|
+
const pair = `${ktLiteral(p.name)} to ${prop}.${fieldProp}`;
|
|
1312
|
+
return receiverMode ? `add(${pair})` : `params += ${pair}`;
|
|
930
1313
|
});
|
|
931
1314
|
if (entries.length === 1) {
|
|
932
1315
|
lines.push(`${indent} is ${sealedName}.${variantName} -> ${entries[0]}`);
|