@workos/oagen-emitters 0.8.2 → 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 +12 -0
- package/dist/index.mjs +1 -1
- package/dist/{plugin-CeNME04k.mjs → plugin-Dh9JSScr.mjs} +377 -46
- 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 +32 -1
- package/src/kotlin/naming.ts +58 -4
- package/src/kotlin/resources.ts +314 -22
- package/src/kotlin/suspend.ts +96 -0
- package/src/kotlin/wrappers.ts +49 -8
- package/test/kotlin/resources.test.ts +94 -1
- package/dist/plugin-CeNME04k.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,6 +39,7 @@ 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
|
|
|
@@ -124,6 +126,9 @@ function generateApiClass(
|
|
|
124
126
|
imports.add('com.workos.common.http.Page');
|
|
125
127
|
imports.add('com.workos.common.http.RequestConfig');
|
|
126
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);
|
|
127
132
|
|
|
128
133
|
const body: string[] = [];
|
|
129
134
|
const seenMethods = new Set<string>();
|
|
@@ -223,17 +228,29 @@ function generateApiClass(
|
|
|
223
228
|
for (const line of sealedLines) lines.push(line);
|
|
224
229
|
|
|
225
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`).';
|
|
226
241
|
if (serviceDescription) {
|
|
227
242
|
const docLines = serviceDescription.trim().split('\n');
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
lines.push(' */');
|
|
234
|
-
}
|
|
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(' */');
|
|
235
248
|
} else {
|
|
236
|
-
lines.push(
|
|
249
|
+
lines.push('/**');
|
|
250
|
+
lines.push(` * API accessor for ${mountName}.`);
|
|
251
|
+
lines.push(' *');
|
|
252
|
+
lines.push(` * ${escapeKdoc(suspendNote)}`);
|
|
253
|
+
lines.push(' */');
|
|
237
254
|
}
|
|
238
255
|
// ktlint requires constructor-property parameters on their own line.
|
|
239
256
|
// The property is `internal` so hand-maintained extension files in the
|
|
@@ -346,11 +363,21 @@ function renderMethod(
|
|
|
346
363
|
const groupParamNames = assignGroupParameterNames(op, paramNames);
|
|
347
364
|
|
|
348
365
|
const params: string[] = [];
|
|
349
|
-
|
|
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));
|
|
350
374
|
|
|
351
375
|
const sortedQuery = [...uniqueQuery].sort((a, b) => (a.required === b.required ? 0 : a.required ? -1 : 1));
|
|
352
376
|
for (const qp of sortedQuery) {
|
|
353
|
-
|
|
377
|
+
pushParam(
|
|
378
|
+
renderParam(qp.name, qp.type, qp.required, method.startsWith('list') && qp.name === 'limit'),
|
|
379
|
+
propertyName(qp.name),
|
|
380
|
+
);
|
|
354
381
|
}
|
|
355
382
|
|
|
356
383
|
// Parameter group params (sealed class types)
|
|
@@ -358,9 +385,9 @@ function renderMethod(
|
|
|
358
385
|
const sealedName = sealedGroupName(group.name);
|
|
359
386
|
const prop = groupParamNames.get(group.name)!;
|
|
360
387
|
if (group.optional) {
|
|
361
|
-
|
|
388
|
+
pushParam(` ${prop}: ${sealedName}? = null`, prop);
|
|
362
389
|
} else {
|
|
363
|
-
|
|
390
|
+
pushParam(` ${prop}: ${sealedName}`, prop);
|
|
364
391
|
}
|
|
365
392
|
}
|
|
366
393
|
|
|
@@ -374,14 +401,17 @@ function renderMethod(
|
|
|
374
401
|
if (isPatch && !bf.required) {
|
|
375
402
|
const baseType = mapTypeRef(bf.type);
|
|
376
403
|
imports.add('com.workos.common.http.PatchField');
|
|
377
|
-
|
|
404
|
+
pushParam(
|
|
405
|
+
` ${bodyParamNames.get(bf.name)!}: PatchField<${baseType}> = PatchField.Absent`,
|
|
406
|
+
bodyParamNames.get(bf.name)!,
|
|
407
|
+
);
|
|
378
408
|
} else {
|
|
379
|
-
|
|
409
|
+
pushParam(renderParamNamed(bodyParamNames.get(bf.name)!, bf.type, bf.required), bodyParamNames.get(bf.name)!);
|
|
380
410
|
}
|
|
381
411
|
}
|
|
382
412
|
|
|
383
413
|
// Per-request options trailer (always optional)
|
|
384
|
-
|
|
414
|
+
pushParam(' requestOptions: RequestOptions? = null', 'requestOptions');
|
|
385
415
|
|
|
386
416
|
const returnType = resolveReturnType(plan, imports, ctx);
|
|
387
417
|
const isPaginated = plan.isPaginated && paginatedItemName !== null;
|
|
@@ -445,6 +475,7 @@ function renderMethod(
|
|
|
445
475
|
}
|
|
446
476
|
lines.push(` )`);
|
|
447
477
|
lines.push(' }');
|
|
478
|
+
appendSuspendVariantLines(lines, method, suspendParams, returnType, op.deprecated);
|
|
448
479
|
return lines.join('\n');
|
|
449
480
|
}
|
|
450
481
|
|
|
@@ -580,9 +611,227 @@ function renderMethod(
|
|
|
580
611
|
}
|
|
581
612
|
|
|
582
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
|
+
|
|
583
628
|
return lines.join('\n');
|
|
584
629
|
}
|
|
585
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
|
+
|
|
586
835
|
function resolveReturnType(plan: ReturnType<typeof planOperation>, imports: Set<string>, ctx?: EmitterContext): string {
|
|
587
836
|
const itemName = plan.isPaginated
|
|
588
837
|
? (resolvePaginatedItemName(plan.paginatedItemModelName, ctx) ?? plan.paginatedItemModelName)
|
|
@@ -660,19 +909,25 @@ function buildMethodKdoc(
|
|
|
660
909
|
// ugly — it nudges callers to add real descriptions to the spec.
|
|
661
910
|
const paramDocs: string[] = [];
|
|
662
911
|
const seenParamDocs = new Set<string>();
|
|
663
|
-
const pushParamDoc = (
|
|
912
|
+
const pushParamDoc = (
|
|
913
|
+
name: string,
|
|
914
|
+
sourceName: string,
|
|
915
|
+
description: string | undefined,
|
|
916
|
+
deprecated?: boolean,
|
|
917
|
+
type?: TypeRef,
|
|
918
|
+
) => {
|
|
664
919
|
if (seenParamDocs.has(name)) return;
|
|
665
920
|
seenParamDocs.add(name);
|
|
666
|
-
paramDocs.push(formatParamDoc(name, description, deprecated, sourceName));
|
|
921
|
+
paramDocs.push(formatParamDoc(name, description, deprecated, sourceName, type));
|
|
667
922
|
};
|
|
668
923
|
for (const pp of pathParams) {
|
|
669
|
-
pushParamDoc(propertyName(pp.name), pp.name, pp.description, pp.deprecated);
|
|
924
|
+
pushParamDoc(propertyName(pp.name), pp.name, pp.description, pp.deprecated, pp.type);
|
|
670
925
|
}
|
|
671
926
|
for (const qp of queryParams) {
|
|
672
|
-
pushParamDoc(propertyName(qp.name), qp.name, qp.description, qp.deprecated);
|
|
927
|
+
pushParamDoc(propertyName(qp.name), qp.name, qp.description, qp.deprecated, qp.type);
|
|
673
928
|
}
|
|
674
929
|
for (const bf of bodyFields) {
|
|
675
|
-
pushParamDoc(bodyParamNames.get(bf.name)!, bf.name, bf.description, bf.deprecated);
|
|
930
|
+
pushParamDoc(bodyParamNames.get(bf.name)!, bf.name, bf.description, bf.deprecated, bf.type);
|
|
676
931
|
}
|
|
677
932
|
// Always document the trailing `requestOptions` parameter with a stable,
|
|
678
933
|
// canned phrasing so generated SDKs are consistent and Dokka's coverage
|
|
@@ -714,6 +969,7 @@ function formatParamDoc(
|
|
|
714
969
|
description: string | undefined,
|
|
715
970
|
deprecated?: boolean,
|
|
716
971
|
sourceName?: string,
|
|
972
|
+
type?: TypeRef,
|
|
717
973
|
): string {
|
|
718
974
|
const firstLine = description?.split('\n').find((l) => l.trim()) ?? '';
|
|
719
975
|
const specText = firstLine.trim();
|
|
@@ -722,7 +978,12 @@ function formatParamDoc(
|
|
|
722
978
|
// the spec didn't provide one. Dokka has no `-Xdoclint:missing` analogue,
|
|
723
979
|
// so emitting a placeholder is the only way to guarantee `@param` coverage.
|
|
724
980
|
const fallback = `the ${humanize(sourceName ?? kotlinName)} of the request.`;
|
|
725
|
-
|
|
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;
|
|
726
987
|
const parts = [deprecationNote, text].filter(Boolean).join(' ');
|
|
727
988
|
return `@param ${kotlinName} ${escapeKdoc(parts)}`;
|
|
728
989
|
}
|
|
@@ -924,7 +1185,38 @@ function generateSealedClass(
|
|
|
924
1185
|
): string[] {
|
|
925
1186
|
const lines: string[] = [];
|
|
926
1187
|
const sealedName = sealedGroupName(group.name);
|
|
927
|
-
|
|
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
|
+
}
|
|
928
1220
|
lines.push(`sealed class ${sealedName} {`);
|
|
929
1221
|
for (let vi = 0; vi < group.variants.length; vi++) {
|
|
930
1222
|
const variant = group.variants[vi];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for emitting `suspend` overloads alongside every generated
|
|
3
|
+
* blocking SDK method. The suspend variant simply delegates to the blocking
|
|
4
|
+
* implementation under `withContext(Dispatchers.IO)`, so callers can invoke
|
|
5
|
+
* any operation from a coroutine context without blocking the calling
|
|
6
|
+
* dispatcher.
|
|
7
|
+
*
|
|
8
|
+
* Naming: the suspend variant uses a `Suspend`-suffixed Kotlin source name
|
|
9
|
+
* (e.g. `deleteEndpointSuspend`). This matters because Kotlin does not let
|
|
10
|
+
* a `suspend` function and a non-`suspend` function with the same name and
|
|
11
|
+
* identical value parameters coexist in the same scope — they are not
|
|
12
|
+
* distinguishable at call sites, even with `@JvmName`. (`@JvmName` only
|
|
13
|
+
* disambiguates JVM signatures, not Kotlin source names.) Naming the suspend
|
|
14
|
+
* variant explicitly sidesteps the conflict and makes the choice between
|
|
15
|
+
* blocking and suspending callable obvious at the call site. The matching
|
|
16
|
+
* `@JvmName("...Suspend")` is now technically redundant but kept for
|
|
17
|
+
* explicit clarity in Java interop / tooling.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { escapeReserved } from './naming.js';
|
|
21
|
+
|
|
22
|
+
export interface SuspendParam {
|
|
23
|
+
/** Already-rendered "name: Type" or "name: Type = default" Kotlin declaration. */
|
|
24
|
+
decl: string;
|
|
25
|
+
/** Bare parameter name to forward to the blocking version. */
|
|
26
|
+
name: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Emit a suspend overload that delegates to a blocking method.
|
|
31
|
+
*
|
|
32
|
+
* The emitted lines preserve the parameter declarations (including default
|
|
33
|
+
* values) so callers can invoke the suspend variant with named arguments and
|
|
34
|
+
* skip optional parameters, just as they do with the blocking version.
|
|
35
|
+
*
|
|
36
|
+
* The emitted suspend method is named `${methodName}Suspend` (a distinct
|
|
37
|
+
* Kotlin source name from the blocking method) — see the file-level KDoc for
|
|
38
|
+
* why this is required.
|
|
39
|
+
*/
|
|
40
|
+
export function emitSuspendVariant(opts: {
|
|
41
|
+
methodName: string;
|
|
42
|
+
params: SuspendParam[];
|
|
43
|
+
returnType: string;
|
|
44
|
+
deprecated?: boolean;
|
|
45
|
+
}): string[] {
|
|
46
|
+
const { methodName, params, returnType, deprecated } = opts;
|
|
47
|
+
const suspendName = `${methodName}Suspend`;
|
|
48
|
+
const lines: string[] = [];
|
|
49
|
+
|
|
50
|
+
lines.push(' /**');
|
|
51
|
+
lines.push(` * Coroutine-aware variant of [${escapeReserved(methodName)}]. Use this from`);
|
|
52
|
+
lines.push(' * a `suspend` function or coroutine scope.');
|
|
53
|
+
lines.push(' *');
|
|
54
|
+
lines.push(` * Delegates to the blocking [${escapeReserved(methodName)}] under`);
|
|
55
|
+
lines.push(' * `withContext(Dispatchers.IO)`, so this is safe to call from any');
|
|
56
|
+
lines.push(' * coroutine dispatcher (including `Dispatchers.Main`).');
|
|
57
|
+
lines.push(' */');
|
|
58
|
+
if (deprecated) lines.push(' @Deprecated("Deprecated operation")');
|
|
59
|
+
lines.push(` @JvmName(${jvmNameLiteral(suspendName)})`);
|
|
60
|
+
|
|
61
|
+
const returnClause = returnType === 'Unit' ? '' : `: ${returnType}`;
|
|
62
|
+
const callArgs = params.map((p) => p.name).join(', ');
|
|
63
|
+
|
|
64
|
+
if (params.length === 0) {
|
|
65
|
+
lines.push(` suspend fun ${escapeReserved(suspendName)}()${returnClause} = withContext(Dispatchers.IO) {`);
|
|
66
|
+
lines.push(` ${escapeReserved(methodName)}()`);
|
|
67
|
+
lines.push(' }');
|
|
68
|
+
return lines;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (params.length === 1) {
|
|
72
|
+
const single = params[0].decl.replace(/^\s+/, '');
|
|
73
|
+
lines.push(
|
|
74
|
+
` suspend fun ${escapeReserved(suspendName)}(${single})${returnClause} = withContext(Dispatchers.IO) {`,
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
lines.push(` suspend fun ${escapeReserved(suspendName)}(`);
|
|
78
|
+
for (let i = 0; i < params.length; i++) {
|
|
79
|
+
const suffix = i === params.length - 1 ? '' : ',';
|
|
80
|
+
lines.push(`${params[i].decl}${suffix}`);
|
|
81
|
+
}
|
|
82
|
+
lines.push(` )${returnClause} = withContext(Dispatchers.IO) {`);
|
|
83
|
+
}
|
|
84
|
+
lines.push(` ${escapeReserved(methodName)}(${callArgs})`);
|
|
85
|
+
lines.push(' }');
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Imports a service file needs to declare any suspend overloads.
|
|
91
|
+
*/
|
|
92
|
+
export const SUSPEND_IMPORTS: readonly string[] = ['kotlinx.coroutines.Dispatchers', 'kotlinx.coroutines.withContext'];
|
|
93
|
+
|
|
94
|
+
function jvmNameLiteral(name: string): string {
|
|
95
|
+
return JSON.stringify(name);
|
|
96
|
+
}
|
package/src/kotlin/wrappers.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import type { EmitterContext, ResolvedOperation, ResolvedWrapper } from '@workos/oagen';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
className,
|
|
4
|
+
propertyName,
|
|
5
|
+
ktLiteral,
|
|
6
|
+
clientFieldExpression,
|
|
7
|
+
escapeReserved,
|
|
8
|
+
humanize,
|
|
9
|
+
maybeShortenEnumParamDescription,
|
|
10
|
+
} from './naming.js';
|
|
3
11
|
import { mapTypeRef, mapTypeRefOptional } from './type-map.js';
|
|
4
12
|
import { resolveWrapperParams } from '../shared/wrapper-utils.js';
|
|
5
13
|
import { sortPathParamsByTemplateOrder } from './resources.js';
|
|
6
14
|
import { buildKotlinPathExpression } from './path-expression.js';
|
|
15
|
+
import { emitSuspendVariant, type SuspendParam } from './suspend.js';
|
|
7
16
|
|
|
8
17
|
/**
|
|
9
18
|
* Emit Kotlin wrapper methods for a union-split operation. Each wrapper
|
|
@@ -49,21 +58,28 @@ function emitWrapperMethod(resolvedOp: ResolvedOperation, wrapper: ResolvedWrapp
|
|
|
49
58
|
kdocLines.push(`${wrapperHumanName.charAt(0).toUpperCase()}${wrapperHumanName.slice(1)}.`);
|
|
50
59
|
}
|
|
51
60
|
const paramDocs: string[] = [];
|
|
52
|
-
const pushParamDoc = (
|
|
61
|
+
const pushParamDoc = (
|
|
62
|
+
kotlinName: string,
|
|
63
|
+
sourceName: string,
|
|
64
|
+
description: string | undefined,
|
|
65
|
+
type?: import('@workos/oagen').TypeRef,
|
|
66
|
+
) => {
|
|
53
67
|
const firstLine =
|
|
54
68
|
description
|
|
55
69
|
?.split('\n')
|
|
56
70
|
.find((l) => l.trim())
|
|
57
71
|
?.trim() ?? '';
|
|
58
72
|
const fallback = `the ${humanize(sourceName)} of the request.`;
|
|
59
|
-
|
|
73
|
+
let text = firstLine || fallback;
|
|
74
|
+
const shortened = maybeShortenEnumParamDescription(type, text);
|
|
75
|
+
if (shortened) text = shortened.description;
|
|
60
76
|
paramDocs.push(`@param ${kotlinName} ${escapeKdoc(text)}`);
|
|
61
77
|
};
|
|
62
78
|
for (const pp of pathParams) {
|
|
63
|
-
pushParamDoc(propertyName(pp.name), pp.name, pp.description);
|
|
79
|
+
pushParamDoc(propertyName(pp.name), pp.name, pp.description, pp.type);
|
|
64
80
|
}
|
|
65
81
|
for (const rp of resolvedParams) {
|
|
66
|
-
pushParamDoc(propertyName(rp.paramName), rp.paramName, rp.field?.description);
|
|
82
|
+
pushParamDoc(propertyName(rp.paramName), rp.paramName, rp.field?.description, rp.field?.type);
|
|
67
83
|
}
|
|
68
84
|
// Trailing `requestOptions` parameter — stable canned phrasing.
|
|
69
85
|
pushParamDoc(
|
|
@@ -82,9 +98,17 @@ function emitWrapperMethod(resolvedOp: ResolvedOperation, wrapper: ResolvedWrapp
|
|
|
82
98
|
|
|
83
99
|
lines.push(' @JvmOverloads');
|
|
84
100
|
|
|
85
|
-
// Build the method parameter list: path params, wrapper params, requestOptions
|
|
101
|
+
// Build the method parameter list: path params, wrapper params, requestOptions.
|
|
102
|
+
// `suspendParams` mirrors `params` but tracks the bare parameter name so the
|
|
103
|
+
// suspend overload (emitted at the end of this function) can forward each
|
|
104
|
+
// argument to the blocking implementation.
|
|
86
105
|
const params: string[] = [];
|
|
87
|
-
|
|
106
|
+
const suspendParams: SuspendParam[] = [];
|
|
107
|
+
for (const pp of pathParams) {
|
|
108
|
+
const decl = ` ${propertyName(pp.name)}: String`;
|
|
109
|
+
params.push(decl);
|
|
110
|
+
suspendParams.push({ decl, name: propertyName(pp.name) });
|
|
111
|
+
}
|
|
88
112
|
for (const rp of resolvedParams) {
|
|
89
113
|
const paramName = propertyName(rp.paramName);
|
|
90
114
|
const kotlinType = rp.field
|
|
@@ -95,9 +119,12 @@ function emitWrapperMethod(resolvedOp: ResolvedOperation, wrapper: ResolvedWrapp
|
|
|
95
119
|
? 'String?'
|
|
96
120
|
: 'String';
|
|
97
121
|
const trailer = rp.isOptional ? ' = null' : '';
|
|
98
|
-
|
|
122
|
+
const decl = ` ${paramName}: ${kotlinType}${trailer}`;
|
|
123
|
+
params.push(decl);
|
|
124
|
+
suspendParams.push({ decl, name: paramName });
|
|
99
125
|
}
|
|
100
126
|
params.push(' requestOptions: RequestOptions? = null');
|
|
127
|
+
suspendParams.push({ decl: ' requestOptions: RequestOptions? = null', name: 'requestOptions' });
|
|
101
128
|
|
|
102
129
|
const returnClause = responseClass ? `: ${responseClass}` : '';
|
|
103
130
|
if (params.length === 1) {
|
|
@@ -140,6 +167,7 @@ function emitWrapperMethod(resolvedOp: ResolvedOperation, wrapper: ResolvedWrapp
|
|
|
140
167
|
}
|
|
141
168
|
lines.push(` )`);
|
|
142
169
|
lines.push(' }');
|
|
170
|
+
appendSuspendVariant(lines, method, suspendParams, responseClass ?? 'Unit');
|
|
143
171
|
return lines;
|
|
144
172
|
}
|
|
145
173
|
|
|
@@ -191,9 +219,22 @@ function emitWrapperMethod(resolvedOp: ResolvedOperation, wrapper: ResolvedWrapp
|
|
|
191
219
|
}
|
|
192
220
|
|
|
193
221
|
lines.push(' }');
|
|
222
|
+
appendSuspendVariant(lines, method, suspendParams, responseClass ?? 'Unit');
|
|
194
223
|
return lines;
|
|
195
224
|
}
|
|
196
225
|
|
|
226
|
+
function appendSuspendVariant(
|
|
227
|
+
lines: string[],
|
|
228
|
+
method: string,
|
|
229
|
+
suspendParams: SuspendParam[],
|
|
230
|
+
returnType: string,
|
|
231
|
+
): void {
|
|
232
|
+
lines.push('');
|
|
233
|
+
for (const ln of emitSuspendVariant({ methodName: method, params: suspendParams, returnType })) {
|
|
234
|
+
lines.push(ln);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
197
238
|
function escapeKdoc(s: string): string {
|
|
198
239
|
return s.replace(/\*\//g, '*\u200b/');
|
|
199
240
|
}
|