@workos/oagen-emitters 0.2.1 → 0.4.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.
Files changed (136) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +129 -0
  5. package/dist/index.d.mts +13 -1
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +14549 -3385
  8. package/dist/index.mjs.map +1 -1
  9. package/docs/sdk-architecture/dotnet.md +336 -0
  10. package/docs/sdk-architecture/go.md +338 -0
  11. package/docs/sdk-architecture/php.md +315 -0
  12. package/docs/sdk-architecture/python.md +511 -0
  13. package/oagen.config.ts +328 -2
  14. package/package.json +9 -5
  15. package/scripts/generate-php.js +13 -0
  16. package/scripts/git-push-with-published-oagen.sh +21 -0
  17. package/smoke/sdk-dotnet.ts +45 -12
  18. package/smoke/sdk-go.ts +116 -42
  19. package/smoke/sdk-php.ts +28 -26
  20. package/smoke/sdk-python.ts +5 -2
  21. package/src/dotnet/client.ts +89 -0
  22. package/src/dotnet/enums.ts +323 -0
  23. package/src/dotnet/fixtures.ts +236 -0
  24. package/src/dotnet/index.ts +246 -0
  25. package/src/dotnet/manifest.ts +36 -0
  26. package/src/dotnet/models.ts +344 -0
  27. package/src/dotnet/naming.ts +330 -0
  28. package/src/dotnet/resources.ts +622 -0
  29. package/src/dotnet/tests.ts +693 -0
  30. package/src/dotnet/type-map.ts +201 -0
  31. package/src/dotnet/wrappers.ts +186 -0
  32. package/src/go/client.ts +141 -0
  33. package/src/go/enums.ts +196 -0
  34. package/src/go/fixtures.ts +212 -0
  35. package/src/go/index.ts +84 -0
  36. package/src/go/manifest.ts +36 -0
  37. package/src/go/models.ts +254 -0
  38. package/src/go/naming.ts +179 -0
  39. package/src/go/resources.ts +827 -0
  40. package/src/go/tests.ts +751 -0
  41. package/src/go/type-map.ts +82 -0
  42. package/src/go/wrappers.ts +261 -0
  43. package/src/index.ts +4 -0
  44. package/src/kotlin/client.ts +53 -0
  45. package/src/kotlin/enums.ts +162 -0
  46. package/src/kotlin/index.ts +92 -0
  47. package/src/kotlin/manifest.ts +55 -0
  48. package/src/kotlin/models.ts +395 -0
  49. package/src/kotlin/naming.ts +223 -0
  50. package/src/kotlin/overrides.ts +25 -0
  51. package/src/kotlin/resources.ts +667 -0
  52. package/src/kotlin/tests.ts +1019 -0
  53. package/src/kotlin/type-map.ts +123 -0
  54. package/src/kotlin/wrappers.ts +168 -0
  55. package/src/node/client.ts +128 -115
  56. package/src/node/enums.ts +9 -0
  57. package/src/node/errors.ts +37 -232
  58. package/src/node/field-plan.ts +726 -0
  59. package/src/node/fixtures.ts +9 -1
  60. package/src/node/index.ts +3 -9
  61. package/src/node/models.ts +178 -21
  62. package/src/node/naming.ts +49 -111
  63. package/src/node/resources.ts +527 -397
  64. package/src/node/sdk-errors.ts +41 -0
  65. package/src/node/tests.ts +69 -19
  66. package/src/node/type-map.ts +4 -2
  67. package/src/node/utils.ts +13 -71
  68. package/src/node/wrappers.ts +151 -0
  69. package/src/php/client.ts +179 -0
  70. package/src/php/enums.ts +67 -0
  71. package/src/php/errors.ts +9 -0
  72. package/src/php/fixtures.ts +181 -0
  73. package/src/php/index.ts +96 -0
  74. package/src/php/manifest.ts +36 -0
  75. package/src/php/models.ts +310 -0
  76. package/src/php/naming.ts +279 -0
  77. package/src/php/resources.ts +636 -0
  78. package/src/php/tests.ts +609 -0
  79. package/src/php/type-map.ts +90 -0
  80. package/src/php/utils.ts +18 -0
  81. package/src/php/wrappers.ts +152 -0
  82. package/src/python/client.ts +345 -0
  83. package/src/python/enums.ts +313 -0
  84. package/src/python/fixtures.ts +196 -0
  85. package/src/python/index.ts +95 -0
  86. package/src/python/manifest.ts +38 -0
  87. package/src/python/models.ts +688 -0
  88. package/src/python/naming.ts +189 -0
  89. package/src/python/resources.ts +1322 -0
  90. package/src/python/tests.ts +1335 -0
  91. package/src/python/type-map.ts +93 -0
  92. package/src/python/wrappers.ts +191 -0
  93. package/src/shared/model-utils.ts +472 -0
  94. package/src/shared/naming-utils.ts +154 -0
  95. package/src/shared/non-spec-services.ts +54 -0
  96. package/src/shared/resolved-ops.ts +109 -0
  97. package/src/shared/wrapper-utils.ts +70 -0
  98. package/test/dotnet/client.test.ts +121 -0
  99. package/test/dotnet/enums.test.ts +193 -0
  100. package/test/dotnet/errors.test.ts +9 -0
  101. package/test/dotnet/manifest.test.ts +82 -0
  102. package/test/dotnet/models.test.ts +260 -0
  103. package/test/dotnet/resources.test.ts +255 -0
  104. package/test/dotnet/tests.test.ts +202 -0
  105. package/test/go/client.test.ts +92 -0
  106. package/test/go/enums.test.ts +132 -0
  107. package/test/go/errors.test.ts +9 -0
  108. package/test/go/models.test.ts +265 -0
  109. package/test/go/resources.test.ts +408 -0
  110. package/test/go/tests.test.ts +143 -0
  111. package/test/kotlin/models.test.ts +135 -0
  112. package/test/kotlin/tests.test.ts +176 -0
  113. package/test/node/client.test.ts +92 -12
  114. package/test/node/enums.test.ts +2 -0
  115. package/test/node/errors.test.ts +2 -41
  116. package/test/node/models.test.ts +2 -0
  117. package/test/node/naming.test.ts +23 -0
  118. package/test/node/resources.test.ts +315 -84
  119. package/test/node/serializers.test.ts +3 -1
  120. package/test/node/type-map.test.ts +11 -0
  121. package/test/php/client.test.ts +95 -0
  122. package/test/php/enums.test.ts +173 -0
  123. package/test/php/errors.test.ts +9 -0
  124. package/test/php/models.test.ts +497 -0
  125. package/test/php/resources.test.ts +682 -0
  126. package/test/php/tests.test.ts +185 -0
  127. package/test/python/client.test.ts +200 -0
  128. package/test/python/enums.test.ts +228 -0
  129. package/test/python/errors.test.ts +16 -0
  130. package/test/python/manifest.test.ts +74 -0
  131. package/test/python/models.test.ts +716 -0
  132. package/test/python/resources.test.ts +617 -0
  133. package/test/python/tests.test.ts +202 -0
  134. package/src/node/common.ts +0 -273
  135. package/src/node/config.ts +0 -71
  136. package/src/node/serializers.ts +0 -746
package/smoke/sdk-go.ts CHANGED
@@ -140,6 +140,43 @@ function loadManifest(sdkPath: string): Map<string, ManifestEntry> | null {
140
140
  return manifest;
141
141
  }
142
142
 
143
+ // ---------------------------------------------------------------------------
144
+ // Accessor map -- discover actual method names from the generated workos.go
145
+ // ---------------------------------------------------------------------------
146
+
147
+ function buildAccessorMap(sdkPath: string): Map<string, string> {
148
+ const map = new Map<string, string>();
149
+ // Find the main workos.go or *.go file that has Client methods
150
+ const candidates = ['workos.go'];
151
+ for (const fname of candidates) {
152
+ const fpath = resolve(sdkPath, fname);
153
+ if (!existsSync(fpath)) continue;
154
+ const content = readFileSync(fpath, 'utf-8');
155
+ // Match: func (c *Client) ServiceName() *serviceNameService {
156
+ const re = /func \(c \*Client\) (\w+)\(\)/g;
157
+ let m;
158
+ while ((m = re.exec(content)) !== null) {
159
+ const accessor = m[1];
160
+ // Map by lowercase key for case-insensitive matching
161
+ map.set(accessor.toLowerCase(), accessor);
162
+ }
163
+ }
164
+ return map;
165
+ }
166
+
167
+ /**
168
+ * Resolve the Go service accessor name from the manifest service name.
169
+ * Uses the accessor map (built from the generated SDK) for exact matching.
170
+ * Falls back to PascalCase for services not found in the map.
171
+ */
172
+ function resolveAccessorName(manifestService: string, accessorMap: Map<string, string>): string {
173
+ // Try case-insensitive lookup: "sso" -> "SSO", "api_keys" -> "ApiKeys"
174
+ const pascalized = toPascalCase(manifestService);
175
+ const found = accessorMap.get(pascalized.toLowerCase());
176
+ if (found) return found;
177
+ return pascalized;
178
+ }
179
+
143
180
  // ---------------------------------------------------------------------------
144
181
  // Method resolution
145
182
  // ---------------------------------------------------------------------------
@@ -345,15 +382,16 @@ function detectModulePath(sdkPath: string): string {
345
382
  const match = goMod.match(/^module\s+(\S+)/m);
346
383
  if (match) return match[1];
347
384
  }
348
- return 'github.com/workos/workos-go/v4';
385
+ return 'github.com/workos/workos-go/v2';
349
386
  }
350
387
 
351
388
  function generateGoImports(
352
389
  modulePath: string,
353
- servicePackages: Set<string>,
390
+ _servicePackages: Set<string>,
354
391
  needsJson: boolean,
355
- needsServicePkg: boolean,
392
+ _needsServicePkg: boolean,
356
393
  ): string {
394
+ // New emitter: flat package -- everything lives in the root module, no sub-packages.
357
395
  const lines: string[] = [];
358
396
  lines.push('import (');
359
397
  lines.push('\t"context"');
@@ -363,20 +401,21 @@ function generateGoImports(
363
401
  lines.push('\t"fmt"');
364
402
  lines.push('\t"os"');
365
403
  lines.push('');
366
- lines.push(`\tworkos "${modulePath}/pkg"`);
367
- if (needsServicePkg) {
368
- for (const pkg of [...servicePackages].sort()) {
369
- lines.push(`\t"${modulePath}/pkg/${pkg}"`);
370
- }
371
- }
404
+ lines.push(`\t"${modulePath}"`);
372
405
  lines.push(')');
373
406
  return lines.join('\n');
374
407
  }
375
408
 
376
- function generateGoPayloadStruct(payload: Record<string, unknown>, optsType: string, servicePackage: string): string {
409
+ function generateGoPayloadStruct(payload: Record<string, unknown>, paramsType: string): string {
410
+ // New emitter: all types in root workos package, params are pointers.
411
+ // Skip nested objects, arrays, and nil values since Go requires typed structs
412
+ // and primitive fields can't be nil.
377
413
  const lines: string[] = [];
378
- lines.push(`${servicePackage}.${optsType}{`);
414
+ lines.push(`&workos.${paramsType}{`);
379
415
  for (const [key, value] of Object.entries(payload)) {
416
+ // Skip nil, nested objects, and arrays
417
+ if (value === null || value === undefined) continue;
418
+ if (typeof value === 'object') continue;
380
419
  const goField = goFieldName(key);
381
420
  lines.push(`\t\t${goField}: ${goLiteral(value)},`);
382
421
  }
@@ -414,9 +453,9 @@ function generateGoCallBlock(
414
453
  pathParams: Record<string, string>,
415
454
  spec: any,
416
455
  callIndex: number,
456
+ accessorMap: Map<string, string>,
417
457
  ): string {
418
458
  const lines: string[] = [];
419
- const servicePackage = goServicePackageName(resolution.service);
420
459
  const method = resolution.method;
421
460
 
422
461
  // Build arguments
@@ -427,52 +466,63 @@ function generateGoCallBlock(
427
466
  args.push(`"${pathParams[p.name] || ''}"`);
428
467
  }
429
468
 
430
- // Request body opts struct
469
+ // Build service-prefixed params struct name (matches emitter's paramsStructName)
470
+ const servicePrefix = goExportedName(resolution.service);
471
+ const paramsTypeName = method.startsWith(servicePrefix) ? `${method}Params` : `${servicePrefix}${method}Params`;
472
+
473
+ // Request body params struct (emitter uses &workos.{ServicePrefix}{Method}Params{...})
474
+ const hasQueryParams = op.queryParams && op.queryParams.length > 0;
431
475
  if (op.requestBody) {
432
476
  const payload = generatePayload(op, spec);
433
477
  if (payload && Object.keys(payload).length > 0) {
434
- const optsType = `${method}Opts`;
435
- args.push(generateGoPayloadStruct(payload, optsType, servicePackage));
436
- }
437
- }
438
-
439
- // Paginated operations: pass opts with Limit=1
440
- if (op.pagination && !op.requestBody) {
441
- const extraParams = op.queryParams.filter((p: any) => !['limit', 'before', 'after', 'order'].includes(p.name));
442
- if (extraParams.length > 0) {
443
- // Match the emitter convention: List → ListFilterOpts, others → ${method}Opts
444
- const optsType = method === 'List' ? 'ListFilterOpts' : `${method}Opts`;
445
- args.push(`${servicePackage}.${optsType}{Limit: 1}`);
478
+ args.push(generateGoPayloadStruct(payload, paramsTypeName));
446
479
  } else {
447
- args.push(`${servicePackage}.ListOpts{Limit: 1}`);
480
+ // Even with empty payload, the method signature requires the params arg
481
+ args.push(`&workos.${paramsTypeName}{}`);
448
482
  }
483
+ } else if (op.pagination || hasQueryParams) {
484
+ // Paginated or query-param operations need a params struct
485
+ args.push(`&workos.${paramsTypeName}{}`);
449
486
  }
450
487
 
451
- // Determine the service accessor on the client
452
- const serviceProp = goExportedName(resolution.service);
488
+ // Service accessor: resolve from the generated SDK's actual accessor names
489
+ const serviceProp = resolveAccessorName(resolution.service, accessorMap);
453
490
 
454
491
  lines.push(`\t// Call ${callIndex}: ${op.httpMethod.toUpperCase()} ${op.path}`);
455
492
  lines.push(`\tfmt.Fprintf(os.Stderr, "CALL_START:${callIndex}\\n")`);
456
493
 
457
- // Determine return type: paginated and GET-with-response return (result, error),
458
- // DELETE returns just error
494
+ // Determine return type: paginated returns Iterator, DELETE and void/redirect return just error
459
495
  const isDelete = op.httpMethod === 'delete';
460
- const hasResponse = !isDelete;
461
-
462
- if (hasResponse) {
463
- lines.push(`\tresult${callIndex}, err${callIndex} := client.${serviceProp}.${method}(${args.join(', ')})`);
496
+ const isPaginated = !!op.pagination;
497
+ const isVoidResponse =
498
+ !isPaginated &&
499
+ !isDelete &&
500
+ ((op.response.kind === 'primitive' && (op.response as any).type === 'unknown') ||
501
+ (op.successResponses && op.successResponses.some((r: any) => r.statusCode >= 300 && r.statusCode < 400)));
502
+
503
+ if (isPaginated) {
504
+ // Iterator-based: call Next() once to trigger the first HTTP request
505
+ lines.push(`\titer${callIndex} := client.${serviceProp}().${method}(${args.join(', ')})`);
506
+ lines.push(`\titer${callIndex}.Next()`);
507
+ lines.push(`\tif err${callIndex} := iter${callIndex}.Err(); err${callIndex} != nil {`);
508
+ lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_ERROR:${callIndex}:%s\\n", err${callIndex}.Error())`);
509
+ lines.push('\t} else {');
510
+ lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_OK:${callIndex}:\\n")`);
511
+ lines.push('\t}');
512
+ } else if (isDelete || isVoidResponse) {
513
+ lines.push(`\terr${callIndex} := client.${serviceProp}().${method}(${args.join(', ')})`);
464
514
  lines.push(`\tif err${callIndex} != nil {`);
465
515
  lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_ERROR:${callIndex}:%s\\n", err${callIndex}.Error())`);
466
516
  lines.push('\t} else {');
467
- lines.push(`\t\tjsonResult${callIndex}, _ := json.Marshal(result${callIndex})`);
468
- lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_OK:${callIndex}:%s\\n", string(jsonResult${callIndex}))`);
517
+ lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_OK:${callIndex}:\\n")`);
469
518
  lines.push('\t}');
470
519
  } else {
471
- lines.push(`\terr${callIndex} := client.${serviceProp}.${method}(${args.join(', ')})`);
520
+ lines.push(`\tresult${callIndex}, err${callIndex} := client.${serviceProp}().${method}(${args.join(', ')})`);
472
521
  lines.push(`\tif err${callIndex} != nil {`);
473
522
  lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_ERROR:${callIndex}:%s\\n", err${callIndex}.Error())`);
474
523
  lines.push('\t} else {');
475
- lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_OK:${callIndex}:\\n")`);
524
+ lines.push(`\t\tjsonResult${callIndex}, _ := json.Marshal(result${callIndex})`);
525
+ lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_OK:${callIndex}:%s\\n", string(jsonResult${callIndex}))`);
476
526
  lines.push('\t}');
477
527
  }
478
528
 
@@ -523,6 +573,11 @@ async function main(): Promise<void> {
523
573
  // Load manifest
524
574
  const manifest = loadManifest(sdkPath);
525
575
 
576
+ // Build accessor name map by scanning the generated workos.go for
577
+ // `func (c *Client) XxxYyy()` patterns, then matching them to manifest
578
+ // service names via case-insensitive comparison.
579
+ const accessorMap = buildAccessorMap(sdkPath);
580
+
526
581
  const baseUrl = process.env.WORKOS_BASE_URL || spec.baseUrl;
527
582
 
528
583
  // Start capture proxy
@@ -633,7 +688,7 @@ async function main(): Promise<void> {
633
688
  // Generate all call blocks for this wave
634
689
  const callBlocks: string[] = [];
635
690
  for (const call of plannedCalls) {
636
- callBlocks.push(generateGoCallBlock(call.op, call.resolution, call.pathParams, spec, call.index));
691
+ callBlocks.push(generateGoCallBlock(call.op, call.resolution, call.pathParams, spec, call.index, accessorMap));
637
692
  }
638
693
 
639
694
  const imports = generateGoImports(modulePath, servicePackages, needsJson, needsServicePkg);
@@ -644,7 +699,7 @@ async function main(): Promise<void> {
644
699
  imports,
645
700
  '',
646
701
  'func main() {',
647
- `\tclient := workos.NewClient("${apiKey}", workos.WithEndpoint("http://127.0.0.1:${proxyPort}"))`,
702
+ `\tclient := workos.NewClient("${apiKey}", workos.WithBaseURL("http://127.0.0.1:${proxyPort}"))`,
648
703
  '\tctx := context.Background()',
649
704
  '',
650
705
  ...callBlocks,
@@ -660,8 +715,10 @@ async function main(): Promise<void> {
660
715
 
661
716
  // Step 1: Build (sync — no proxy needed during compilation)
662
717
  let buildError: string | null = null;
718
+
719
+ // Run go mod tidy first to resolve dependencies
663
720
  try {
664
- execSync('go build -o smoke-driver main.go', {
721
+ execSync('go mod tidy', {
665
722
  cwd: tmpDir,
666
723
  timeout: 120_000,
667
724
  env: {
@@ -673,9 +730,26 @@ async function main(): Promise<void> {
673
730
  });
674
731
  } catch (err: any) {
675
732
  const stderr = typeof err.stderr === 'string' ? err.stderr : '';
676
- buildError = stderr.trim().split('\n').slice(0, 5).join(' ') || 'go build failed';
733
+ buildError = `go mod tidy failed: ${stderr.trim().split('\n').slice(0, 3).join(' ')}`;
677
734
  }
678
735
 
736
+ if (!buildError)
737
+ try {
738
+ execSync('go build -o smoke-driver main.go', {
739
+ cwd: tmpDir,
740
+ timeout: 120_000,
741
+ env: {
742
+ ...process.env,
743
+ GOPATH: process.env.GOPATH || resolve(process.env.HOME || '~', 'go'),
744
+ },
745
+ encoding: 'utf-8',
746
+ stdio: ['pipe', 'pipe', 'pipe'],
747
+ });
748
+ } catch (err: any) {
749
+ const stderr = typeof err.stderr === 'string' ? err.stderr : '';
750
+ buildError = stderr.trim().split('\n').slice(0, 5).join(' ') || 'go build failed';
751
+ }
752
+
679
753
  if (buildError) {
680
754
  // Build failure affects entire wave
681
755
  const elapsed = Date.now() - waveStart;
package/smoke/sdk-php.ts CHANGED
@@ -184,8 +184,8 @@ function loadManifest(sdkPath: string): Map<string, ManifestEntry> | null {
184
184
  // ---------------------------------------------------------------------------
185
185
 
186
186
  interface MethodResolution {
187
- className: string;
188
- method: string;
187
+ service: string; // camelCase accessor on client (e.g., "organizations")
188
+ method: string; // camelCase method name (e.g., "get")
189
189
  tier: ExchangeProvenance['resolutionTier'];
190
190
  confidence: number;
191
191
  }
@@ -200,14 +200,12 @@ function resolveMethod(
200
200
  if (manifest) {
201
201
  const entry = manifest.get(httpKey);
202
202
  if (entry) {
203
- const className = entry.service.charAt(0).toUpperCase() + entry.service.slice(1);
204
- return { className, method: entry.sdkMethod, tier: 'manifest', confidence: 1.0 };
203
+ return { service: entry.service, method: entry.sdkMethod, tier: 'manifest', confidence: 1.0 };
205
204
  }
206
205
  }
207
206
 
208
207
  const sdkProp = SERVICE_PROPERTY_MAP[irService] || toCamelCase(irService);
209
- const className = sdkProp.charAt(0).toUpperCase() + sdkProp.slice(1);
210
- return { className, method: toCamelCase(op.name), tier: 'exact', confidence: 0.8 };
208
+ return { service: sdkProp, method: toCamelCase(op.name), tier: 'exact', confidence: 0.8 };
211
209
  }
212
210
 
213
211
  // ---------------------------------------------------------------------------
@@ -278,9 +276,14 @@ function buildBatchedPhpScript(
278
276
  }
279
277
  lines.push('');
280
278
 
281
- // Configure SDK
282
- lines.push(`${namespace}\\Client::setApiKey('${apiKey}');`);
283
- lines.push(`${namespace}\\Client::setBaseUrl('http://127.0.0.1:${proxyPort}');`);
279
+ // Configure SDK — generated SDK uses instance-based client with Guzzle handler
280
+ lines.push(`use GuzzleHttp\\HandlerStack;`);
281
+ lines.push(`use GuzzleHttp\\Handler\\CurlHandler;`);
282
+ lines.push('');
283
+ lines.push(`$client = new ${namespace}\\${namespace}(`);
284
+ lines.push(` apiKey: '${escapePhpString(apiKey)}',`);
285
+ lines.push(` baseUrl: 'http://127.0.0.1:${proxyPort}',`);
286
+ lines.push(');');
284
287
  lines.push('');
285
288
 
286
289
  for (const call of calls) {
@@ -289,7 +292,8 @@ function buildBatchedPhpScript(
289
292
  // Marker: start
290
293
  lines.push(`fwrite(STDERR, "OAGEN_CALL_START:${index}\\n");`);
291
294
 
292
- // Build arguments
295
+ // Build arguments — generated PHP SDK takes positional path params,
296
+ // then named keyword args for body fields and query params
293
297
  const phpArgs: string[] = [];
294
298
 
295
299
  for (const p of op.pathParams) {
@@ -299,33 +303,31 @@ function buildBatchedPhpScript(
299
303
 
300
304
  if (op.requestBody) {
301
305
  const payload = generatePayload(op, spec);
302
- if (payload && Object.keys(payload).length > 0) {
303
- phpArgs.push(phpArrayLiteral(payload));
304
- } else {
305
- phpArgs.push('[]');
306
+ if (payload && typeof payload === 'object') {
307
+ // Pass as named arguments (the generated SDK uses promoted properties)
308
+ for (const [key, value] of Object.entries(payload)) {
309
+ phpArgs.push(`${toCamelCase(key)}: ${phpArrayLiteral(value)}`);
310
+ }
306
311
  }
307
312
  }
308
313
 
309
314
  if (!op.requestBody && op.queryParams.some((p) => p.required)) {
310
315
  const queryOpts = generateQueryParams(op, spec);
311
- if (Object.keys(queryOpts).length > 0) {
312
- phpArgs.push(phpArrayLiteral(queryOpts));
316
+ for (const [key, value] of Object.entries(queryOpts)) {
317
+ phpArgs.push(`${toCamelCase(key)}: ${phpArrayLiteral(value)}`);
313
318
  }
314
319
  }
315
320
 
316
- if (op.pagination && phpArgs.length === 0) {
317
- phpArgs.push("['limit' => 1]");
318
- } else if (op.pagination && !op.requestBody) {
319
- const last = phpArgs[phpArgs.length - 1];
320
- if (last && last.startsWith('[')) {
321
- phpArgs[phpArgs.length - 1] = last.replace(/\]$/, ", 'limit' => 1]");
322
- } else {
323
- phpArgs.push("['limit' => 1]");
321
+ if (op.pagination) {
322
+ if (!phpArgs.some((a) => a.startsWith('limit:'))) {
323
+ phpArgs.push('limit: 1');
324
324
  }
325
325
  }
326
326
 
327
+ // The generated SDK uses $client->resource()->method(...) pattern
328
+ const serviceAccessor = resolution.service;
327
329
  lines.push('try {');
328
- lines.push(` $result = ${namespace}\\${resolution.className}::${resolution.method}(${phpArgs.join(', ')});`);
330
+ lines.push(` $result = $client->${serviceAccessor}()->${resolution.method}(${phpArgs.join(', ')});`);
329
331
  lines.push(` fwrite(STDERR, "OAGEN_CALL_OK:${index}\\n");`);
330
332
  lines.push('} catch (\\Throwable $e) {');
331
333
  lines.push(` fwrite(STDERR, "OAGEN_CALL_ERROR:${index}:" . $e->getMessage() . "\\n");`);
@@ -669,7 +671,7 @@ function buildExchange(
669
671
  provenance: {
670
672
  resolutionTier: resolution.tier,
671
673
  resolutionConfidence: resolution.confidence,
672
- sdkMethodName: `${resolution.className}::${resolution.method}`,
674
+ sdkMethodName: `${resolution.service}->${resolution.method}`,
673
675
  captureIndex: 0,
674
676
  totalCaptures: 1,
675
677
  },
@@ -284,7 +284,10 @@ function buildBatchedPythonScript(
284
284
  calls: PlannedCall[],
285
285
  spec: any,
286
286
  ): string {
287
- const srcPath = resolve(sdkPath, 'src');
287
+ // Use src/ subdirectory if it exists, otherwise use the SDK root directly.
288
+ // Generated SDKs use a flat layout (workos/ at root), while some hand-written
289
+ // SDKs nest under src/.
290
+ const srcPath = existsSync(resolve(sdkPath, 'src')) ? resolve(sdkPath, 'src') : resolve(sdkPath);
288
291
  const lines: string[] = [];
289
292
 
290
293
  // Preamble -- loaded once
@@ -504,7 +507,7 @@ async function main(): Promise<void> {
504
507
  const child = spawn(python3Path, [scriptPath], {
505
508
  env: {
506
509
  ...process.env,
507
- PYTHONPATH: resolve(sdkPath, 'src'),
510
+ PYTHONPATH: existsSync(resolve(sdkPath, 'src')) ? resolve(sdkPath, 'src') : resolve(sdkPath),
508
511
  PYTHONDONTWRITEBYTECODE: '1',
509
512
  },
510
513
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -0,0 +1,89 @@
1
+ import type { ApiSpec, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
2
+ import { toPascalCase, toSnakeCase } from '@workos/oagen';
3
+ import { resolveResourceClassName } from './resources.js';
4
+ import { className, serviceTypeName, humanize } from './naming.js';
5
+ import { getMountTarget } from '../shared/resolved-ops.js';
6
+
7
+ /**
8
+ * Generate the C# client file with service accessors.
9
+ * Produces: WorkOSClient.Generated.cs (partial class with service properties).
10
+ */
11
+ export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
12
+ return [generateClientFile(spec, ctx)];
13
+ }
14
+
15
+ /**
16
+ * Deduplicate services by mount target.
17
+ */
18
+ function deduplicateByMount(services: Service[], ctx: EmitterContext): Service[] {
19
+ const byTarget = new Map<string, Service>();
20
+ for (const s of services) {
21
+ const target = getMountTarget(s, ctx);
22
+ const existing = byTarget.get(target);
23
+ if (!existing || toPascalCase(s.name) === target) {
24
+ byTarget.set(target, s);
25
+ }
26
+ }
27
+ return [...byTarget.values()];
28
+ }
29
+
30
+ /**
31
+ * Build map of service name -> accessor property name.
32
+ */
33
+ export function buildServiceAccessPaths(services: Service[], ctx: EmitterContext): Map<string, string> {
34
+ const topLevel = deduplicateByMount(services, ctx);
35
+ const paths = new Map<string, string>();
36
+
37
+ for (const service of topLevel) {
38
+ const resolvedName = resolveResourceClassName(service, ctx);
39
+ const prop = toSnakeCase(resolvedName);
40
+ paths.set(service.name, prop);
41
+ }
42
+
43
+ // Also map mount targets
44
+ for (const service of services) {
45
+ const target = getMountTarget(service, ctx);
46
+ if (!paths.has(target)) {
47
+ const existing = paths.get(service.name);
48
+ if (existing) paths.set(target, existing);
49
+ }
50
+ }
51
+
52
+ return paths;
53
+ }
54
+
55
+ function generateClientFile(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
56
+ const topLevel = deduplicateByMount(spec.services, ctx);
57
+ const lines: string[] = [];
58
+
59
+ lines.push(`namespace ${ctx.namespacePascal}`);
60
+ lines.push('{');
61
+ lines.push(' /// <summary>');
62
+ lines.push(' /// Generated service accessors for WorkOSClient.');
63
+ lines.push(' /// </summary>');
64
+ lines.push(' public partial class WorkOSClient');
65
+ lines.push(' {');
66
+
67
+ // Service properties with lazy initialization
68
+ for (const service of topLevel) {
69
+ const resolvedName = resolveResourceClassName(service, ctx);
70
+ const propName = className(resolvedName);
71
+ const svcType = serviceTypeName(resolvedName);
72
+ const backingField = propName.charAt(0).toLowerCase() + propName.slice(1);
73
+ const human = humanize(resolvedName);
74
+ lines.push(` private ${svcType} ${backingField};`);
75
+ lines.push('');
76
+ lines.push(` /// <summary>Gets the <see cref="${svcType}"/> for ${human} API operations.</summary>`);
77
+ lines.push(` public virtual ${svcType} ${propName} => this.${backingField} ??= new ${svcType}(this);`);
78
+ lines.push('');
79
+ }
80
+
81
+ lines.push(' }');
82
+ lines.push('}');
83
+
84
+ return {
85
+ path: 'Client/WorkOSClient.Generated.cs',
86
+ content: lines.join('\n'),
87
+ overwriteExisting: true,
88
+ };
89
+ }