@workos/oagen-emitters 0.15.2 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +48 -1
  4. package/dist/index.d.mts +51 -2
  5. package/dist/index.d.mts.map +1 -1
  6. package/dist/index.mjs +852 -2
  7. package/dist/index.mjs.map +1 -0
  8. package/dist/{plugin-Xkr83G9A.mjs → plugin-CpO8rePT.mjs} +1219 -493
  9. package/dist/plugin-CpO8rePT.mjs.map +1 -0
  10. package/dist/plugin.mjs +1 -1
  11. package/package.json +7 -7
  12. package/src/dotnet/naming.ts +1 -1
  13. package/src/go/naming.ts +1 -1
  14. package/src/index.ts +15 -0
  15. package/src/node/enums.ts +17 -4
  16. package/src/node/index.ts +264 -4
  17. package/src/node/live-surface.ts +309 -0
  18. package/src/node/models.ts +69 -3
  19. package/src/node/naming.ts +204 -23
  20. package/src/node/resources.ts +39 -3
  21. package/src/node/tests.ts +29 -3
  22. package/src/node/utils.ts +140 -22
  23. package/src/snippets/dotnet.ts +159 -0
  24. package/src/snippets/go.ts +148 -0
  25. package/src/snippets/index.ts +8 -0
  26. package/src/snippets/kotlin.ts +144 -0
  27. package/src/snippets/php.ts +149 -0
  28. package/src/snippets/plugin.ts +36 -0
  29. package/src/snippets/python.ts +135 -0
  30. package/src/snippets/ruby.ts +152 -0
  31. package/src/snippets/rust.ts +189 -0
  32. package/test/node/enums.test.ts +239 -2
  33. package/test/node/live-surface.test.ts +771 -1
  34. package/test/node/models.test.ts +738 -3
  35. package/test/node/naming.test.ts +159 -0
  36. package/test/node/resources.test.ts +464 -0
  37. package/test/node/utils.test.ts +157 -2
  38. package/test/snippets/_helpers.ts +67 -0
  39. package/test/snippets/dotnet.test.ts +49 -0
  40. package/test/snippets/go.test.ts +94 -0
  41. package/test/snippets/kotlin.test.ts +53 -0
  42. package/test/snippets/php.test.ts +48 -0
  43. package/test/snippets/python.test.ts +73 -0
  44. package/test/snippets/ruby.test.ts +339 -0
  45. package/test/snippets/rust.test.ts +76 -0
  46. package/dist/plugin-Xkr83G9A.mjs.map +0 -1
@@ -225,7 +225,12 @@ function ignoredResourceMethodNames(ctx: EmitterContext, resourcePath: string):
225
225
  const methods = new Set<string>();
226
226
  for (const block of content.matchAll(/@oagen-ignore-start[\s\S]*?@oagen-ignore-end/g)) {
227
227
  for (const line of block[0].split('\n')) {
228
- const match = line.match(/^\s{2}(?:(?:public|private|protected)\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(/);
228
+ // Match the method name followed by `(` or by `<` — the latter covers
229
+ // generic methods (`getProfile<T extends ...>(...)`), including
230
+ // multi-line type-parameter lists where the line ends right after `<`.
231
+ // Matching only up to the opening bracket sidesteps balancing nested
232
+ // angle brackets like `<T extends Record<string, unknown> = ...>`.
233
+ const match = line.match(/^\s{2}(?:(?:public|private|protected)\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)\s*[<(]/);
229
234
  if (match) methods.add(match[1]);
230
235
  }
231
236
  }
@@ -310,6 +315,18 @@ function renderOptionsParam(param: OptionsObjectParam): string {
310
315
  return `options${param.optional ? '?' : ''}: ${param.type}`;
311
316
  }
312
317
 
318
+ /**
319
+ * Whether a baseline-derived type reference is a plain TypeScript identifier
320
+ * that can appear in a named import. Baseline (live-SDK) method params can
321
+ * carry inline object-literal TYPES (e.g. `{ intent: GenerateLinkIntent; ... }`);
322
+ * treating that literal text as a type NAME would slugify it into a filename
323
+ * and emit a named import of a brace-expression — both invalid. Literal types
324
+ * are kept inline in the emitted signature instead and never imported.
325
+ */
326
+ function isValidTypeIdentifier(name: string): boolean {
327
+ return /^[A-Za-z_$][\w$]*$/.test(name);
328
+ }
329
+
313
330
  function autoPaginatableItemType(returnType: string | undefined): string | undefined {
314
331
  // Match both AutoPaginatable<T> and the legacy List<T> pattern so baseline
315
332
  // item types are extracted even when the hand-written code predates AutoPaginatable.
@@ -933,6 +950,9 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
933
950
 
934
951
  const importedTypeNames = new Set<string>();
935
952
  for (const optionType of optionObjectTypes) {
953
+ // Inline object-literal types from the baseline surface are rendered
954
+ // inline in the method signature — they have no importable name or file.
955
+ if (!isValidTypeIdentifier(optionType)) continue;
936
956
  if (importedTypeNames.has(optionType)) continue;
937
957
  importedTypeNames.add(optionType);
938
958
  const sourceFile = baselineTypeSourceFile(ctx, optionType);
@@ -1675,7 +1695,17 @@ function renderOptionsObjectMethod(
1675
1695
  const wireType = wireInterfaceName(itemType);
1676
1696
  const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
1677
1697
  const needsWireSerializer = extraParams.some((p) => fieldName(p.name) !== wireFieldName(p.name));
1678
- const paginationType = needsWireSerializer ? 'PaginationOptions' : optionParam.type;
1698
+ // When path params are destructured out of the options object, the value
1699
+ // passed to AutoPaginatable (and to fetchAndDeserialize) is the REST
1700
+ // object — typed Omit<FullOptions, pathFields> — not the full options
1701
+ // interface. Declaring the full interface as the second type argument
1702
+ // fails TS2322 because the rest object lacks the required path-param
1703
+ // fields, so parameterize over the rest type actually passed.
1704
+ const restOptionsType =
1705
+ pathBindings.length > 0
1706
+ ? `Omit<${optionParam.type}, ${pathBindings.map((b) => `'${b}'`).join(' | ')}>`
1707
+ : optionParam.type;
1708
+ const paginationType = needsWireSerializer ? 'PaginationOptions' : restOptionsType;
1679
1709
  const returnType = needsWireSerializer
1680
1710
  ? `Promise<AutoPaginatable<${itemType}, ${paginationType}>>`
1681
1711
  : (preferredBaselineReturnType(ctx, baselineMethod?.returnType) ??
@@ -1793,7 +1823,13 @@ function renderOptionsObjectMethod(
1793
1823
 
1794
1824
  lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${methodReturnType} {`);
1795
1825
  renderOptionsObjectDestructure(lines, pathBindings);
1796
- lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}${queryOptionsArg});`);
1826
+ // POST/PUT/PATCH require the entity argument even without a request body —
1827
+ // the client signature is `post(path, entity, options?)`, so a body-less
1828
+ // call must still pass `{}` or the generated code fails with TS2554.
1829
+ const emptyBodyArg = httpMethodNeedsBody(op.httpMethod) ? ', {}' : '';
1830
+ lines.push(
1831
+ ` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}${emptyBodyArg}${queryOptionsArg});`,
1832
+ );
1797
1833
  if (override?.returnExpression) {
1798
1834
  lines.push(` const result = ${returnExpr};`);
1799
1835
  lines.push(` return ${override.returnExpression};`);
package/src/node/tests.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import type { ApiSpec, Service, Operation, Model, TypeRef, EmitterContext, GeneratedFile } from '@workos/oagen';
3
+ import type {
4
+ ApiSpec,
5
+ Service,
6
+ Operation,
7
+ Model,
8
+ TypeRef,
9
+ Parameter,
10
+ EmitterContext,
11
+ GeneratedFile,
12
+ } from '@workos/oagen';
4
13
  import { planOperation, toCamelCase, toPascalCase } from '@workos/oagen';
5
14
  import { unwrapListModel, ID_PREFIXES } from './fixtures.js';
6
15
  import {
@@ -333,6 +342,20 @@ function pathParamTestValue(param: { type: TypeRef; name?: string } | undefined,
333
342
  return 'test_id';
334
343
  }
335
344
 
345
+ function queryParamTestValue(param: Parameter, modelMap?: Map<string, Model>): string {
346
+ if (param.example !== undefined) {
347
+ if (Array.isArray(param.example)) {
348
+ return `[${param.example.map((v: unknown) => (typeof v === 'string' ? `'${v}'` : String(v))).join(', ')}]`;
349
+ }
350
+ const isDateTime = param.type.kind === 'primitive' && param.type.format === 'date-time';
351
+ if (isDateTime && typeof param.example === 'string') {
352
+ return `new Date('${param.example}')`;
353
+ }
354
+ return typeof param.example === 'string' ? `'${param.example}'` : String(param.example);
355
+ }
356
+ return fixtureValueForType(param.type, param.name, 'Options', modelMap) ?? "'test'";
357
+ }
358
+
336
359
  /** Build test arguments for all path params (handles multiple path params). */
337
360
  function buildTestPathArgs(op: Operation): string {
338
361
  // Detect path template variables (may be more than op.pathParams if spec is incomplete)
@@ -372,7 +395,10 @@ function renderPaginatedTest(
372
395
  const optionsArg = buildOptionsObjectTestArg(op, plan, baselineMethod, modelMap, ctx);
373
396
  const baselineItemType = autoPaginatableItemType(baselineMethod?.returnType);
374
397
  const generatedItemType = ctx ? resolveInterfaceName(itemModelName, ctx) : null;
375
- const skipFieldAssertions = Boolean(baselineItemType && generatedItemType && baselineItemType !== generatedItemType);
398
+ const baselineHasNonPaginatableReturn = Boolean(baselineMethod?.returnType && !baselineItemType);
399
+ const skipFieldAssertions =
400
+ baselineHasNonPaginatableReturn ||
401
+ Boolean(baselineItemType && generatedItemType && baselineItemType !== generatedItemType);
376
402
 
377
403
  lines.push(" it('returns paginated results', async () => {");
378
404
  lines.push(` fetchOnce(list${itemModelName}Fixture);`);
@@ -649,7 +675,7 @@ function buildOptionsObjectTestArg(
649
675
  : op.queryParams;
650
676
  for (const param of queryParams) {
651
677
  const localName = fieldName(param.name);
652
- const value = fixtureValueForType(param.type, param.name, 'Options', modelMap) ?? "'test'";
678
+ const value = queryParamTestValue(param, modelMap);
653
679
  entries.push(`${localName}: ${value}`);
654
680
  }
655
681
 
package/src/node/utils.ts CHANGED
@@ -11,12 +11,15 @@ import { mapTypeRef } from './type-map.js';
11
11
  import {
12
12
  resolveInterfaceName,
13
13
  fieldName,
14
+ fileName,
14
15
  resolveServiceDir,
15
16
  resolveMethodName,
16
17
  buildServiceNameMap,
17
18
  } from './naming.js';
18
- import { getMountTarget } from '../shared/resolved-ops.js';
19
- import { assignModelsToServices } from '@workos/oagen';
19
+ import { getMountTarget, groupByMount } from '../shared/resolved-ops.js';
20
+ import { assignModelsToServices, collectModelRefs, collectFieldDependencies } from '@workos/oagen';
21
+ import { isNodeOwnedService } from './options.js';
22
+ import { liveSurfaceHasExistingSdk, liveSurfaceHasFile } from './live-surface.js';
20
23
 
21
24
  /**
22
25
  * Compute a relative import path between two files within the generated SDK.
@@ -203,7 +206,7 @@ export function createServiceDirResolver(
203
206
  serviceNameMap: Map<string, string>;
204
207
  resolveDir: (irService: string | undefined) => string;
205
208
  } {
206
- const modelToService = assignModelsToServices(models, services, ctx.modelHints);
209
+ const modelToService = assignModelsToEmittableServices(models, services, ctx);
207
210
  const serviceNameMap = buildServiceNameMap(services, ctx);
208
211
 
209
212
  // Per-name → directory override, harvested from the live SDK surface.
@@ -212,25 +215,7 @@ export function createServiceDirResolver(
212
215
  // baseline directory string (e.g., "user-management"). The override map is
213
216
  // attached by tagging the model name with a directory prefix that bypasses
214
217
  // the IR-service lookup. Concretely we keep a side map.
215
- const baselineDirByModel = new Map<string, string>();
216
- const recordSource = (name: string, info: { sourceFile?: string } | undefined) => {
217
- const sourceFile = info?.sourceFile;
218
- if (!sourceFile) return;
219
- const m = sourceFile.match(/^src\/([^/]+)\//);
220
- if (!m) return;
221
- baselineDirByModel.set(name, m[1]);
222
- };
223
- // Both interfaces and type aliases can shadow IR model names — e.g.
224
- // `type Role = EnvironmentRole | OrganizationRole;` is the live SDK's
225
- // canonical Role definition even though the IR represents Role as a model.
226
- for (const [name, info] of Object.entries(ctx.apiSurface?.interfaces ?? {})) {
227
- recordSource(name, info as { sourceFile?: string });
228
- }
229
- for (const [name, info] of Object.entries(ctx.apiSurface?.typeAliases ?? {})) {
230
- if (!baselineDirByModel.has(name)) {
231
- recordSource(name, info as { sourceFile?: string });
232
- }
233
- }
218
+ const baselineDirByModel = harvestBaselineDirByModel(ctx);
234
219
 
235
220
  // Override modelToService for any IR model that has a baseline sourceFile.
236
221
  // We invent a synthetic IR-service key that maps directly to the baseline
@@ -255,6 +240,139 @@ export function createServiceDirResolver(
255
240
  return { modelToService, serviceNameMap, resolveDir };
256
241
  }
257
242
 
243
+ /**
244
+ * Map baseline interface / type-alias names to the top-level `src/<dir>/`
245
+ * their `sourceFile` lives in. Both kinds can shadow IR model names — e.g.
246
+ * `type Role = EnvironmentRole | OrganizationRole;` is the live SDK's
247
+ * canonical Role definition even though the IR represents Role as a model.
248
+ */
249
+ function harvestBaselineDirByModel(ctx: EmitterContext): Map<string, string> {
250
+ const baselineDirByModel = new Map<string, string>();
251
+ const recordSource = (name: string, info: { sourceFile?: string } | undefined) => {
252
+ const sourceFile = info?.sourceFile;
253
+ if (!sourceFile) return;
254
+ const m = sourceFile.match(/^src\/([^/]+)\//);
255
+ if (!m) return;
256
+ baselineDirByModel.set(name, m[1]);
257
+ };
258
+ for (const [name, info] of Object.entries(ctx.apiSurface?.interfaces ?? {})) {
259
+ recordSource(name, info as { sourceFile?: string });
260
+ }
261
+ for (const [name, info] of Object.entries(ctx.apiSurface?.typeAliases ?? {})) {
262
+ if (!baselineDirByModel.has(name)) {
263
+ recordSource(name, info as { sourceFile?: string });
264
+ }
265
+ }
266
+ return baselineDirByModel;
267
+ }
268
+
269
+ /**
270
+ * `assignModelsToServices` plus an owned-service correction pass.
271
+ *
272
+ * The engine's assignment is first-reference-wins: a model referenced by both
273
+ * Organizations and AuditLogs lands in `organizations/` even when only
274
+ * AuditLogs is owned this run. Against an existing SDK, `applyLiveSurface`
275
+ * then drops the model file (a non-owned, non-adopted directory cannot
276
+ * receive new paths) while the owned resource still imports
277
+ * `../organizations/interfaces/<model>.interface` — an import that resolves
278
+ * to nothing (TS2307).
279
+ *
280
+ * The correction re-homes such models to the owned service that references
281
+ * them, so emission and import planning agree on a directory that is allowed
282
+ * to receive files. It only fires when:
283
+ * 1. `ownedServices` is configured and the run targets an existing SDK;
284
+ * 2. the model has no baseline `sourceFile` (otherwise the baseline-dir
285
+ * override keeps imports pointing at the on-disk location);
286
+ * 3. the model's computed interface path does not already exist on disk;
287
+ * 4. the assigned service is neither owned itself nor sharing a directory
288
+ * with an owned service.
289
+ */
290
+ export function assignModelsToEmittableServices(
291
+ models: Model[],
292
+ services: Service[],
293
+ ctx?: EmitterContext,
294
+ ): Map<string, string> {
295
+ const modelToService = assignModelsToServices(models, services, ctx?.modelHints);
296
+ if (ctx) {
297
+ reassignOwnedServiceDependencies(modelToService, models, services, ctx);
298
+ }
299
+ return modelToService;
300
+ }
301
+
302
+ function reassignOwnedServiceDependencies(
303
+ modelToService: Map<string, string>,
304
+ models: Model[],
305
+ services: Service[],
306
+ ctx: EmitterContext,
307
+ ): void {
308
+ const serviceNameMap = buildServiceNameMap(services, ctx);
309
+ // Ownership is a property of the MOUNT target, not the IR service: an op
310
+ // can live on a non-owned IR service (e.g. Organizations, because its path
311
+ // starts with /organizations) while being mounted on an owned service via
312
+ // `resolvedOperations` (e.g. AuditLogs' retention endpoints). Walking only
313
+ // IR services misses such ops entirely, so the models they reference stay
314
+ // assigned to the unemittable IR directory and are never emitted anywhere.
315
+ // Regroup by mount target when resolved operations exist — same as
316
+ // `buildGeneratedResourceModelUsage` and `computeOwnedServiceDirs`.
317
+ const mountGroups = groupByMount(ctx);
318
+ const candidateServices: Service[] =
319
+ mountGroups.size > 0 ? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations })) : services;
320
+ const ownedServices = candidateServices.filter((s) => isNodeOwnedService(ctx, s.name, serviceNameMap.get(s.name)));
321
+ if (ownedServices.length === 0) return;
322
+ // Greenfield generation emits every directory; nothing is unemittable.
323
+ if (!liveSurfaceHasExistingSdk()) return;
324
+
325
+ const dirOf = (irService: string | undefined): string =>
326
+ irService ? resolveServiceDir(serviceNameMap.get(irService) ?? irService) : 'common';
327
+ const ownedDirs = new Set(ownedServices.map((s) => dirOf(s.name)));
328
+ const baselineDirByModel = harvestBaselineDirByModel(ctx);
329
+ const modelsByName = new Map(models.map((m) => [m.name, m]));
330
+
331
+ for (const service of ownedServices) {
332
+ for (const name of collectServiceModelClosure(service, modelsByName)) {
333
+ if (!modelsByName.has(name)) continue;
334
+ if (baselineDirByModel.has(name)) continue; // declared in the baseline → on disk
335
+ const assigned = modelToService.get(name);
336
+ if (assigned && isNodeOwnedService(ctx, assigned, serviceNameMap.get(assigned))) continue;
337
+ const assignedDir = dirOf(assigned);
338
+ if (ownedDirs.has(assignedDir)) continue;
339
+ if (liveSurfaceHasFile(`src/${assignedDir}/interfaces/${fileName(name)}.interface.ts`)) continue;
340
+ modelToService.set(name, service.name);
341
+ }
342
+ }
343
+ }
344
+
345
+ /** Model names referenced by a service's operations, expanded through fields. */
346
+ function collectServiceModelClosure(service: Service, modelsByName: Map<string, Model>): Set<string> {
347
+ const referenced = new Set<string>();
348
+ const add = (ref: TypeRef | undefined): void => {
349
+ if (!ref) return;
350
+ for (const name of collectModelRefs(ref)) referenced.add(name);
351
+ };
352
+ for (const op of service.operations) {
353
+ add(op.requestBody);
354
+ add(op.response);
355
+ for (const param of [...op.pathParams, ...op.queryParams, ...op.headerParams, ...(op.cookieParams ?? [])]) {
356
+ add(param.type);
357
+ }
358
+ if (op.pagination) add(op.pagination.itemType);
359
+ }
360
+
361
+ const queue = [...referenced];
362
+ while (queue.length > 0) {
363
+ const name = queue.pop()!;
364
+ const model = modelsByName.get(name);
365
+ if (!model) continue;
366
+ for (const dep of collectFieldDependencies(model).models) {
367
+ if (!referenced.has(dep)) {
368
+ referenced.add(dep);
369
+ queue.push(dep);
370
+ }
371
+ }
372
+ }
373
+ return referenced;
374
+ }
375
+
258
376
  /**
259
377
  * Check if baseline interface fields appear to contain generic type parameters.
260
378
  *
@@ -0,0 +1,159 @@
1
+ import type { EmitterContext, ExampleBuilder, ResolvedOperation, SnippetArg, SnippetEmitter } from '@workos/oagen';
2
+ import { collectSnippetArgs, collectWrapperArgs } from '@workos/oagen';
3
+ import {
4
+ appendAsyncSuffix,
5
+ className,
6
+ fieldName,
7
+ methodName,
8
+ trimMountedResourceFromMethod,
9
+ } from '../dotnet/naming.js';
10
+
11
+ const INDENT = ' ';
12
+
13
+ export const dotnetSnippetEmitter: SnippetEmitter = {
14
+ language: 'dotnet',
15
+ fileExtension: 'cs',
16
+
17
+ renderOperation(resolved, ctx, examples) {
18
+ if (resolved.urlBuilder) return null;
19
+
20
+ // Mirror the .NET SDK's naming pipeline directly from the resolved op:
21
+ // PascalCase → trim mount-target resource → append Async. We avoid
22
+ // calling back into resolveMethodName so the snippet emitter does not
23
+ // re-validate the whole resolved-ops set on every invocation.
24
+ const rawMethodName =
25
+ resolved.wrappers && resolved.wrappers.length > 0 ? resolved.wrappers[0]!.name : resolved.methodName;
26
+ const stem = trimMountedResourceFromMethod(methodName(rawMethodName), resolved.mountOn);
27
+ const method = appendAsyncSuffix(stem);
28
+
29
+ return renderCall(resolved, ctx, examples, method);
30
+ },
31
+ };
32
+
33
+ function renderCall(
34
+ resolved: ResolvedOperation,
35
+ ctx: EmitterContext,
36
+ examples: ExampleBuilder,
37
+ method: string,
38
+ ): string {
39
+ const accessor = className(resolved.mountOn);
40
+ // Options class follows the `{Resource}{Method}Options` convention used by
41
+ // the SDK (e.g. OrganizationsCreateOptions). The method here already had its
42
+ // resource prefix trimmed and Async suffix appended, so reconstruct.
43
+ const stem = method.replace(/Async$/, '');
44
+ const optsType = `${accessor}${stem}Options`;
45
+
46
+ let args: SnippetArg[];
47
+ let pathArgs: SnippetArg[];
48
+ let optionsArgs: SnippetArg[];
49
+
50
+ if (resolved.wrappers && resolved.wrappers.length > 0) {
51
+ args = collectWrapperArgs(resolved.wrappers[0]!, ctx, examples);
52
+ pathArgs = [];
53
+ optionsArgs = args;
54
+ } else {
55
+ const collected = collectSnippetArgs(resolved, ctx, examples);
56
+ args = collected.args;
57
+ pathArgs = args.filter((a) => a.source === 'path');
58
+ optionsArgs = args.filter((a) => a.source !== 'path');
59
+ }
60
+
61
+ const lines: string[] = [];
62
+ lines.push('using WorkOS;');
63
+ lines.push('');
64
+ lines.push('var client = new WorkOSClient(new WorkOSOptions');
65
+ lines.push('{');
66
+ lines.push(`${INDENT}ApiKey = "sk_example_123456789",`);
67
+ lines.push(`${INDENT}ClientId = "client_123456789",`);
68
+ lines.push('});');
69
+ lines.push('');
70
+
71
+ const callParts: string[] = [];
72
+ for (const p of pathArgs) callParts.push(renderValue(p.value));
73
+ if (optionsArgs.length > 0) callParts.push(renderOptions(optsType, optionsArgs));
74
+
75
+ const lhs = `await client.${accessor}.${method}`;
76
+ if (callParts.length === 0) {
77
+ lines.push(`${lhs}();`);
78
+ } else if (callParts.length === 1) {
79
+ lines.push(`${lhs}(${indentContinuationLines(callParts[0]!, INDENT)});`);
80
+ } else {
81
+ lines.push(`${lhs}(`);
82
+ for (let i = 0; i < callParts.length; i++) {
83
+ const trailing = i < callParts.length - 1 ? ',' : '';
84
+ lines.push(`${INDENT}${indentContinuationLines(callParts[i]!, INDENT)}${trailing}`);
85
+ }
86
+ lines.push(');');
87
+ }
88
+
89
+ return lines.join('\n');
90
+ }
91
+
92
+ function renderOptions(typeName: string, args: SnippetArg[]): string {
93
+ const lines: string[] = [`new ${typeName}`, '{'];
94
+ for (let i = 0; i < args.length; i++) {
95
+ const a = args[i]!;
96
+ const field = fieldName(a.wireName);
97
+ const value = renderValue(a.value);
98
+ const trailing = i < args.length - 1 ? ',' : ',';
99
+ lines.push(`${INDENT}${field} = ${indentContinuationLines(value, INDENT)}${trailing}`);
100
+ }
101
+ lines.push('}');
102
+ return lines.join('\n');
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // C# literal rendering
107
+ // ---------------------------------------------------------------------------
108
+
109
+ function renderValue(value: unknown): string {
110
+ if (value === null || value === undefined) return 'null';
111
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
112
+ if (typeof value === 'number') return String(value);
113
+ if (typeof value === 'string') return csharpString(value);
114
+ if (Array.isArray(value)) return renderArray(value);
115
+ if (typeof value === 'object') return renderDict(value as Record<string, unknown>);
116
+ return 'null';
117
+ }
118
+
119
+ function csharpString(s: string): string {
120
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
121
+ }
122
+
123
+ function renderArray(items: unknown[]): string {
124
+ if (items.length === 0) return 'new[] { }';
125
+ const rendered = items.map((v) => renderValue(v));
126
+ const oneline = `new[] { ${rendered.join(', ')} }`;
127
+ if (oneline.length <= 80 && rendered.every((r) => !r.includes('\n'))) return oneline;
128
+ const lines: string[] = ['new[]', '{'];
129
+ for (let i = 0; i < rendered.length; i++) {
130
+ const trailing = i < rendered.length - 1 ? ',' : ',';
131
+ lines.push(`${INDENT}${indentContinuationLines(rendered[i]!, INDENT)}${trailing}`);
132
+ }
133
+ lines.push('}');
134
+ return lines.join('\n');
135
+ }
136
+
137
+ function renderDict(obj: Record<string, unknown>): string {
138
+ // Plain dictionary literal — the dotnet SDK exposes models, but the snippet
139
+ // doesn't know which model class corresponds to each nested object. Emit a
140
+ // Dictionary<string, object> initializer that the SDK serializer can handle
141
+ // or a developer can replace with the typed model.
142
+ const entries = Object.entries(obj);
143
+ if (entries.length === 0) return 'new Dictionary<string, object>()';
144
+ const rendered = entries.map(([k, v]) => ({ key: k, value: renderValue(v) }));
145
+ const lines: string[] = ['new Dictionary<string, object>', '{'];
146
+ for (let i = 0; i < rendered.length; i++) {
147
+ const e = rendered[i]!;
148
+ const trailing = i < rendered.length - 1 ? ',' : ',';
149
+ lines.push(`${INDENT}{ "${e.key}", ${indentContinuationLines(e.value, INDENT)} }${trailing}`);
150
+ }
151
+ lines.push('}');
152
+ return lines.join('\n');
153
+ }
154
+
155
+ function indentContinuationLines(s: string, indent: string): string {
156
+ if (!s.includes('\n')) return s;
157
+ const lines = s.split('\n');
158
+ return lines.map((line, i) => (i === 0 ? line : `${indent}${line}`)).join('\n');
159
+ }
@@ -0,0 +1,148 @@
1
+ import type { EmitterContext, ExampleBuilder, ResolvedOperation, SnippetArg, SnippetEmitter } from '@workos/oagen';
2
+ import { collectSnippetArgs, collectWrapperArgs } from '@workos/oagen';
3
+ import { className, fieldName, methodName, trimMountedResourceFromMethod } from '../go/naming.js';
4
+
5
+ const INDENT = '\t';
6
+
7
+ export const goSnippetEmitter: SnippetEmitter = {
8
+ language: 'go',
9
+ fileExtension: 'go',
10
+
11
+ renderOperation(resolved, ctx, examples) {
12
+ if (resolved.urlBuilder) return null;
13
+
14
+ // Mirror the Go SDK's naming pipeline: PascalCase the resolved method
15
+ // name (or the wrapper name for split ops), then trim any mount-target
16
+ // resource words from the end so `CreateOrganization` on `Organizations`
17
+ // becomes `Create`. We compute these directly from the resolved op so
18
+ // the snippet emitter does not depend on a fresh `buildResolvedLookup`
19
+ // (which would re-validate the entire spec for uniqueness).
20
+ const rawMethodName =
21
+ resolved.wrappers && resolved.wrappers.length > 0 ? resolved.wrappers[0]!.name : resolved.methodName;
22
+ const method = trimMountedResourceFromMethod(methodName(rawMethodName), resolved.mountOn);
23
+
24
+ return renderCall(resolved, ctx, examples, method);
25
+ },
26
+ };
27
+
28
+ function renderCall(
29
+ resolved: ResolvedOperation,
30
+ ctx: EmitterContext,
31
+ examples: ExampleBuilder,
32
+ method: string,
33
+ ): string {
34
+ const accessorMethod = className(resolved.mountOn);
35
+ const optsTypeName = `${accessorMethod}${method}Params`;
36
+
37
+ let args: SnippetArg[];
38
+ let pathArgs: SnippetArg[];
39
+ let bodyAndQueryArgs: SnippetArg[];
40
+
41
+ if (resolved.wrappers && resolved.wrappers.length > 0) {
42
+ args = collectWrapperArgs(resolved.wrappers[0]!, ctx, examples);
43
+ pathArgs = [];
44
+ bodyAndQueryArgs = args;
45
+ } else {
46
+ const collected = collectSnippetArgs(resolved, ctx, examples);
47
+ args = collected.args;
48
+ pathArgs = args.filter((a) => a.source === 'path');
49
+ bodyAndQueryArgs = args.filter((a) => a.source !== 'path');
50
+ }
51
+
52
+ const lines: string[] = [];
53
+ lines.push('package main');
54
+ lines.push('');
55
+ lines.push('import (');
56
+ lines.push(`${INDENT}"context"`);
57
+ lines.push('');
58
+ lines.push(`${INDENT}"github.com/workos/workos-go/v9"`);
59
+ lines.push(')');
60
+ lines.push('');
61
+ lines.push('func main() {');
62
+ lines.push(`${INDENT}client := workos.NewClient("sk_example_123456789")`);
63
+ lines.push('');
64
+
65
+ // Build the method call. Go SDK methods take (ctx, positional path params..., *OptsStruct).
66
+ const callParts: string[] = ['context.Background()'];
67
+ for (const p of pathArgs) {
68
+ callParts.push(renderValue(p.value));
69
+ }
70
+
71
+ if (bodyAndQueryArgs.length === 0) {
72
+ lines.push(`${INDENT}_, err := client.${accessorMethod}().${method}(${callParts.join(', ')})`);
73
+ } else {
74
+ const optsLines = buildOptsStruct(optsTypeName, bodyAndQueryArgs);
75
+ callParts.push(optsLines);
76
+ const joined = callParts.length === 2 ? callParts.join(', ') : callParts.join(', ');
77
+ lines.push(`${INDENT}_, err := client.${accessorMethod}().${method}(${joined})`);
78
+ }
79
+ lines.push(`${INDENT}if err != nil {`);
80
+ lines.push(`${INDENT}${INDENT}panic(err)`);
81
+ lines.push(`${INDENT}}`);
82
+ lines.push('}');
83
+
84
+ return lines.join('\n');
85
+ }
86
+
87
+ function buildOptsStruct(typeName: string, args: SnippetArg[]): string {
88
+ const lines: string[] = [`&workos.${typeName}{`];
89
+ for (const a of args) {
90
+ const field = fieldName(a.wireName);
91
+ const value = renderValue(a.value);
92
+ const indentedValue = indentContinuationLines(value, `${INDENT}${INDENT}`);
93
+ lines.push(`${INDENT}${INDENT}${field}: ${indentedValue},`);
94
+ }
95
+ lines.push(`${INDENT}}`);
96
+ return lines.join('\n');
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Go literal rendering
101
+ // ---------------------------------------------------------------------------
102
+
103
+ function renderValue(value: unknown): string {
104
+ if (value === null || value === undefined) return 'nil';
105
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
106
+ if (typeof value === 'number') return String(value);
107
+ if (typeof value === 'string') return goString(value);
108
+ if (Array.isArray(value)) return renderSlice(value);
109
+ if (typeof value === 'object') return renderStruct(value as Record<string, unknown>);
110
+ return 'nil';
111
+ }
112
+
113
+ function goString(s: string): string {
114
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
115
+ }
116
+
117
+ function renderSlice(items: unknown[]): string {
118
+ if (items.length === 0) return '[]any{}';
119
+ const rendered = items.map((v) => renderValue(v));
120
+ const oneline = `[]any{${rendered.join(', ')}}`;
121
+ if (oneline.length <= 80 && rendered.every((r) => !r.includes('\n'))) return oneline;
122
+ const lines: string[] = ['[]any{'];
123
+ for (const r of rendered) {
124
+ lines.push(`${INDENT}${indentContinuationLines(r, INDENT)},`);
125
+ }
126
+ lines.push('}');
127
+ return lines.join('\n');
128
+ }
129
+
130
+ function renderStruct(obj: Record<string, unknown>): string {
131
+ const entries = Object.entries(obj);
132
+ if (entries.length === 0) return 'map[string]any{}';
133
+ const rendered = entries.map(([k, v]) => ({ key: k, value: renderValue(v) }));
134
+ const oneline = `map[string]any{${rendered.map((e) => `"${e.key}": ${e.value}`).join(', ')}}`;
135
+ if (oneline.length <= 80 && rendered.every((e) => !e.value.includes('\n'))) return oneline;
136
+ const lines: string[] = ['map[string]any{'];
137
+ for (const e of rendered) {
138
+ lines.push(`${INDENT}"${e.key}": ${indentContinuationLines(e.value, INDENT)},`);
139
+ }
140
+ lines.push('}');
141
+ return lines.join('\n');
142
+ }
143
+
144
+ function indentContinuationLines(s: string, indent: string): string {
145
+ if (!s.includes('\n')) return s;
146
+ const lines = s.split('\n');
147
+ return lines.map((line, i) => (i === 0 ? line : `${indent}${line}`)).join('\n');
148
+ }
@@ -0,0 +1,8 @@
1
+ export { rubySnippetEmitter } from './ruby.js';
2
+ export { pythonSnippetEmitter } from './python.js';
3
+ export { phpSnippetEmitter } from './php.js';
4
+ export { goSnippetEmitter } from './go.js';
5
+ export { dotnetSnippetEmitter } from './dotnet.js';
6
+ export { kotlinSnippetEmitter } from './kotlin.js';
7
+ export { rustSnippetEmitter } from './rust.js';
8
+ export { workosSnippetsPlugin } from './plugin.js';