@workos/oagen-emitters 0.8.0 → 0.8.2

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.
@@ -1,5 +1,5 @@
1
1
  import type { EmitterContext, ResolvedOperation, ResolvedWrapper } from '@workos/oagen';
2
- import { className, propertyName, ktLiteral, clientFieldExpression, escapeReserved } from './naming.js';
2
+ import { className, propertyName, ktLiteral, clientFieldExpression, escapeReserved, humanize } from './naming.js';
3
3
  import { mapTypeRef, mapTypeRefOptional } from './type-map.js';
4
4
  import { resolveWrapperParams } from '../shared/wrapper-utils.js';
5
5
  import { sortPathParamsByTemplateOrder } from './resources.js';
@@ -35,7 +35,11 @@ function emitWrapperMethod(resolvedOp: ResolvedOperation, wrapper: ResolvedWrapp
35
35
 
36
36
  const lines: string[] = [];
37
37
 
38
- // Build KDoc from operation description + @param docs for each wrapper param.
38
+ // Build KDoc: operation description + a `@param` line for *every* parameter
39
+ // (Dokka does not flag missing @param blocks, so coverage has to be enforced
40
+ // at emit time) + `@return` when there's a response model. Spec-provided
41
+ // descriptions are preferred; the fallback is templated from the parameter
42
+ // name so the SDK still compiles cleanly under failOnWarning.
39
43
  const kdocLines: string[] = [];
40
44
  const opDesc = (op.description ?? '').trim();
41
45
  const wrapperHumanName = method.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
@@ -45,29 +49,36 @@ function emitWrapperMethod(resolvedOp: ResolvedOperation, wrapper: ResolvedWrapp
45
49
  kdocLines.push(`${wrapperHumanName.charAt(0).toUpperCase()}${wrapperHumanName.slice(1)}.`);
46
50
  }
47
51
  const paramDocs: string[] = [];
52
+ const pushParamDoc = (kotlinName: string, sourceName: string, description: string | undefined) => {
53
+ const firstLine =
54
+ description
55
+ ?.split('\n')
56
+ .find((l) => l.trim())
57
+ ?.trim() ?? '';
58
+ const fallback = `the ${humanize(sourceName)} of the request.`;
59
+ const text = firstLine || fallback;
60
+ paramDocs.push(`@param ${kotlinName} ${escapeKdoc(text)}`);
61
+ };
48
62
  for (const pp of pathParams) {
49
- if (pp.description?.trim()) {
50
- paramDocs.push(`@param ${propertyName(pp.name)} ${escapeKdoc(pp.description.split('\n')[0].trim())}`);
51
- }
63
+ pushParamDoc(propertyName(pp.name), pp.name, pp.description);
52
64
  }
53
65
  for (const rp of resolvedParams) {
54
- const desc = rp.field?.description?.trim();
55
- if (desc) {
56
- paramDocs.push(`@param ${propertyName(rp.paramName)} ${escapeKdoc(desc.split('\n')[0])}`);
57
- }
66
+ pushParamDoc(propertyName(rp.paramName), rp.paramName, rp.field?.description);
58
67
  }
68
+ // Trailing `requestOptions` parameter — stable canned phrasing.
69
+ pushParamDoc(
70
+ 'requestOptions',
71
+ 'request_options',
72
+ 'per-request overrides (idempotency key, API key, headers, timeout)',
73
+ );
59
74
  if (responseClass) {
60
75
  paramDocs.push(`@return the ${responseClass}`);
61
76
  }
62
- if (paramDocs.length > 0 || kdocLines.length > 0) {
63
- lines.push(' /**');
64
- for (const l of kdocLines) lines.push(` * ${escapeKdoc(l)}`);
65
- if (paramDocs.length > 0) {
66
- lines.push(' *');
67
- for (const p of paramDocs) lines.push(` * ${p}`);
68
- }
69
- lines.push(' */');
70
- }
77
+ lines.push(' /**');
78
+ for (const l of kdocLines) lines.push(` * ${escapeKdoc(l)}`);
79
+ lines.push(' *');
80
+ for (const p of paramDocs) lines.push(` * ${p}`);
81
+ lines.push(' */');
71
82
 
72
83
  lines.push(' @JvmOverloads');
73
84
 
@@ -101,6 +112,37 @@ function emitWrapperMethod(resolvedOp: ResolvedOperation, wrapper: ResolvedWrapp
101
112
  lines.push(` )${returnClause} {`);
102
113
  }
103
114
 
115
+ // The /user_management/authenticate endpoint is union-split into one
116
+ // wrapper per grant_type. Every variant posts the same shape (caller
117
+ // params + grant_type + client_id + client_secret) to the same path with
118
+ // the same response model, so we route through a single `authenticate(...)`
119
+ // private helper instead of duplicating the request boilerplate per grant.
120
+ const inferred = wrapper.inferFromClient ?? [];
121
+ const usesStandardClientCreds = inferred.includes('client_id') && inferred.includes('client_secret');
122
+ if (
123
+ op.path === '/user_management/authenticate' &&
124
+ op.httpMethod.toUpperCase() === 'POST' &&
125
+ responseClass === 'AuthenticateResponse' &&
126
+ typeof wrapper.defaults?.grant_type === 'string' &&
127
+ usesStandardClientCreds
128
+ ) {
129
+ const grantType = wrapper.defaults.grant_type;
130
+ lines.push(` return authenticate(`);
131
+ lines.push(` grantType = ${ktLiteral(grantType)},`);
132
+ lines.push(` requestOptions = requestOptions,`);
133
+ const entryLines = resolvedParams.map((rp) => {
134
+ const paramName = propertyName(rp.paramName);
135
+ return ` ${ktLiteral(rp.paramName)} to ${paramName}`;
136
+ });
137
+ for (let i = 0; i < entryLines.length; i++) {
138
+ const sep = i === entryLines.length - 1 ? '' : ',';
139
+ lines.push(`${entryLines[i]}${sep}`);
140
+ }
141
+ lines.push(` )`);
142
+ lines.push(' }');
143
+ return lines;
144
+ }
145
+
104
146
  // Build body using bodyOf() — consistent with non-wrapper methods.
105
147
  // bodyOf() automatically drops null optional values.
106
148
  const bodyEntries: string[] = [];
@@ -286,12 +286,10 @@ function generateServiceInits(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
286
286
  overwriteExisting: true,
287
287
  });
288
288
 
289
- // Ensure models/__init__.py exists even if no models are assigned to this service
290
- files.push({
291
- path: `src/${ctx.namespace}/${dirName}/models/__init__.py`,
292
- content: '',
293
- skipIfExists: true,
294
- });
289
+ // models/__init__.py is emitted unconditionally by `models.ts` including
290
+ // an empty barrel for services with no models — so we don't need a safety
291
+ // net here. (A `skipIfExists` safety net previously caused stale imports
292
+ // to survive when the underlying module was pruned.)
295
293
  }
296
294
 
297
295
  return files;
@@ -447,6 +447,24 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
447
447
  serviceDirModelPaths.add(`src/${ctx.namespace}/${dirName}/models`);
448
448
  }
449
449
 
450
+ // Emit an empty barrel for every service-models dir that has no symbols of
451
+ // its own (e.g. a service whose models live in another package via
452
+ // cross-domain aliases). Otherwise the live SDK can keep a stale
453
+ // `__init__.py` from a previous spec revision — when the underlying module
454
+ // gets pruned the dangling re-export survives and breaks pyright. Done here
455
+ // (not in client.ts) so a subsequent emission for the same path with real
456
+ // content always wins last-write-wins.
457
+ for (const dirPath of serviceDirModelPaths) {
458
+ if (!symbolsByDir.has(dirPath)) {
459
+ files.push({
460
+ path: `${dirPath}/__init__.py`,
461
+ content: '',
462
+ integrateTarget: true,
463
+ overwriteExisting: true,
464
+ });
465
+ }
466
+ }
467
+
450
468
  for (const [dirPath, names] of symbolsByDir) {
451
469
  // Use `import X as X` syntax for explicit re-exports (required by pyright strict)
452
470
  const uniqueNames = [...new Set(names)].sort();
@@ -719,7 +737,12 @@ function deserializeField(ref: any, accessor: string, isRequired: boolean, walru
719
737
  const dispatchMap = entries.map(([value, modelName]) => `"${value}": ${className(modelName)}`).join(', ');
720
738
  const dataExpr = isRequired ? accessor : walrusVar;
721
739
  const dataCast = `cast(Dict[str, Any], ${dataExpr})`;
722
- const lookupExpr = `{${dispatchMap}}.get(${dataCast}.get("${ref.discriminator.property}"))`;
740
+ // The dispatch dict has `str` keys, so pyright (strict) rejects the
741
+ // raw `Any | None` returned by `.get(prop)` even though `dict.get`
742
+ // accepts any hashable. Cast through `str` to satisfy the parameter
743
+ // type — runtime semantics are unchanged because a missing/`None`
744
+ // discriminator simply misses the dispatch and falls through.
745
+ const lookupExpr = `{${dispatchMap}}.get(cast(str, ${dataCast}.get("${ref.discriminator.property}")))`;
723
746
  const branch = `(_disc.from_dict(${dataCast}) if (_disc := ${lookupExpr}) is not None else ${dataExpr})`;
724
747
  if (isRequired) return branch;
725
748
  return `(${branch}) if (${walrusVar} := ${accessor}) is not None else None`;
@@ -760,6 +783,12 @@ function serializeField(ref: any, accessor: string): string {
760
783
  if (uniqueModels.length === 1) {
761
784
  return `${accessor}.to_dict()`;
762
785
  }
786
+ // Discriminated union: deserialize produced a dataclass instance for
787
+ // known discriminator values and the raw dict for unknowns. Round-trip
788
+ // both — call `.to_dict()` if it exists, otherwise pass through.
789
+ if (ref.discriminator && ref.discriminator.mapping && modelVariants.length > 0) {
790
+ return `${accessor}.to_dict() if hasattr(${accessor}, "to_dict") else ${accessor}`;
791
+ }
763
792
  return accessor;
764
793
  }
765
794
  default: