@workos/oagen-emitters 0.4.0 → 0.5.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/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lint.yml +1 -1
- package/.github/workflows/release-please.yml +2 -2
- package/.github/workflows/release.yml +1 -1
- package/.husky/pre-push +11 -0
- package/.node-version +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +35 -224
- package/dist/index.d.mts +9 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -15234
- package/dist/plugin-BSop9f9z.mjs +21471 -0
- package/dist/plugin-BSop9f9z.mjs.map +1 -0
- package/dist/plugin.d.mts +7 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +2 -0
- package/docs/sdk-architecture/dotnet.md +5 -5
- package/oagen.config.ts +5 -373
- package/package.json +10 -34
- package/src/dotnet/index.ts +6 -4
- package/src/dotnet/models.ts +58 -82
- package/src/dotnet/naming.ts +44 -6
- package/src/dotnet/resources.ts +350 -29
- package/src/dotnet/tests.ts +44 -24
- package/src/dotnet/type-map.ts +44 -17
- package/src/dotnet/wrappers.ts +21 -10
- package/src/go/client.ts +35 -3
- package/src/go/enums.ts +4 -0
- package/src/go/index.ts +10 -5
- package/src/go/models.ts +6 -1
- package/src/go/resources.ts +534 -73
- package/src/go/tests.ts +39 -3
- package/src/go/type-map.ts +8 -3
- package/src/go/wrappers.ts +79 -21
- package/src/index.ts +14 -0
- package/src/kotlin/client.ts +7 -2
- package/src/kotlin/enums.ts +30 -3
- package/src/kotlin/models.ts +97 -6
- package/src/kotlin/naming.ts +7 -1
- package/src/kotlin/resources.ts +370 -39
- package/src/kotlin/tests.ts +120 -6
- package/src/node/client.ts +38 -11
- package/src/node/field-plan.ts +12 -14
- package/src/node/fixtures.ts +39 -3
- package/src/node/models.ts +281 -37
- package/src/node/resources.ts +156 -52
- package/src/node/tests.ts +76 -27
- package/src/node/type-map.ts +1 -31
- package/src/node/utils.ts +96 -6
- package/src/node/wrappers.ts +31 -1
- package/src/php/models.ts +0 -33
- package/src/php/resources.ts +199 -18
- package/src/php/tests.ts +26 -2
- package/src/php/type-map.ts +16 -2
- package/src/php/wrappers.ts +6 -2
- package/src/plugin.ts +50 -0
- package/src/python/client.ts +13 -3
- package/src/python/enums.ts +28 -3
- package/src/python/index.ts +35 -27
- package/src/python/models.ts +138 -1
- package/src/python/resources.ts +234 -17
- package/src/python/tests.ts +260 -16
- package/src/python/type-map.ts +16 -2
- package/src/ruby/client.ts +238 -0
- package/src/ruby/enums.ts +149 -0
- package/src/ruby/index.ts +93 -0
- package/src/ruby/manifest.ts +35 -0
- package/src/ruby/models.ts +360 -0
- package/src/ruby/naming.ts +187 -0
- package/src/ruby/rbi.ts +313 -0
- package/src/ruby/resources.ts +799 -0
- package/src/ruby/tests.ts +459 -0
- package/src/ruby/type-map.ts +97 -0
- package/src/ruby/wrappers.ts +161 -0
- package/src/shared/model-utils.ts +131 -7
- package/src/shared/naming-utils.ts +36 -0
- package/src/shared/non-spec-services.ts +13 -0
- package/src/shared/resolved-ops.ts +75 -1
- package/test/dotnet/client.test.ts +2 -2
- package/test/dotnet/models.test.ts +7 -9
- package/test/dotnet/resources.test.ts +135 -3
- package/test/dotnet/tests.test.ts +5 -5
- package/test/entrypoint.test.ts +89 -0
- package/test/go/client.test.ts +6 -6
- package/test/go/resources.test.ts +156 -7
- package/test/kotlin/models.test.ts +1 -1
- package/test/kotlin/resources.test.ts +210 -0
- package/test/node/models.test.ts +134 -1
- package/test/node/resources.test.ts +134 -26
- package/test/node/utils.test.ts +140 -0
- package/test/php/models.test.ts +5 -4
- package/test/php/resources.test.ts +66 -1
- package/test/plugin.test.ts +50 -0
- package/test/python/client.test.ts +56 -0
- package/test/python/models.test.ts +99 -0
- package/test/python/resources.test.ts +294 -0
- package/test/python/tests.test.ts +91 -0
- package/test/ruby/client.test.ts +81 -0
- package/test/ruby/resources.test.ts +386 -0
- package/test/shared/resolved-ops.test.ts +122 -0
- package/tsdown.config.ts +1 -1
- package/dist/index.mjs.map +0 -1
- package/scripts/generate-php.js +0 -13
- package/scripts/git-push-with-published-oagen.sh +0 -21
package/src/dotnet/tests.ts
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import type { ApiSpec, Service, Operation, EmitterContext, GeneratedFile } from '@workos/oagen';
|
|
2
2
|
import { planOperation } from '@workos/oagen';
|
|
3
3
|
import {
|
|
4
|
-
className,
|
|
5
4
|
fixtureFileName,
|
|
6
5
|
fieldName as csFieldName,
|
|
7
6
|
methodName as csMethodName,
|
|
7
|
+
appendAsyncSuffix,
|
|
8
|
+
modelClassName,
|
|
8
9
|
resolveMethodName,
|
|
10
|
+
resolveMethodStem,
|
|
9
11
|
serviceTypeName,
|
|
10
12
|
} from './naming.js';
|
|
13
|
+
import { resolveModelName } from './type-map.js';
|
|
11
14
|
import { resolveResourceClassName, sortPathParamsByTemplateOrder, optionsClassName } from './resources.js';
|
|
12
15
|
import { generateFixtures, generateModelFixture } from './fixtures.js';
|
|
13
16
|
import { isListWrapperModel } from './models.js';
|
|
14
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
groupByMount,
|
|
19
|
+
buildResolvedLookup,
|
|
20
|
+
lookupResolved,
|
|
21
|
+
buildHiddenParams,
|
|
22
|
+
collectGroupedParamNames,
|
|
23
|
+
} from '../shared/resolved-ops.js';
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
26
|
* Generate C# test files and JSON fixtures.
|
|
@@ -194,7 +203,7 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
194
203
|
lines.push(` this.httpMock.AssertRequestWasMade(HttpMethod.Delete, "${expectedPath}");`);
|
|
195
204
|
lines.push(' }');
|
|
196
205
|
} else if (plan.responseModelName) {
|
|
197
|
-
const respModel = plan.responseModelName;
|
|
206
|
+
const respModel = resolveModelName(plan.responseModelName);
|
|
198
207
|
const fixturePath = `testdata/${fixtureFileName(respModel)}.json`;
|
|
199
208
|
const httpMethodCs = op.httpMethod.charAt(0).toUpperCase() + op.httpMethod.slice(1).toLowerCase();
|
|
200
209
|
|
|
@@ -261,8 +270,9 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
261
270
|
const plan = planOperation(op);
|
|
262
271
|
if (!plan.isPaginated || !op.pagination) continue;
|
|
263
272
|
|
|
264
|
-
const
|
|
265
|
-
const
|
|
273
|
+
const methodStem = resolveCsMethodStem(op, resolvedName, ctx);
|
|
274
|
+
const autoPagingMethod = `${methodStem}AutoPagingAsync`;
|
|
275
|
+
const autoPagingTestName = `Test${autoPagingMethod}`;
|
|
266
276
|
if (emittedTestMethods.has(autoPagingTestName)) continue;
|
|
267
277
|
emittedTestMethods.add(autoPagingTestName);
|
|
268
278
|
|
|
@@ -282,8 +292,8 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
282
292
|
if (inner) resolved = inner;
|
|
283
293
|
}
|
|
284
294
|
}
|
|
285
|
-
itemTypeName =
|
|
286
|
-
fixtureName = fixtureFileName(resolved.name);
|
|
295
|
+
itemTypeName = modelClassName(resolveModelName(resolved.name));
|
|
296
|
+
fixtureName = fixtureFileName(resolveModelName(resolved.name));
|
|
287
297
|
}
|
|
288
298
|
}
|
|
289
299
|
|
|
@@ -310,7 +320,7 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
310
320
|
);
|
|
311
321
|
lines.push('');
|
|
312
322
|
lines.push(` var items = new List<${itemTypeName}>();`);
|
|
313
|
-
lines.push(` await foreach (var item in this.service.${
|
|
323
|
+
lines.push(` await foreach (var item in this.service.${autoPagingMethod}(${autoPagingArgs}))`);
|
|
314
324
|
lines.push(' {');
|
|
315
325
|
lines.push(' items.Add(item);');
|
|
316
326
|
lines.push(' }');
|
|
@@ -319,7 +329,7 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
319
329
|
lines.push(' }');
|
|
320
330
|
|
|
321
331
|
// Test with empty first page
|
|
322
|
-
const emptyTestName = `Test${
|
|
332
|
+
const emptyTestName = `Test${autoPagingMethod}Empty`;
|
|
323
333
|
if (!emittedTestMethods.has(emptyTestName)) {
|
|
324
334
|
emittedTestMethods.add(emptyTestName);
|
|
325
335
|
lines.push('');
|
|
@@ -332,7 +342,7 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
332
342
|
);
|
|
333
343
|
lines.push('');
|
|
334
344
|
lines.push(` var items = new List<${itemTypeName}>();`);
|
|
335
|
-
lines.push(` await foreach (var item in this.service.${
|
|
345
|
+
lines.push(` await foreach (var item in this.service.${autoPagingMethod}(${autoPagingArgs}))`);
|
|
336
346
|
lines.push(' {');
|
|
337
347
|
lines.push(' items.Add(item);');
|
|
338
348
|
lines.push(' }');
|
|
@@ -348,7 +358,8 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
348
358
|
if (!resolvedOp?.wrappers || resolvedOp.wrappers.length === 0) continue;
|
|
349
359
|
|
|
350
360
|
for (const wrapper of resolvedOp.wrappers) {
|
|
351
|
-
const
|
|
361
|
+
const wrapperMethodStem = csMethodName(wrapper.name);
|
|
362
|
+
const wrapperMethod = appendAsyncSuffix(wrapperMethodStem);
|
|
352
363
|
const wrapperTestName = `Test${wrapperMethod}`;
|
|
353
364
|
if (emittedTestMethods.has(wrapperTestName)) continue;
|
|
354
365
|
emittedTestMethods.add(wrapperTestName);
|
|
@@ -363,7 +374,7 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
363
374
|
lines.push(' {');
|
|
364
375
|
|
|
365
376
|
if (responseType) {
|
|
366
|
-
const fixturePath = `testdata/${fixtureFileName(responseType)}.json`;
|
|
377
|
+
const fixturePath = `testdata/${fixtureFileName(resolveModelName(responseType))}.json`;
|
|
367
378
|
lines.push(` var fixture = System.IO.File.ReadAllText("${fixturePath}");`);
|
|
368
379
|
lines.push(
|
|
369
380
|
` this.httpMock.MockResponse(HttpMethod.${httpMethodCs}, "${expectedPath}", HttpStatusCode.OK, fixture);`,
|
|
@@ -379,7 +390,7 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
379
390
|
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
380
391
|
wrapperArgs.push(`"test_${p.name}"`);
|
|
381
392
|
}
|
|
382
|
-
wrapperArgs.push(`new ${
|
|
393
|
+
wrapperArgs.push(`new ${wrapperMethodStem}Options()`);
|
|
383
394
|
|
|
384
395
|
if (responseType) {
|
|
385
396
|
lines.push(` var result = await this.service.${wrapperMethod}(${wrapperArgs.join(', ')});`);
|
|
@@ -411,11 +422,11 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
411
422
|
);
|
|
412
423
|
if (plan.isPaginated || plan.isDelete || !plan.responseModelName) {
|
|
413
424
|
lines.push(
|
|
414
|
-
` await Assert.ThrowsAsync<
|
|
425
|
+
` await Assert.ThrowsAsync<AuthenticationException>(() => this.service.${method}(${callArgs}));`,
|
|
415
426
|
);
|
|
416
427
|
} else {
|
|
417
428
|
lines.push(
|
|
418
|
-
` await Assert.ThrowsAsync<
|
|
429
|
+
` await Assert.ThrowsAsync<AuthenticationException>(() => this.service.${method}(${callArgs}));`,
|
|
419
430
|
);
|
|
420
431
|
}
|
|
421
432
|
lines.push(' }');
|
|
@@ -428,7 +439,7 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
428
439
|
lines.push(
|
|
429
440
|
` this.httpMock.MockResponseForAnyRequest(HttpStatusCode.NotFound, "{\\"code\\":\\"not_found\\",\\"message\\":\\"Not Found\\"}");`,
|
|
430
441
|
);
|
|
431
|
-
lines.push(` await Assert.ThrowsAsync<
|
|
442
|
+
lines.push(` await Assert.ThrowsAsync<NotFoundException>(() => this.service.${method}(${callArgs}));`);
|
|
432
443
|
lines.push(' }');
|
|
433
444
|
|
|
434
445
|
// 422
|
|
@@ -440,7 +451,7 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
440
451
|
` this.httpMock.MockResponseForAnyRequest((HttpStatusCode)422, "{\\"code\\":\\"unprocessable_entity\\",\\"message\\":\\"Unprocessable\\"}");`,
|
|
441
452
|
);
|
|
442
453
|
lines.push(
|
|
443
|
-
` await Assert.ThrowsAsync<
|
|
454
|
+
` await Assert.ThrowsAsync<UnprocessableEntityException>(() => this.service.${method}(${callArgs}));`,
|
|
444
455
|
);
|
|
445
456
|
lines.push(' }');
|
|
446
457
|
|
|
@@ -453,7 +464,7 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
453
464
|
` this.httpMock.MockResponseForAnyRequest((HttpStatusCode)429, "{\\"code\\":\\"too_many_requests\\",\\"message\\":\\"Too Many Requests\\"}");`,
|
|
454
465
|
);
|
|
455
466
|
lines.push(
|
|
456
|
-
` await Assert.ThrowsAsync<
|
|
467
|
+
` await Assert.ThrowsAsync<RateLimitExceededException>(() => this.service.${method}(${callArgs}));`,
|
|
457
468
|
);
|
|
458
469
|
lines.push(' }');
|
|
459
470
|
|
|
@@ -465,7 +476,7 @@ function generateServiceTest(service: Service, spec: ApiSpec, ctx: EmitterContex
|
|
|
465
476
|
lines.push(
|
|
466
477
|
` this.httpMock.MockResponseForAnyRequest(HttpStatusCode.InternalServerError, "{\\"code\\":\\"server_error\\",\\"message\\":\\"Server Error\\"}");`,
|
|
467
478
|
);
|
|
468
|
-
lines.push(` await Assert.ThrowsAsync<
|
|
479
|
+
lines.push(` await Assert.ThrowsAsync<ServerException>(() => this.service.${method}(${callArgs}));`);
|
|
469
480
|
lines.push(' }');
|
|
470
481
|
}
|
|
471
482
|
|
|
@@ -483,6 +494,10 @@ function resolveCsMethodName(op: Operation, mountName: string, ctx: EmitterConte
|
|
|
483
494
|
return resolveMethodName(op, { name: mountName, operations: [op] }, ctx);
|
|
484
495
|
}
|
|
485
496
|
|
|
497
|
+
function resolveCsMethodStem(op: Operation, mountName: string, ctx: EmitterContext): string {
|
|
498
|
+
return resolveMethodStem(op, { name: mountName, operations: [op] }, ctx);
|
|
499
|
+
}
|
|
500
|
+
|
|
486
501
|
function buildMethodCallArgs(op: Operation, plan: any, ctx: EmitterContext, mountName: string): string {
|
|
487
502
|
const args: string[] = [];
|
|
488
503
|
|
|
@@ -502,7 +517,10 @@ function buildMethodCallArgs(op: Operation, plan: any, ctx: EmitterContext, moun
|
|
|
502
517
|
const resolvedLookup = buildResolvedLookup(ctx);
|
|
503
518
|
const resolvedOp = lookupResolved(op, resolvedLookup);
|
|
504
519
|
const hidden = buildHiddenParams(resolvedOp);
|
|
505
|
-
const
|
|
520
|
+
const groupedParams = collectGroupedParamNames(op);
|
|
521
|
+
const hasVisibleQueryParams =
|
|
522
|
+
op.queryParams.filter((qp) => !hidden.has(qp.name) && !groupedParams.has(qp.name)).length > 0 ||
|
|
523
|
+
(op.parameterGroups?.length ?? 0) > 0;
|
|
506
524
|
const hasBody = plan.hasBody && op.requestBody;
|
|
507
525
|
let hasVisibleBodyFields = false;
|
|
508
526
|
if (hasBody && op.requestBody?.kind === 'model') {
|
|
@@ -513,8 +531,8 @@ function buildMethodCallArgs(op: Operation, plan: any, ctx: EmitterContext, moun
|
|
|
513
531
|
}
|
|
514
532
|
|
|
515
533
|
if (hasVisibleBodyFields || hasVisibleQueryParams) {
|
|
516
|
-
const
|
|
517
|
-
const optName = optionsClassName(mountName,
|
|
534
|
+
const methodStem = resolveCsMethodStem(op, mountName, ctx);
|
|
535
|
+
const optName = optionsClassName(mountName, methodStem);
|
|
518
536
|
args.push(`new ${optName}()`);
|
|
519
537
|
}
|
|
520
538
|
|
|
@@ -566,8 +584,10 @@ function buildRequestShapeSeed(op: Operation, plan: any, ctx: EmitterContext, mo
|
|
|
566
584
|
// call sends it via the body — so skip the query assertion to avoid a
|
|
567
585
|
// false-failing `AssertQueryParam`.
|
|
568
586
|
const bodyWireNames = new Set(bodySeeds.map((s) => s.wire));
|
|
587
|
+
const groupedParamNames = collectGroupedParamNames(op);
|
|
569
588
|
for (const param of op.queryParams) {
|
|
570
589
|
if (hidden.has(param.name)) continue;
|
|
590
|
+
if (groupedParamNames.has(param.name)) continue;
|
|
571
591
|
if (!param.required) continue;
|
|
572
592
|
if (!isSeedableStringRef(param.type)) continue;
|
|
573
593
|
// Skip pagination fields — they're set by the caller or the autopaging loop
|
|
@@ -585,8 +605,8 @@ function buildRequestShapeSeed(op: Operation, plan: any, ctx: EmitterContext, mo
|
|
|
585
605
|
return { setupLines: [], seededCallArgs: null, assertLines: [] };
|
|
586
606
|
}
|
|
587
607
|
|
|
588
|
-
const
|
|
589
|
-
const optName = optionsClassName(mountName,
|
|
608
|
+
const methodStem = resolveCsMethodStem(op, mountName, ctx);
|
|
609
|
+
const optName = optionsClassName(mountName, methodStem);
|
|
590
610
|
|
|
591
611
|
// Rebuild call args with a seeded options variable named `options`.
|
|
592
612
|
const args: string[] = [];
|
package/src/dotnet/type-map.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TypeRef, PrimitiveType, UnionType } from '@workos/oagen';
|
|
2
2
|
import { mapTypeRef as irMapTypeRef } from '@workos/oagen';
|
|
3
|
-
import { className } from './naming.js';
|
|
3
|
+
import { className, modelClassName } from './naming.js';
|
|
4
4
|
|
|
5
5
|
/** Known C# value types that need `?` for nullable. */
|
|
6
6
|
const VALUE_TYPES = new Set(['int', 'long', 'double', 'bool', 'float', 'decimal', 'byte', 'short', 'DateTimeOffset']);
|
|
@@ -21,12 +21,35 @@ const enumAliases = new Map<string, string>();
|
|
|
21
21
|
*/
|
|
22
22
|
const singleValueEnumNames = new Set<string>();
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Module-level alias map for structurally-identical models. Populated by
|
|
26
|
+
* `setModelAliases` from model deduplication; consulted by `mapTypeRef` so
|
|
27
|
+
* that every reference to a duplicate model resolves to the canonical name.
|
|
28
|
+
*/
|
|
29
|
+
const modelAliases = new Map<string, string>();
|
|
30
|
+
|
|
24
31
|
/** Replace the current enum-alias map. Safe to call more than once. */
|
|
25
32
|
export function setEnumAliases(aliases: Map<string, string>): void {
|
|
26
33
|
enumAliases.clear();
|
|
27
34
|
for (const [k, v] of aliases) enumAliases.set(k, v);
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
/** Replace the current model-alias map. Safe to call more than once. */
|
|
38
|
+
export function setModelAliases(aliases: Map<string, string>): void {
|
|
39
|
+
modelAliases.clear();
|
|
40
|
+
for (const [k, v] of aliases) modelAliases.set(k, v);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Check if a model name is an alias (i.e., structurally identical to another model). */
|
|
44
|
+
export function isModelAlias(name: string): boolean {
|
|
45
|
+
return modelAliases.has(name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Resolve a model name to its canonical form (identity if not an alias). */
|
|
49
|
+
export function resolveModelName(name: string): string {
|
|
50
|
+
return modelAliases.get(name) ?? name;
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
/** Replace the set of enum names that are single-value discriminator stand-ins. */
|
|
31
54
|
export function setSingleValueEnumNames(names: Iterable<string>): void {
|
|
32
55
|
singleValueEnumNames.clear();
|
|
@@ -45,7 +68,7 @@ export function mapTypeRef(ref: TypeRef): string {
|
|
|
45
68
|
return irMapTypeRef<string>(ref, {
|
|
46
69
|
primitive: mapPrimitive,
|
|
47
70
|
array: (_ref, items) => `List<${items}>`,
|
|
48
|
-
model: (r) =>
|
|
71
|
+
model: (r) => modelClassName(modelAliases.get(r.name) ?? r.name),
|
|
49
72
|
enum: (r) => {
|
|
50
73
|
// Single-value enums (discriminator consts in disguise) map to `string`
|
|
51
74
|
// so the caller can't misuse a public one-member enum type. The
|
|
@@ -123,21 +146,25 @@ export function isEnumRef(ref: TypeRef): boolean {
|
|
|
123
146
|
}
|
|
124
147
|
|
|
125
148
|
/**
|
|
126
|
-
* Emit JSON attributes for a request-side property.
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
149
|
+
* Emit JSON attributes for a request-side property. Property name mapping is
|
|
150
|
+
* handled by a global SnakeCaseLower / SnakeCaseNamingStrategy configuration
|
|
151
|
+
* on both serializers, so per-property name attributes are not emitted.
|
|
152
|
+
*
|
|
153
|
+
* When `isRequiredEnum` is true, configure both serializers to skip the field
|
|
154
|
+
* when its value equals the enum default (0 = Unknown sentinel). This converts
|
|
155
|
+
* "unset required enum" from a silent `"unknown"` wire value into a clean
|
|
156
|
+
* omission so the API returns a clear `missing required field` error instead
|
|
157
|
+
* of a confusing 422.
|
|
131
158
|
*/
|
|
132
|
-
export function emitJsonPropertyAttributes(
|
|
159
|
+
export function emitJsonPropertyAttributes(_wireName: string, options: { isRequiredEnum?: boolean } = {}): string[] {
|
|
133
160
|
if (options.isRequiredEnum) {
|
|
134
161
|
return [
|
|
135
|
-
` [JsonProperty(
|
|
136
|
-
` [STJS.JsonPropertyName("${wireName}")]`,
|
|
162
|
+
` [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]`,
|
|
137
163
|
` [STJS.JsonIgnore(Condition = STJS.JsonIgnoreCondition.WhenWritingDefault)]`,
|
|
138
164
|
];
|
|
139
165
|
}
|
|
140
|
-
|
|
166
|
+
// Convention-based: SnakeCaseLower naming policy handles the name mapping.
|
|
167
|
+
return [];
|
|
141
168
|
}
|
|
142
169
|
|
|
143
170
|
function mapPrimitive(ref: PrimitiveType): string {
|
|
@@ -188,13 +215,13 @@ function joinUnionVariants(_ref: UnionType, variants: string[]): string {
|
|
|
188
215
|
return 'object';
|
|
189
216
|
}
|
|
190
217
|
|
|
191
|
-
if (unique.length >= 2 && unique.length <=
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
if (unique.length >=
|
|
218
|
+
if (unique.length >= 2 && unique.length <= 9) return `OneOf.OneOf<${unique.join(', ')}>`;
|
|
219
|
+
// OneOf supports arity 2-9. Higher-arity unions collapse to `object`,
|
|
220
|
+
// losing type information. Warn so the author knows the spec outgrew the
|
|
221
|
+
// runtime support instead of silently degrading.
|
|
222
|
+
if (unique.length >= 10) {
|
|
196
223
|
console.warn(
|
|
197
|
-
`[oagen:dotnet] Union with ${unique.length} variants exceeds
|
|
224
|
+
`[oagen:dotnet] Union with ${unique.length} variants exceeds OneOf<T0..T8> arity; falling back to object. Variants: ${unique.join(', ')}`,
|
|
198
225
|
);
|
|
199
226
|
}
|
|
200
227
|
return 'object';
|
package/src/dotnet/wrappers.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { EmitterContext, ResolvedOperation, ResolvedWrapper } from '@workos/oagen';
|
|
2
2
|
import {
|
|
3
|
-
className as csClassName,
|
|
4
3
|
fieldName as csFieldName,
|
|
5
4
|
methodName as csMethodName,
|
|
6
5
|
localName,
|
|
@@ -10,6 +9,8 @@ import {
|
|
|
10
9
|
escapeXml,
|
|
11
10
|
emitXmlDoc,
|
|
12
11
|
humanize,
|
|
12
|
+
appendAsyncSuffix,
|
|
13
|
+
modelClassName,
|
|
13
14
|
} from './naming.js';
|
|
14
15
|
import { sortPathParamsByTemplateOrder } from './resources.js';
|
|
15
16
|
import { resolveWrapperParams, formatWrapperDescription, type ResolvedWrapperParam } from '../shared/wrapper-utils.js';
|
|
@@ -44,9 +45,10 @@ function emitWrapperMethod(
|
|
|
44
45
|
_ctx: EmitterContext,
|
|
45
46
|
): void {
|
|
46
47
|
const op = resolvedOp.operation;
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
48
|
+
const methodStem = csMethodName(wrapper.name);
|
|
49
|
+
const method = appendAsyncSuffix(methodStem);
|
|
50
|
+
const optionsClass = `${methodStem}Options`;
|
|
51
|
+
const responseType = wrapper.responseModelName ? modelClassName(wrapper.responseModelName) : null;
|
|
50
52
|
|
|
51
53
|
// XML doc
|
|
52
54
|
lines.push(` /// <summary>${formatWrapperDescription(wrapper.name)}.</summary>`);
|
|
@@ -63,12 +65,18 @@ function emitWrapperMethod(
|
|
|
63
65
|
|
|
64
66
|
// Signature
|
|
65
67
|
const sigParams: string[] = [];
|
|
68
|
+
const argNames: string[] = [];
|
|
66
69
|
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
67
|
-
|
|
70
|
+
const name = localName(p.name);
|
|
71
|
+
sigParams.push(`string ${name}`);
|
|
72
|
+
argNames.push(name);
|
|
68
73
|
}
|
|
69
74
|
sigParams.push(`${optionsClass} options`);
|
|
75
|
+
argNames.push('options');
|
|
70
76
|
sigParams.push('RequestOptions? requestOptions = null');
|
|
77
|
+
argNames.push('requestOptions');
|
|
71
78
|
sigParams.push('CancellationToken cancellationToken = default');
|
|
79
|
+
argNames.push('cancellationToken');
|
|
72
80
|
|
|
73
81
|
const returnType = responseType ? `Task<${responseType}>` : 'Task';
|
|
74
82
|
lines.push(` public async ${returnType} ${method}(${sigParams.join(', ')})`);
|
|
@@ -96,7 +104,7 @@ function emitWrapperMethod(
|
|
|
96
104
|
if (op.pathParams.length > 0) {
|
|
97
105
|
let interpolated = op.path;
|
|
98
106
|
for (const p of sortPathParamsByTemplateOrder(op)) {
|
|
99
|
-
interpolated = interpolated.replace(`{${p.name}}`, `{${localName(p.name)}}`);
|
|
107
|
+
interpolated = interpolated.replace(`{${p.name}}`, `{Uri.EscapeDataString(${localName(p.name)})}`);
|
|
100
108
|
}
|
|
101
109
|
pathExpr = `$"${interpolated}"`;
|
|
102
110
|
} else {
|
|
@@ -116,6 +124,13 @@ function emitWrapperMethod(
|
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
lines.push(' }');
|
|
127
|
+
|
|
128
|
+
lines.push('');
|
|
129
|
+
lines.push(` /// <summary>Compatibility wrapper for <see cref="${method}"/>.</summary>`);
|
|
130
|
+
lines.push(` public Task${responseType ? `<${responseType}>` : ''} ${methodStem}(${sigParams.join(', ')})`);
|
|
131
|
+
lines.push(' {');
|
|
132
|
+
lines.push(` return this.${method}(${argNames.join(', ')});`);
|
|
133
|
+
lines.push(' }');
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
// NOTE: T26 (wrapper DRY) — the AuthenticateWith* wrappers share a small
|
|
@@ -158,8 +173,6 @@ export function generateWrapperOptionsClasses(resolvedOp: ResolvedOperation, ctx
|
|
|
158
173
|
// Hidden fields (defaults + inferred)
|
|
159
174
|
for (const key of Object.keys(wrapper.defaults)) {
|
|
160
175
|
const csField = csFieldName(key);
|
|
161
|
-
lines.push(` [JsonProperty("${key}")]`);
|
|
162
|
-
lines.push(` [STJS.JsonPropertyName("${key}")]`);
|
|
163
176
|
lines.push(` internal string ${csField} { get; set; } = default!;`);
|
|
164
177
|
lines.push('');
|
|
165
178
|
}
|
|
@@ -167,8 +180,6 @@ export function generateWrapperOptionsClasses(resolvedOp: ResolvedOperation, ctx
|
|
|
167
180
|
const csField = csFieldName(key);
|
|
168
181
|
// Skip if already added as a default
|
|
169
182
|
if (Object.keys(wrapper.defaults).includes(key)) continue;
|
|
170
|
-
lines.push(` [JsonProperty("${key}")]`);
|
|
171
|
-
lines.push(` [STJS.JsonPropertyName("${key}")]`);
|
|
172
183
|
lines.push(` internal string ${csField} { get; set; } = default!;`);
|
|
173
184
|
lines.push('');
|
|
174
185
|
}
|
package/src/go/client.ts
CHANGED
|
@@ -2,8 +2,9 @@ import type { ApiSpec, EmitterContext, GeneratedFile, Service } from '@workos/oa
|
|
|
2
2
|
import { toPascalCase, toSnakeCase } from '@workos/oagen';
|
|
3
3
|
// naming utilities used indirectly via resolveResourceClassName
|
|
4
4
|
import { resolveResourceClassName } from './resources.js';
|
|
5
|
-
import { unexportedName } from './naming.js';
|
|
5
|
+
import { className, unexportedName } from './naming.js';
|
|
6
6
|
import { getMountTarget } from '../shared/resolved-ops.js';
|
|
7
|
+
import { NON_SPEC_SERVICES } from '../shared/non-spec-services.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Generate the Go client file with service accessors.
|
|
@@ -15,6 +16,16 @@ export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFil
|
|
|
15
16
|
return [generateWorkOSFile(spec, ctx)];
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Non-spec services marked with `hasClientAccessor: true` (passwordless, vault)
|
|
21
|
+
* are included in the generated Client struct, constructor, and accessor methods
|
|
22
|
+
* — identical to spec-driven services. Their service type (e.g. PasswordlessService)
|
|
23
|
+
* is defined in a hand-written @oagen-ignore-file, but the Client wiring is generated.
|
|
24
|
+
*
|
|
25
|
+
* Other non-spec modules (webhook_verification, actions, etc.) remain fully
|
|
26
|
+
* self-contained in their @oagen-ignore-file files.
|
|
27
|
+
*/
|
|
28
|
+
|
|
18
29
|
/**
|
|
19
30
|
* Deduplicate services by mount target.
|
|
20
31
|
*/
|
|
@@ -72,6 +83,8 @@ function generateWorkOSFile(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
|
72
83
|
lines.push('\tbaseURL string');
|
|
73
84
|
lines.push('\thttpClient *http.Client');
|
|
74
85
|
lines.push('\tmaxRetries int');
|
|
86
|
+
lines.push('\tlogger Logger');
|
|
87
|
+
lines.push('\tappInfo appInfo');
|
|
75
88
|
lines.push('');
|
|
76
89
|
// Service fields
|
|
77
90
|
for (const service of topLevel) {
|
|
@@ -80,6 +93,11 @@ function generateWorkOSFile(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
|
80
93
|
const serviceTypeName = serviceType(resolvedName);
|
|
81
94
|
lines.push(`\t${fieldNameStr} *${serviceTypeName}`);
|
|
82
95
|
}
|
|
96
|
+
// Non-spec service fields (hand-written types, generated wiring)
|
|
97
|
+
for (const ns of NON_SPEC_SERVICES.filter((s) => s.hasClientAccessor)) {
|
|
98
|
+
const name = className(toPascalCase(ns.id));
|
|
99
|
+
lines.push(`\t${unexportedName(name)} *${serviceType(name)}`);
|
|
100
|
+
}
|
|
83
101
|
lines.push('}');
|
|
84
102
|
lines.push('');
|
|
85
103
|
|
|
@@ -102,6 +120,11 @@ function generateWorkOSFile(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
|
102
120
|
const serviceTypeName = serviceType(resolvedName);
|
|
103
121
|
lines.push(`\tc.${fieldNameStr} = &${serviceTypeName}{client: c}`);
|
|
104
122
|
}
|
|
123
|
+
// Initialize non-spec services
|
|
124
|
+
for (const ns of NON_SPEC_SERVICES.filter((s) => s.hasClientAccessor)) {
|
|
125
|
+
const name = className(toPascalCase(ns.id));
|
|
126
|
+
lines.push(`\tc.${unexportedName(name)} = &${serviceType(name)}{client: c}`);
|
|
127
|
+
}
|
|
105
128
|
lines.push('\treturn c');
|
|
106
129
|
lines.push('}');
|
|
107
130
|
lines.push('');
|
|
@@ -118,7 +141,16 @@ function generateWorkOSFile(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
|
|
|
118
141
|
lines.push('}');
|
|
119
142
|
lines.push('');
|
|
120
143
|
}
|
|
121
|
-
|
|
144
|
+
// Non-spec service accessor methods
|
|
145
|
+
for (const ns of NON_SPEC_SERVICES.filter((s) => s.hasClientAccessor)) {
|
|
146
|
+
const name = className(toPascalCase(ns.id));
|
|
147
|
+
const typeName = serviceType(name);
|
|
148
|
+
lines.push(`// ${name} returns the ${name} service.`);
|
|
149
|
+
lines.push(`func (c *Client) ${name}() *${typeName} {`);
|
|
150
|
+
lines.push(`\treturn c.${unexportedName(name)}`);
|
|
151
|
+
lines.push('}');
|
|
152
|
+
lines.push('');
|
|
153
|
+
}
|
|
122
154
|
return {
|
|
123
155
|
path: `${ctx.namespace}.go`,
|
|
124
156
|
content: lines.join('\n'),
|
|
@@ -137,5 +169,5 @@ function singularizePascal(name: string): string {
|
|
|
137
169
|
}
|
|
138
170
|
|
|
139
171
|
function serviceType(name: string): string {
|
|
140
|
-
return `${
|
|
172
|
+
return `${className(singularizePascal(name))}Service`;
|
|
141
173
|
}
|
package/src/go/enums.ts
CHANGED
|
@@ -29,6 +29,10 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
29
29
|
if (canonicalName) {
|
|
30
30
|
const aliasType = className(enumDef.name);
|
|
31
31
|
const canonicalType = className(canonicalName);
|
|
32
|
+
// Skip when different IR names map to the same Go type (e.g. synthetic
|
|
33
|
+
// enums from enrichModelsFromSpec whose underscore names collapse to the
|
|
34
|
+
// same PascalCase as the original enum).
|
|
35
|
+
if (aliasType === canonicalType) continue;
|
|
32
36
|
lines.push(`// ${aliasType} is an alias for ${canonicalType}.`);
|
|
33
37
|
lines.push(`type ${aliasType} = ${canonicalType}`);
|
|
34
38
|
lines.push('');
|
package/src/go/index.ts
CHANGED
|
@@ -72,12 +72,17 @@ export const goEmitter: Emitter = {
|
|
|
72
72
|
return '// Code generated by oagen. DO NOT EDIT.';
|
|
73
73
|
},
|
|
74
74
|
|
|
75
|
-
formatCommand(
|
|
76
|
-
//
|
|
77
|
-
//
|
|
75
|
+
formatCommand(_targetDir: string): FormatCommand | null {
|
|
76
|
+
// oagen appends all generated file paths (including .json fixtures) to the
|
|
77
|
+
// format command. gofmt errors on non-.go files, so filter them out.
|
|
78
|
+
// Same pattern as the Python emitter's ruff wrapper.
|
|
78
79
|
return {
|
|
79
|
-
cmd: '
|
|
80
|
-
args: [
|
|
80
|
+
cmd: 'bash',
|
|
81
|
+
args: [
|
|
82
|
+
'-c',
|
|
83
|
+
'GO_FILES=$(printf "%s\\n" "$@" | grep "\\.go$"); [ -n "$GO_FILES" ] && echo "$GO_FILES" | xargs gofmt -w',
|
|
84
|
+
'--',
|
|
85
|
+
],
|
|
81
86
|
batchSize: 999999,
|
|
82
87
|
};
|
|
83
88
|
},
|
package/src/go/models.ts
CHANGED
|
@@ -59,9 +59,14 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
|
|
|
59
59
|
if (batchedAliases.has(model.name)) continue;
|
|
60
60
|
|
|
61
61
|
const canonicalStruct = className(canonicalName);
|
|
62
|
+
// Skip when different IR names map to the same Go type (e.g. synthetic
|
|
63
|
+
// models from enrichModelsFromSpec whose underscore names collapse to the
|
|
64
|
+
// same PascalCase as the original model).
|
|
65
|
+
if (structName === canonicalStruct) continue;
|
|
66
|
+
|
|
62
67
|
const hash = modelHashMap.get(model.name)!;
|
|
63
68
|
const groupNames = hashGroups.get(hash) ?? [];
|
|
64
|
-
const aliases = groupNames.filter((n) => aliasOf.has(n));
|
|
69
|
+
const aliases = groupNames.filter((n) => aliasOf.has(n) && className(n) !== className(aliasOf.get(n)!));
|
|
65
70
|
|
|
66
71
|
if (aliases.length >= 5) {
|
|
67
72
|
// Batch emit all aliases for this group at once
|