@workos/oagen-emitters 0.2.0 → 0.3.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/.husky/pre-commit +1 -0
- package/.oxfmtrc.json +8 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +129 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +11943 -2728
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/go.md +338 -0
- package/docs/sdk-architecture/php.md +315 -0
- package/docs/sdk-architecture/python.md +511 -0
- package/oagen.config.ts +298 -2
- package/package.json +9 -5
- package/scripts/generate-php.js +13 -0
- package/scripts/git-push-with-published-oagen.sh +21 -0
- package/smoke/sdk-dotnet.ts +17 -3
- package/smoke/sdk-elixir.ts +17 -3
- package/smoke/sdk-go.ts +137 -46
- package/smoke/sdk-kotlin.ts +23 -4
- package/smoke/sdk-node.ts +15 -3
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- package/smoke/sdk-ruby.ts +17 -3
- package/smoke/sdk-rust.ts +16 -3
- package/src/go/client.ts +141 -0
- package/src/go/enums.ts +196 -0
- package/src/go/fixtures.ts +212 -0
- package/src/go/index.ts +81 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +191 -0
- package/src/go/resources.ts +827 -0
- package/src/go/tests.ts +751 -0
- package/src/go/type-map.ts +82 -0
- package/src/go/wrappers.ts +261 -0
- package/src/index.ts +3 -0
- package/src/node/client.ts +167 -122
- package/src/node/enums.ts +13 -4
- package/src/node/errors.ts +42 -233
- package/src/node/field-plan.ts +726 -0
- package/src/node/fixtures.ts +15 -5
- package/src/node/index.ts +65 -16
- package/src/node/models.ts +264 -96
- package/src/node/naming.ts +52 -25
- package/src/node/resources.ts +621 -172
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +71 -27
- package/src/node/type-map.ts +4 -2
- package/src/node/utils.ts +56 -64
- package/src/node/wrappers.ts +151 -0
- package/src/php/client.ts +171 -0
- package/src/php/enums.ts +67 -0
- package/src/php/errors.ts +9 -0
- package/src/php/fixtures.ts +181 -0
- package/src/php/index.ts +96 -0
- package/src/php/manifest.ts +36 -0
- package/src/php/models.ts +310 -0
- package/src/php/naming.ts +298 -0
- package/src/php/resources.ts +561 -0
- package/src/php/tests.ts +533 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +151 -0
- package/src/python/client.ts +337 -0
- package/src/python/enums.ts +313 -0
- package/src/python/fixtures.ts +196 -0
- package/src/python/index.ts +95 -0
- package/src/python/manifest.ts +38 -0
- package/src/python/models.ts +688 -0
- package/src/python/naming.ts +209 -0
- package/src/python/resources.ts +1322 -0
- package/src/python/tests.ts +1335 -0
- package/src/python/type-map.ts +93 -0
- package/src/python/wrappers.ts +191 -0
- package/src/shared/model-utils.ts +255 -0
- package/src/shared/naming-utils.ts +107 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +59 -0
- package/test/go/client.test.ts +92 -0
- package/test/go/enums.test.ts +132 -0
- package/test/go/errors.test.ts +9 -0
- package/test/go/models.test.ts +265 -0
- package/test/go/resources.test.ts +408 -0
- package/test/go/tests.test.ts +143 -0
- package/test/node/client.test.ts +199 -94
- package/test/node/enums.test.ts +75 -3
- package/test/node/errors.test.ts +2 -41
- package/test/node/models.test.ts +109 -20
- package/test/node/naming.test.ts +37 -4
- package/test/node/resources.test.ts +662 -30
- package/test/node/serializers.test.ts +36 -7
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +94 -0
- package/test/php/enums.test.ts +173 -0
- package/test/php/errors.test.ts +9 -0
- package/test/php/models.test.ts +497 -0
- package/test/php/resources.test.ts +644 -0
- package/test/php/tests.test.ts +118 -0
- package/test/python/client.test.ts +200 -0
- package/test/python/enums.test.ts +228 -0
- package/test/python/errors.test.ts +16 -0
- package/test/python/manifest.test.ts +74 -0
- package/test/python/models.test.ts +716 -0
- package/test/python/resources.test.ts +617 -0
- package/test/python/tests.test.ts +202 -0
- package/src/node/common.ts +0 -273
- package/src/node/config.ts +0 -71
- package/src/node/serializers.ts +0 -744
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
|
// ---------------------------------------------------------------------------
|
|
@@ -257,7 +294,12 @@ class CaptureProxy {
|
|
|
257
294
|
}
|
|
258
295
|
}
|
|
259
296
|
|
|
260
|
-
const capturedReq: CapturedRequest = {
|
|
297
|
+
const capturedReq: CapturedRequest = {
|
|
298
|
+
method,
|
|
299
|
+
path,
|
|
300
|
+
queryParams,
|
|
301
|
+
body: parsedReqBody,
|
|
302
|
+
};
|
|
261
303
|
|
|
262
304
|
// Forward to the real API
|
|
263
305
|
const targetUrl = new URL(path, this.targetBaseUrl);
|
|
@@ -340,15 +382,16 @@ function detectModulePath(sdkPath: string): string {
|
|
|
340
382
|
const match = goMod.match(/^module\s+(\S+)/m);
|
|
341
383
|
if (match) return match[1];
|
|
342
384
|
}
|
|
343
|
-
return 'github.com/workos/workos-go/
|
|
385
|
+
return 'github.com/workos/workos-go/v2';
|
|
344
386
|
}
|
|
345
387
|
|
|
346
388
|
function generateGoImports(
|
|
347
389
|
modulePath: string,
|
|
348
|
-
|
|
390
|
+
_servicePackages: Set<string>,
|
|
349
391
|
needsJson: boolean,
|
|
350
|
-
|
|
392
|
+
_needsServicePkg: boolean,
|
|
351
393
|
): string {
|
|
394
|
+
// New emitter: flat package -- everything lives in the root module, no sub-packages.
|
|
352
395
|
const lines: string[] = [];
|
|
353
396
|
lines.push('import (');
|
|
354
397
|
lines.push('\t"context"');
|
|
@@ -358,20 +401,21 @@ function generateGoImports(
|
|
|
358
401
|
lines.push('\t"fmt"');
|
|
359
402
|
lines.push('\t"os"');
|
|
360
403
|
lines.push('');
|
|
361
|
-
lines.push(`\
|
|
362
|
-
if (needsServicePkg) {
|
|
363
|
-
for (const pkg of [...servicePackages].sort()) {
|
|
364
|
-
lines.push(`\t"${modulePath}/pkg/${pkg}"`);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
404
|
+
lines.push(`\t"${modulePath}"`);
|
|
367
405
|
lines.push(')');
|
|
368
406
|
return lines.join('\n');
|
|
369
407
|
}
|
|
370
408
|
|
|
371
|
-
function generateGoPayloadStruct(payload: Record<string, unknown>,
|
|
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.
|
|
372
413
|
const lines: string[] = [];
|
|
373
|
-
lines.push(
|
|
414
|
+
lines.push(`&workos.${paramsType}{`);
|
|
374
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;
|
|
375
419
|
const goField = goFieldName(key);
|
|
376
420
|
lines.push(`\t\t${goField}: ${goLiteral(value)},`);
|
|
377
421
|
}
|
|
@@ -409,9 +453,9 @@ function generateGoCallBlock(
|
|
|
409
453
|
pathParams: Record<string, string>,
|
|
410
454
|
spec: any,
|
|
411
455
|
callIndex: number,
|
|
456
|
+
accessorMap: Map<string, string>,
|
|
412
457
|
): string {
|
|
413
458
|
const lines: string[] = [];
|
|
414
|
-
const servicePackage = goServicePackageName(resolution.service);
|
|
415
459
|
const method = resolution.method;
|
|
416
460
|
|
|
417
461
|
// Build arguments
|
|
@@ -422,52 +466,63 @@ function generateGoCallBlock(
|
|
|
422
466
|
args.push(`"${pathParams[p.name] || ''}"`);
|
|
423
467
|
}
|
|
424
468
|
|
|
425
|
-
//
|
|
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;
|
|
426
475
|
if (op.requestBody) {
|
|
427
476
|
const payload = generatePayload(op, spec);
|
|
428
477
|
if (payload && Object.keys(payload).length > 0) {
|
|
429
|
-
|
|
430
|
-
args.push(generateGoPayloadStruct(payload, optsType, servicePackage));
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Paginated operations: pass opts with Limit=1
|
|
435
|
-
if (op.pagination && !op.requestBody) {
|
|
436
|
-
const extraParams = op.queryParams.filter((p: any) => !['limit', 'before', 'after', 'order'].includes(p.name));
|
|
437
|
-
if (extraParams.length > 0) {
|
|
438
|
-
// Match the emitter convention: List → ListFilterOpts, others → ${method}Opts
|
|
439
|
-
const optsType = method === 'List' ? 'ListFilterOpts' : `${method}Opts`;
|
|
440
|
-
args.push(`${servicePackage}.${optsType}{Limit: 1}`);
|
|
478
|
+
args.push(generateGoPayloadStruct(payload, paramsTypeName));
|
|
441
479
|
} else {
|
|
442
|
-
|
|
480
|
+
// Even with empty payload, the method signature requires the params arg
|
|
481
|
+
args.push(`&workos.${paramsTypeName}{}`);
|
|
443
482
|
}
|
|
483
|
+
} else if (op.pagination || hasQueryParams) {
|
|
484
|
+
// Paginated or query-param operations need a params struct
|
|
485
|
+
args.push(`&workos.${paramsTypeName}{}`);
|
|
444
486
|
}
|
|
445
487
|
|
|
446
|
-
//
|
|
447
|
-
const serviceProp =
|
|
488
|
+
// Service accessor: resolve from the generated SDK's actual accessor names
|
|
489
|
+
const serviceProp = resolveAccessorName(resolution.service, accessorMap);
|
|
448
490
|
|
|
449
491
|
lines.push(`\t// Call ${callIndex}: ${op.httpMethod.toUpperCase()} ${op.path}`);
|
|
450
492
|
lines.push(`\tfmt.Fprintf(os.Stderr, "CALL_START:${callIndex}\\n")`);
|
|
451
493
|
|
|
452
|
-
// Determine return type: paginated and
|
|
453
|
-
// DELETE returns just error
|
|
494
|
+
// Determine return type: paginated returns Iterator, DELETE and void/redirect return just error
|
|
454
495
|
const isDelete = op.httpMethod === 'delete';
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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(', ')})`);
|
|
459
514
|
lines.push(`\tif err${callIndex} != nil {`);
|
|
460
515
|
lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_ERROR:${callIndex}:%s\\n", err${callIndex}.Error())`);
|
|
461
516
|
lines.push('\t} else {');
|
|
462
|
-
lines.push(`\t\
|
|
463
|
-
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")`);
|
|
464
518
|
lines.push('\t}');
|
|
465
519
|
} else {
|
|
466
|
-
lines.push(`\
|
|
520
|
+
lines.push(`\tresult${callIndex}, err${callIndex} := client.${serviceProp}().${method}(${args.join(', ')})`);
|
|
467
521
|
lines.push(`\tif err${callIndex} != nil {`);
|
|
468
522
|
lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_ERROR:${callIndex}:%s\\n", err${callIndex}.Error())`);
|
|
469
523
|
lines.push('\t} else {');
|
|
470
|
-
lines.push(`\t\
|
|
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}))`);
|
|
471
526
|
lines.push('\t}');
|
|
472
527
|
}
|
|
473
528
|
|
|
@@ -518,6 +573,11 @@ async function main(): Promise<void> {
|
|
|
518
573
|
// Load manifest
|
|
519
574
|
const manifest = loadManifest(sdkPath);
|
|
520
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
|
+
|
|
521
581
|
const baseUrl = process.env.WORKOS_BASE_URL || spec.baseUrl;
|
|
522
582
|
|
|
523
583
|
// Start capture proxy
|
|
@@ -580,7 +640,11 @@ async function main(): Promise<void> {
|
|
|
580
640
|
|
|
581
641
|
// Build planned calls for this wave, resolving methods
|
|
582
642
|
const plannedCalls: PlannedCall[] = [];
|
|
583
|
-
const waveSkipped: Array<{
|
|
643
|
+
const waveSkipped: Array<{
|
|
644
|
+
op: Operation;
|
|
645
|
+
irService: string;
|
|
646
|
+
reason: string;
|
|
647
|
+
}> = [];
|
|
584
648
|
|
|
585
649
|
for (const { op, irService, pathParams } of wave.calls) {
|
|
586
650
|
const resolution = resolveMethod(op, irService, manifest);
|
|
@@ -624,7 +688,7 @@ async function main(): Promise<void> {
|
|
|
624
688
|
// Generate all call blocks for this wave
|
|
625
689
|
const callBlocks: string[] = [];
|
|
626
690
|
for (const call of plannedCalls) {
|
|
627
|
-
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));
|
|
628
692
|
}
|
|
629
693
|
|
|
630
694
|
const imports = generateGoImports(modulePath, servicePackages, needsJson, needsServicePkg);
|
|
@@ -635,7 +699,7 @@ async function main(): Promise<void> {
|
|
|
635
699
|
imports,
|
|
636
700
|
'',
|
|
637
701
|
'func main() {',
|
|
638
|
-
`\tclient := workos.NewClient("${apiKey}", workos.
|
|
702
|
+
`\tclient := workos.NewClient("${apiKey}", workos.WithBaseURL("http://127.0.0.1:${proxyPort}"))`,
|
|
639
703
|
'\tctx := context.Background()',
|
|
640
704
|
'',
|
|
641
705
|
...callBlocks,
|
|
@@ -651,19 +715,41 @@ async function main(): Promise<void> {
|
|
|
651
715
|
|
|
652
716
|
// Step 1: Build (sync — no proxy needed during compilation)
|
|
653
717
|
let buildError: string | null = null;
|
|
718
|
+
|
|
719
|
+
// Run go mod tidy first to resolve dependencies
|
|
654
720
|
try {
|
|
655
|
-
execSync('go
|
|
721
|
+
execSync('go mod tidy', {
|
|
656
722
|
cwd: tmpDir,
|
|
657
723
|
timeout: 120_000,
|
|
658
|
-
env: {
|
|
724
|
+
env: {
|
|
725
|
+
...process.env,
|
|
726
|
+
GOPATH: process.env.GOPATH || resolve(process.env.HOME || '~', 'go'),
|
|
727
|
+
},
|
|
659
728
|
encoding: 'utf-8',
|
|
660
729
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
661
730
|
});
|
|
662
731
|
} catch (err: any) {
|
|
663
732
|
const stderr = typeof err.stderr === 'string' ? err.stderr : '';
|
|
664
|
-
buildError = stderr.trim().split('\n').slice(0,
|
|
733
|
+
buildError = `go mod tidy failed: ${stderr.trim().split('\n').slice(0, 3).join(' ')}`;
|
|
665
734
|
}
|
|
666
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
|
+
|
|
667
753
|
if (buildError) {
|
|
668
754
|
// Build failure affects entire wave
|
|
669
755
|
const elapsed = Date.now() - waveStart;
|
|
@@ -903,7 +989,12 @@ function makeSkippedExchange(op: Operation, service: string, reason: string): Ca
|
|
|
903
989
|
operationId: op.name,
|
|
904
990
|
service,
|
|
905
991
|
operationName: op.name,
|
|
906
|
-
request: {
|
|
992
|
+
request: {
|
|
993
|
+
method: op.httpMethod.toUpperCase(),
|
|
994
|
+
path: op.path,
|
|
995
|
+
queryParams: {},
|
|
996
|
+
body: null,
|
|
997
|
+
},
|
|
907
998
|
response: { status: 0, body: null },
|
|
908
999
|
outcome: 'skipped',
|
|
909
1000
|
error: reason,
|
package/smoke/sdk-kotlin.ts
CHANGED
|
@@ -123,7 +123,12 @@ function createProxyServer(
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
captures.push({
|
|
126
|
-
request: {
|
|
126
|
+
request: {
|
|
127
|
+
method: req.method!,
|
|
128
|
+
path: url.pathname,
|
|
129
|
+
queryParams,
|
|
130
|
+
body,
|
|
131
|
+
},
|
|
127
132
|
response: { status: proxyRes.statusCode!, body: resBody },
|
|
128
133
|
});
|
|
129
134
|
|
|
@@ -135,7 +140,12 @@ function createProxyServer(
|
|
|
135
140
|
proxyReq.on('error', (err) => {
|
|
136
141
|
console.error('Proxy request error:', err.message);
|
|
137
142
|
captures.push({
|
|
138
|
-
request: {
|
|
143
|
+
request: {
|
|
144
|
+
method: req.method!,
|
|
145
|
+
path: url.pathname,
|
|
146
|
+
queryParams,
|
|
147
|
+
body,
|
|
148
|
+
},
|
|
139
149
|
response: { status: 502, body: { error: err.message } },
|
|
140
150
|
});
|
|
141
151
|
res.writeHead(502);
|
|
@@ -499,7 +509,11 @@ async function main(): Promise<void> {
|
|
|
499
509
|
|
|
500
510
|
// Build planned calls for this wave, resolving methods
|
|
501
511
|
const plannedCalls: PlannedCall[] = [];
|
|
502
|
-
const waveSkipped: Array<{
|
|
512
|
+
const waveSkipped: Array<{
|
|
513
|
+
op: Operation;
|
|
514
|
+
irService: string;
|
|
515
|
+
reason: string;
|
|
516
|
+
}> = [];
|
|
503
517
|
|
|
504
518
|
for (const { op, irService, pathParams } of wave.calls) {
|
|
505
519
|
const resolution = resolveMethod(op, irService, manifest);
|
|
@@ -754,7 +768,12 @@ function makeSkippedExchange(op: Operation, service: string, reason: string): Ca
|
|
|
754
768
|
operationId: op.name,
|
|
755
769
|
service,
|
|
756
770
|
operationName: op.name,
|
|
757
|
-
request: {
|
|
771
|
+
request: {
|
|
772
|
+
method: op.httpMethod.toUpperCase(),
|
|
773
|
+
path: op.path,
|
|
774
|
+
queryParams: {},
|
|
775
|
+
body: null,
|
|
776
|
+
},
|
|
758
777
|
response: { status: 0, body: null },
|
|
759
778
|
outcome: 'skipped',
|
|
760
779
|
error: reason,
|
package/smoke/sdk-node.ts
CHANGED
|
@@ -52,7 +52,10 @@ interface CapturedResponse {
|
|
|
52
52
|
// HTTP Interception
|
|
53
53
|
// ---------------------------------------------------------------------------
|
|
54
54
|
|
|
55
|
-
let currentCapture: {
|
|
55
|
+
let currentCapture: {
|
|
56
|
+
request: CapturedRequest;
|
|
57
|
+
response: CapturedResponse;
|
|
58
|
+
} | null = null;
|
|
56
59
|
const originalFetch = globalThis.fetch;
|
|
57
60
|
|
|
58
61
|
function interceptFetch(): void {
|
|
@@ -259,7 +262,11 @@ async function main(): Promise<void> {
|
|
|
259
262
|
const groups = planOperations(spec);
|
|
260
263
|
const ids = new IdRegistry();
|
|
261
264
|
const exchanges: CapturedExchange[] = [];
|
|
262
|
-
const createdEntities: Array<{
|
|
265
|
+
const createdEntities: Array<{
|
|
266
|
+
service: string;
|
|
267
|
+
id: string;
|
|
268
|
+
deleteFn?: () => Promise<void>;
|
|
269
|
+
}> = [];
|
|
263
270
|
const delayMs = Number(process.env.SMOKE_DELAY_MS) || 200;
|
|
264
271
|
|
|
265
272
|
let successCount = 0;
|
|
@@ -452,7 +459,12 @@ function makeSkippedExchange(op: Operation, service: string, reason: string): Ca
|
|
|
452
459
|
operationId: op.name,
|
|
453
460
|
service,
|
|
454
461
|
operationName: op.name,
|
|
455
|
-
request: {
|
|
462
|
+
request: {
|
|
463
|
+
method: op.httpMethod.toUpperCase(),
|
|
464
|
+
path: op.path,
|
|
465
|
+
queryParams: {},
|
|
466
|
+
body: null,
|
|
467
|
+
},
|
|
456
468
|
response: { status: 0, body: null },
|
|
457
469
|
outcome: 'skipped',
|
|
458
470
|
error: reason,
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
283
|
-
lines.push(
|
|
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 &&
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
312
|
-
phpArgs.push(phpArrayLiteral(
|
|
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
|
|
317
|
-
phpArgs.
|
|
318
|
-
|
|
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 = ${
|
|
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.
|
|
674
|
+
sdkMethodName: `${resolution.service}->${resolution.method}`,
|
|
673
675
|
captureIndex: 0,
|
|
674
676
|
totalCaptures: 1,
|
|
675
677
|
},
|
package/smoke/sdk-python.ts
CHANGED
|
@@ -284,7 +284,10 @@ function buildBatchedPythonScript(
|
|
|
284
284
|
calls: PlannedCall[],
|
|
285
285
|
spec: any,
|
|
286
286
|
): string {
|
|
287
|
-
|
|
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'],
|
package/smoke/sdk-ruby.ts
CHANGED
|
@@ -160,7 +160,12 @@ function startProxy(
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
const capturedReq: CapturedRequest = {
|
|
163
|
+
const capturedReq: CapturedRequest = {
|
|
164
|
+
method,
|
|
165
|
+
path,
|
|
166
|
+
queryParams,
|
|
167
|
+
body: reqBody,
|
|
168
|
+
};
|
|
164
169
|
|
|
165
170
|
// Forward to real API
|
|
166
171
|
const forwardHeaders: Record<string, string> = {
|
|
@@ -420,7 +425,11 @@ async function main(): Promise<void> {
|
|
|
420
425
|
|
|
421
426
|
// Build planned calls for this wave, resolving methods
|
|
422
427
|
const plannedCalls: PlannedCall[] = [];
|
|
423
|
-
const waveSkipped: Array<{
|
|
428
|
+
const waveSkipped: Array<{
|
|
429
|
+
op: Operation;
|
|
430
|
+
irService: string;
|
|
431
|
+
reason: string;
|
|
432
|
+
}> = [];
|
|
424
433
|
|
|
425
434
|
for (const { op, irService, pathParams } of wave.calls) {
|
|
426
435
|
const resolution = resolveMethod(op, irService, manifest);
|
|
@@ -678,7 +687,12 @@ function makeSkippedExchange(op: Operation, service: string, reason: string): Ca
|
|
|
678
687
|
operationId: op.name,
|
|
679
688
|
service,
|
|
680
689
|
operationName: op.name,
|
|
681
|
-
request: {
|
|
690
|
+
request: {
|
|
691
|
+
method: op.httpMethod.toUpperCase(),
|
|
692
|
+
path: op.path,
|
|
693
|
+
queryParams: {},
|
|
694
|
+
body: null,
|
|
695
|
+
},
|
|
682
696
|
response: { status: 0, body: null },
|
|
683
697
|
outcome: 'skipped',
|
|
684
698
|
error: reason,
|
package/smoke/sdk-rust.ts
CHANGED
|
@@ -452,7 +452,11 @@ serde_json = "1"
|
|
|
452
452
|
|
|
453
453
|
// Build planned calls for this wave, resolving methods
|
|
454
454
|
const plannedCalls: PlannedCall[] = [];
|
|
455
|
-
const waveSkipped: Array<{
|
|
455
|
+
const waveSkipped: Array<{
|
|
456
|
+
op: Operation;
|
|
457
|
+
irService: string;
|
|
458
|
+
reason: string;
|
|
459
|
+
}> = [];
|
|
456
460
|
|
|
457
461
|
for (const { op, irService, pathParams } of wave.calls) {
|
|
458
462
|
const resolution = resolveMethod(op, irService, manifest);
|
|
@@ -534,7 +538,11 @@ serde_json = "1"
|
|
|
534
538
|
await new Promise<void>((resolvePromise, rejectPromise) => {
|
|
535
539
|
const child = spawn(join(tmpDir, 'target', 'debug', 'smoke-driver'), [], {
|
|
536
540
|
cwd: tmpDir,
|
|
537
|
-
env: {
|
|
541
|
+
env: {
|
|
542
|
+
...process.env,
|
|
543
|
+
WORKOS_API_KEY: apiKey,
|
|
544
|
+
WORKOS_BASE_URL: `http://localhost:${proxy.port}`,
|
|
545
|
+
},
|
|
538
546
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
539
547
|
});
|
|
540
548
|
|
|
@@ -729,7 +737,12 @@ function makeSkippedExchange(op: Operation, service: string, reason: string): Ca
|
|
|
729
737
|
operationId: op.name,
|
|
730
738
|
service,
|
|
731
739
|
operationName: op.name,
|
|
732
|
-
request: {
|
|
740
|
+
request: {
|
|
741
|
+
method: op.httpMethod.toUpperCase(),
|
|
742
|
+
path: op.path,
|
|
743
|
+
queryParams: {},
|
|
744
|
+
body: null,
|
|
745
|
+
},
|
|
733
746
|
response: { status: 0, body: null },
|
|
734
747
|
outcome: 'skipped',
|
|
735
748
|
error: reason,
|