@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.
Files changed (105) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/lint.yml +1 -1
  3. package/.github/workflows/release-please.yml +2 -2
  4. package/.github/workflows/release.yml +1 -1
  5. package/.husky/pre-push +11 -0
  6. package/.node-version +1 -1
  7. package/.release-please-manifest.json +1 -1
  8. package/CHANGELOG.md +8 -0
  9. package/README.md +35 -224
  10. package/dist/index.d.mts +9 -1
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +2 -15234
  13. package/dist/plugin-BSop9f9z.mjs +21471 -0
  14. package/dist/plugin-BSop9f9z.mjs.map +1 -0
  15. package/dist/plugin.d.mts +7 -0
  16. package/dist/plugin.d.mts.map +1 -0
  17. package/dist/plugin.mjs +2 -0
  18. package/docs/sdk-architecture/dotnet.md +5 -5
  19. package/oagen.config.ts +5 -373
  20. package/package.json +10 -34
  21. package/src/dotnet/index.ts +6 -4
  22. package/src/dotnet/models.ts +58 -82
  23. package/src/dotnet/naming.ts +44 -6
  24. package/src/dotnet/resources.ts +350 -29
  25. package/src/dotnet/tests.ts +44 -24
  26. package/src/dotnet/type-map.ts +44 -17
  27. package/src/dotnet/wrappers.ts +21 -10
  28. package/src/go/client.ts +35 -3
  29. package/src/go/enums.ts +4 -0
  30. package/src/go/index.ts +10 -5
  31. package/src/go/models.ts +6 -1
  32. package/src/go/resources.ts +534 -73
  33. package/src/go/tests.ts +39 -3
  34. package/src/go/type-map.ts +8 -3
  35. package/src/go/wrappers.ts +79 -21
  36. package/src/index.ts +14 -0
  37. package/src/kotlin/client.ts +7 -2
  38. package/src/kotlin/enums.ts +30 -3
  39. package/src/kotlin/models.ts +97 -6
  40. package/src/kotlin/naming.ts +7 -1
  41. package/src/kotlin/resources.ts +370 -39
  42. package/src/kotlin/tests.ts +120 -6
  43. package/src/node/client.ts +38 -11
  44. package/src/node/field-plan.ts +12 -14
  45. package/src/node/fixtures.ts +39 -3
  46. package/src/node/models.ts +281 -37
  47. package/src/node/resources.ts +156 -52
  48. package/src/node/tests.ts +76 -27
  49. package/src/node/type-map.ts +1 -31
  50. package/src/node/utils.ts +96 -6
  51. package/src/node/wrappers.ts +31 -1
  52. package/src/php/models.ts +0 -33
  53. package/src/php/resources.ts +199 -18
  54. package/src/php/tests.ts +26 -2
  55. package/src/php/type-map.ts +16 -2
  56. package/src/php/wrappers.ts +6 -2
  57. package/src/plugin.ts +50 -0
  58. package/src/python/client.ts +13 -3
  59. package/src/python/enums.ts +28 -3
  60. package/src/python/index.ts +35 -27
  61. package/src/python/models.ts +138 -1
  62. package/src/python/resources.ts +234 -17
  63. package/src/python/tests.ts +260 -16
  64. package/src/python/type-map.ts +16 -2
  65. package/src/ruby/client.ts +238 -0
  66. package/src/ruby/enums.ts +149 -0
  67. package/src/ruby/index.ts +93 -0
  68. package/src/ruby/manifest.ts +35 -0
  69. package/src/ruby/models.ts +360 -0
  70. package/src/ruby/naming.ts +187 -0
  71. package/src/ruby/rbi.ts +313 -0
  72. package/src/ruby/resources.ts +799 -0
  73. package/src/ruby/tests.ts +459 -0
  74. package/src/ruby/type-map.ts +97 -0
  75. package/src/ruby/wrappers.ts +161 -0
  76. package/src/shared/model-utils.ts +131 -7
  77. package/src/shared/naming-utils.ts +36 -0
  78. package/src/shared/non-spec-services.ts +13 -0
  79. package/src/shared/resolved-ops.ts +75 -1
  80. package/test/dotnet/client.test.ts +2 -2
  81. package/test/dotnet/models.test.ts +7 -9
  82. package/test/dotnet/resources.test.ts +135 -3
  83. package/test/dotnet/tests.test.ts +5 -5
  84. package/test/entrypoint.test.ts +89 -0
  85. package/test/go/client.test.ts +6 -6
  86. package/test/go/resources.test.ts +156 -7
  87. package/test/kotlin/models.test.ts +1 -1
  88. package/test/kotlin/resources.test.ts +210 -0
  89. package/test/node/models.test.ts +134 -1
  90. package/test/node/resources.test.ts +134 -26
  91. package/test/node/utils.test.ts +140 -0
  92. package/test/php/models.test.ts +5 -4
  93. package/test/php/resources.test.ts +66 -1
  94. package/test/plugin.test.ts +50 -0
  95. package/test/python/client.test.ts +56 -0
  96. package/test/python/models.test.ts +99 -0
  97. package/test/python/resources.test.ts +294 -0
  98. package/test/python/tests.test.ts +91 -0
  99. package/test/ruby/client.test.ts +81 -0
  100. package/test/ruby/resources.test.ts +386 -0
  101. package/test/shared/resolved-ops.test.ts +122 -0
  102. package/tsdown.config.ts +1 -1
  103. package/dist/index.mjs.map +0 -1
  104. package/scripts/generate-php.js +0 -13
  105. package/scripts/git-push-with-published-oagen.sh +0 -21
@@ -1,14 +1,20 @@
1
1
  import type { Model, EmitterContext, GeneratedFile, TypeRef } from '@workos/oagen';
2
- import { mapTypeRef, isValueTypeRef, isEnumRef, emitJsonPropertyAttributes } from './type-map.js';
2
+ import {
3
+ mapTypeRef,
4
+ isValueTypeRef,
5
+ isEnumRef,
6
+ emitJsonPropertyAttributes,
7
+ setModelAliases,
8
+ isModelAlias,
9
+ } from './type-map.js';
3
10
  import {
4
11
  articleFor,
5
- className,
6
- escapeXml,
7
12
  fieldName,
8
13
  humanize,
9
14
  emitXmlDoc,
10
15
  deprecationMessage,
11
16
  escapeCsAttributeString,
17
+ modelClassName,
12
18
  } from './naming.js';
13
19
 
14
20
  // Import and re-export shared model detection utilities
@@ -35,90 +41,23 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
35
41
 
36
42
  const files: GeneratedFile[] = [];
37
43
 
38
- // Build structural hash for deduplication. Run the hash → canonical pass
39
- // iteratively so that parent classes whose only structural difference is
40
- // an already-aliased child type also collapse. Terminates when a full
41
- // round produces no new aliases.
42
- const eligibleModels = models.filter((m) => !isListWrapperModel(m) && !isListMetadataModel(m));
43
- const aliasOf = new Map<string, string>();
44
- while (true) {
45
- const hashGroups = new Map<string, string[]>();
46
- for (const model of eligibleModels) {
47
- const hash = structuralHash(model, aliasOf);
48
- if (!hashGroups.has(hash)) hashGroups.set(hash, []);
49
- hashGroups.get(hash)!.push(model.name);
50
- }
51
-
52
- let added = false;
53
- for (const [hash, names] of hashGroups) {
54
- if (names.length <= 1) continue;
55
- if (hash === '') continue;
56
- const sorted = [...names].sort();
57
- const canonical = sorted[0];
58
- for (let i = 1; i < sorted.length; i++) {
59
- const name = sorted[i];
60
- if (aliasOf.get(name) !== canonical) {
61
- aliasOf.set(name, canonical);
62
- added = true;
63
- }
64
- }
65
- }
66
- if (!added) break;
67
- }
44
+ // Compute and publish model aliases so mapTypeRef rewrites references.
45
+ primeModelAliases(models);
68
46
 
69
47
  for (const model of models) {
70
48
  if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
71
49
 
72
- const csClassName = className(model.name);
73
- const canonicalName = aliasOf.get(model.name);
74
-
75
- if (canonicalName) {
76
- // Emit alias as subclass of canonical
77
- const canonicalClass = className(canonicalName);
78
- const lines: string[] = [];
79
- lines.push(`namespace ${ctx.namespacePascal}`);
80
- lines.push('{');
81
- if (model.description) {
82
- const descLines = model.description
83
- .split('\n')
84
- .map((l) => l.trim())
85
- .filter((l) => l);
86
- lines.push(` /// <summary>${escapeXml(descLines[0])}</summary>`);
87
- if (descLines.length > 1) {
88
- lines.push(` /// <remarks>`);
89
- for (const remark of descLines.slice(1)) {
90
- lines.push(` /// ${escapeXml(remark)}`);
91
- }
92
- lines.push(` /// Structurally identical to <see cref="${canonicalClass}"/>.`);
93
- lines.push(` /// </remarks>`);
94
- } else {
95
- lines.push(` /// <remarks>Structurally identical to <see cref="${canonicalClass}"/>.</remarks>`);
96
- }
97
- } else {
98
- const human = humanize(model.name);
99
- lines.push(` /// <summary>Represents ${articleFor(human)} ${human}.</summary>`);
100
- lines.push(` /// <remarks>Structurally identical to <see cref="${canonicalClass}"/>.</remarks>`);
101
- }
102
- lines.push(` public class ${csClassName} : ${canonicalClass} { }`);
103
- lines.push('}');
50
+ const csClassName = modelClassName(model.name);
104
51
 
105
- files.push({
106
- path: `Entities/${csClassName}.cs`,
107
- content: lines.join('\n'),
108
- overwriteExisting: true,
109
- });
110
- continue;
111
- }
52
+ // Skip alias models — all references are already rewritten to the
53
+ // canonical type by mapTypeRef, so the alias class would be dead code.
54
+ if (isModelAlias(model.name)) continue;
112
55
 
113
56
  const lines: string[] = [];
114
- const needsCollections = model.fields.some((f) => {
115
- const csType = mapTypeRef(f.type);
116
- return csType.startsWith('List<') || csType.startsWith('Dictionary<');
117
- });
118
- const needsSystem = model.fields.some((f) => {
119
- const csType = mapTypeRef(f.type);
120
- return csType.includes('DateTimeOffset');
121
- });
57
+ const fieldTypes = model.fields.map((f) => mapTypeRef(f.type));
58
+ const needsCollections = fieldTypes.some((t) => t.startsWith('List<') || t.startsWith('Dictionary<'));
59
+ const needsSystem = fieldTypes.some((t) => t.includes('DateTimeOffset'));
60
+ const needsJsonAttrs = model.fields.some((f) => f.required && isEnumRef(f.type));
122
61
 
123
62
  lines.push(`namespace ${ctx.namespacePascal}`);
124
63
  lines.push('{');
@@ -128,8 +67,10 @@ export function generateModels(models: Model[], ctx: EmitterContext): GeneratedF
128
67
  if (needsCollections) {
129
68
  lines.push(' using System.Collections.Generic;');
130
69
  }
131
- lines.push(' using Newtonsoft.Json;');
132
- lines.push(' using STJS = System.Text.Json.Serialization;');
70
+ if (needsJsonAttrs) {
71
+ lines.push(' using Newtonsoft.Json;');
72
+ lines.push(' using STJS = System.Text.Json.Serialization;');
73
+ }
133
74
  lines.push('');
134
75
 
135
76
  // XML doc comment
@@ -305,6 +246,41 @@ function singleValueConstInitializer(ref: TypeRef, enumConstByName: Map<string,
305
246
  return JSON.stringify(wire);
306
247
  }
307
248
 
249
+ /**
250
+ * Compute and publish the model alias map. Safe to call multiple times
251
+ * (idempotent for a given set of models). Must be invoked before any emitter
252
+ * phase that calls `mapTypeRef` with model references.
253
+ */
254
+ export function primeModelAliases(models: Model[]): void {
255
+ const eligibleModels = models.filter((m) => !isListWrapperModel(m) && !isListMetadataModel(m));
256
+ const aliasOf = new Map<string, string>();
257
+ while (true) {
258
+ const hashGroups = new Map<string, string[]>();
259
+ for (const model of eligibleModels) {
260
+ const hash = structuralHash(model, aliasOf);
261
+ if (!hashGroups.has(hash)) hashGroups.set(hash, []);
262
+ hashGroups.get(hash)!.push(model.name);
263
+ }
264
+
265
+ let added = false;
266
+ for (const [hash, names] of hashGroups) {
267
+ if (names.length <= 1) continue;
268
+ if (hash === '') continue;
269
+ const sorted = [...names].sort();
270
+ const canonical = sorted[0];
271
+ for (let i = 1; i < sorted.length; i++) {
272
+ const name = sorted[i];
273
+ if (aliasOf.get(name) !== canonical) {
274
+ aliasOf.set(name, canonical);
275
+ added = true;
276
+ }
277
+ }
278
+ }
279
+ if (!added) break;
280
+ }
281
+ setModelAliases(aliasOf);
282
+ }
283
+
308
284
  /**
309
285
  * Normalize a TypeRef for structural comparison.
310
286
  * Enum references are normalized to their values (not names) so that
@@ -1,6 +1,6 @@
1
1
  import type { Operation, Service, EmitterContext } from '@workos/oagen';
2
2
  import { toPascalCase, toSnakeCase } from '@workos/oagen';
3
- import { buildResolvedLookup, lookupMethodName, getMountTarget } from '../shared/resolved-ops.js';
3
+ import { buildResolvedLookup, lookupMethodName, lookupResolved, getMountTarget } from '../shared/resolved-ops.js';
4
4
  import { stripUrnPrefix } from '../shared/naming-utils.js';
5
5
 
6
6
  /** PascalCase class/type name. */
@@ -8,6 +8,17 @@ export function className(name: string): string {
8
8
  return toPascalCase(stripUrnPrefix(name));
9
9
  }
10
10
 
11
+ /** Display name for a model type, including consumer-friendly aliases. */
12
+ export function modelClassName(name: string): string {
13
+ switch (name) {
14
+ case 'EmailChangeConfirmationUser':
15
+ case 'UserlandUser':
16
+ return 'User';
17
+ default:
18
+ return className(name);
19
+ }
20
+ }
21
+
11
22
  /** PascalCase file name (without extension). */
12
23
  export function fileName(name: string): string {
13
24
  return toPascalCase(stripUrnPrefix(name));
@@ -57,15 +68,42 @@ export function resolveServiceDir(resolvedServiceName: string): string {
57
68
  return moduleName(resolvedServiceName);
58
69
  }
59
70
 
60
- /** Resolve the SDK method name for an operation. */
61
- export function resolveMethodName(op: Operation, _service: Service, ctx: EmitterContext): string {
71
+ function trimAsyncSuffix(name: string): string {
72
+ return name.endsWith('Async') ? name.slice(0, -5) : name;
73
+ }
74
+
75
+ /** Append Async once for TAP-style method names. */
76
+ export function appendAsyncSuffix(name: string): string {
77
+ return name.endsWith('Async') ? name : `${name}Async`;
78
+ }
79
+
80
+ /** Resolve the stable method stem for an operation (without any Async suffix). */
81
+ export function resolveMethodStem(op: Operation, _service: Service, ctx: EmitterContext): string {
62
82
  const lookup = buildResolvedLookup(ctx);
63
83
  const resolved = lookupMethodName(op, lookup);
64
- if (resolved) return trimMountedResourceFromMethod(methodName(resolved), resolveClassName(_service, ctx));
84
+ if (resolved) {
85
+ return trimMountedResourceFromMethod(methodName(trimAsyncSuffix(resolved)), resolveClassName(_service, ctx));
86
+ }
65
87
  const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
66
88
  const existing = ctx.overlayLookup?.methodByOperation?.get(httpKey);
67
- if (existing) return trimMountedResourceFromMethod(methodName(existing.methodName), resolveClassName(_service, ctx));
68
- return trimMountedResourceFromMethod(methodName(op.name), resolveClassName(_service, ctx));
89
+ if (existing) {
90
+ return trimMountedResourceFromMethod(
91
+ methodName(trimAsyncSuffix(existing.methodName)),
92
+ resolveClassName(_service, ctx),
93
+ );
94
+ }
95
+ return trimMountedResourceFromMethod(methodName(trimAsyncSuffix(op.name)), resolveClassName(_service, ctx));
96
+ }
97
+
98
+ /** Resolve the SDK method name for an operation. */
99
+ export function resolveMethodName(op: Operation, service: Service, ctx: EmitterContext): string {
100
+ const stem = resolveMethodStem(op, service, ctx);
101
+ const resolved = lookupResolved(op, buildResolvedLookup(ctx));
102
+ if (resolved?.urlBuilder ?? false) {
103
+ return stem;
104
+ }
105
+
106
+ return appendAsyncSuffix(stem);
69
107
  }
70
108
 
71
109
  /** Resolve the SDK class name for a service. */