@workos/oagen-emitters 0.3.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 (128) 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 +15 -0
  9. package/README.md +35 -224
  10. package/dist/index.d.mts +12 -1
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +2 -12737
  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 +336 -0
  19. package/oagen.config.ts +5 -343
  20. package/package.json +10 -34
  21. package/smoke/sdk-dotnet.ts +45 -12
  22. package/src/dotnet/client.ts +89 -0
  23. package/src/dotnet/enums.ts +323 -0
  24. package/src/dotnet/fixtures.ts +236 -0
  25. package/src/dotnet/index.ts +248 -0
  26. package/src/dotnet/manifest.ts +36 -0
  27. package/src/dotnet/models.ts +320 -0
  28. package/src/dotnet/naming.ts +368 -0
  29. package/src/dotnet/resources.ts +943 -0
  30. package/src/dotnet/tests.ts +713 -0
  31. package/src/dotnet/type-map.ts +228 -0
  32. package/src/dotnet/wrappers.ts +197 -0
  33. package/src/go/client.ts +35 -3
  34. package/src/go/enums.ts +4 -0
  35. package/src/go/index.ts +15 -7
  36. package/src/go/models.ts +6 -1
  37. package/src/go/naming.ts +5 -17
  38. package/src/go/resources.ts +534 -73
  39. package/src/go/tests.ts +39 -3
  40. package/src/go/type-map.ts +8 -3
  41. package/src/go/wrappers.ts +79 -21
  42. package/src/index.ts +15 -0
  43. package/src/kotlin/client.ts +58 -0
  44. package/src/kotlin/enums.ts +189 -0
  45. package/src/kotlin/index.ts +92 -0
  46. package/src/kotlin/manifest.ts +55 -0
  47. package/src/kotlin/models.ts +486 -0
  48. package/src/kotlin/naming.ts +229 -0
  49. package/src/kotlin/overrides.ts +25 -0
  50. package/src/kotlin/resources.ts +998 -0
  51. package/src/kotlin/tests.ts +1133 -0
  52. package/src/kotlin/type-map.ts +123 -0
  53. package/src/kotlin/wrappers.ts +168 -0
  54. package/src/node/client.ts +84 -7
  55. package/src/node/field-plan.ts +12 -14
  56. package/src/node/fixtures.ts +39 -3
  57. package/src/node/index.ts +1 -0
  58. package/src/node/models.ts +281 -37
  59. package/src/node/resources.ts +319 -95
  60. package/src/node/tests.ts +108 -29
  61. package/src/node/type-map.ts +1 -31
  62. package/src/node/utils.ts +96 -6
  63. package/src/node/wrappers.ts +31 -1
  64. package/src/php/client.ts +11 -3
  65. package/src/php/models.ts +0 -33
  66. package/src/php/naming.ts +2 -21
  67. package/src/php/resources.ts +275 -19
  68. package/src/php/tests.ts +118 -18
  69. package/src/php/type-map.ts +16 -2
  70. package/src/php/wrappers.ts +7 -2
  71. package/src/plugin.ts +50 -0
  72. package/src/python/client.ts +50 -32
  73. package/src/python/enums.ts +35 -10
  74. package/src/python/index.ts +35 -27
  75. package/src/python/models.ts +139 -2
  76. package/src/python/naming.ts +2 -22
  77. package/src/python/resources.ts +234 -17
  78. package/src/python/tests.ts +260 -16
  79. package/src/python/type-map.ts +16 -2
  80. package/src/ruby/client.ts +238 -0
  81. package/src/ruby/enums.ts +149 -0
  82. package/src/ruby/index.ts +93 -0
  83. package/src/ruby/manifest.ts +35 -0
  84. package/src/ruby/models.ts +360 -0
  85. package/src/ruby/naming.ts +187 -0
  86. package/src/ruby/rbi.ts +313 -0
  87. package/src/ruby/resources.ts +799 -0
  88. package/src/ruby/tests.ts +459 -0
  89. package/src/ruby/type-map.ts +97 -0
  90. package/src/ruby/wrappers.ts +161 -0
  91. package/src/shared/model-utils.ts +357 -16
  92. package/src/shared/naming-utils.ts +83 -0
  93. package/src/shared/non-spec-services.ts +13 -0
  94. package/src/shared/resolved-ops.ts +75 -1
  95. package/src/shared/wrapper-utils.ts +12 -1
  96. package/test/dotnet/client.test.ts +121 -0
  97. package/test/dotnet/enums.test.ts +193 -0
  98. package/test/dotnet/errors.test.ts +9 -0
  99. package/test/dotnet/manifest.test.ts +82 -0
  100. package/test/dotnet/models.test.ts +258 -0
  101. package/test/dotnet/resources.test.ts +387 -0
  102. package/test/dotnet/tests.test.ts +202 -0
  103. package/test/entrypoint.test.ts +89 -0
  104. package/test/go/client.test.ts +6 -6
  105. package/test/go/resources.test.ts +156 -7
  106. package/test/kotlin/models.test.ts +135 -0
  107. package/test/kotlin/resources.test.ts +210 -0
  108. package/test/kotlin/tests.test.ts +176 -0
  109. package/test/node/client.test.ts +74 -0
  110. package/test/node/models.test.ts +134 -1
  111. package/test/node/resources.test.ts +343 -34
  112. package/test/node/utils.test.ts +140 -0
  113. package/test/php/client.test.ts +2 -1
  114. package/test/php/models.test.ts +5 -4
  115. package/test/php/resources.test.ts +103 -0
  116. package/test/php/tests.test.ts +67 -0
  117. package/test/plugin.test.ts +50 -0
  118. package/test/python/client.test.ts +56 -0
  119. package/test/python/models.test.ts +99 -0
  120. package/test/python/resources.test.ts +294 -0
  121. package/test/python/tests.test.ts +91 -0
  122. package/test/ruby/client.test.ts +81 -0
  123. package/test/ruby/resources.test.ts +386 -0
  124. package/test/shared/resolved-ops.test.ts +122 -0
  125. package/tsdown.config.ts +1 -1
  126. package/dist/index.mjs.map +0 -1
  127. package/scripts/generate-php.js +0 -13
  128. package/scripts/git-push-with-published-oagen.sh +0 -21
@@ -0,0 +1,313 @@
1
+ import type { ApiSpec, EmitterContext, GeneratedFile, TypeRef, Model } from '@workos/oagen';
2
+ import { mapTypeRef as irMapTypeRef } from '@workos/oagen';
3
+ import { className, fieldName, fileName, safeParamName, resolveMethodName } from './naming.js';
4
+ import {
5
+ buildResolvedLookup,
6
+ groupByMount,
7
+ lookupResolved,
8
+ buildHiddenParams,
9
+ collectGroupedParamNames,
10
+ } from '../shared/resolved-ops.js';
11
+ import { isListWrapperModel, isListMetadataModel } from '../shared/model-utils.js';
12
+
13
+ /**
14
+ * Map an IR TypeRef to a Sorbet type string for RBI files.
15
+ */
16
+ function mapSorbetType(ref: TypeRef): string {
17
+ return irMapTypeRef<string>(ref, {
18
+ primitive: (r) => {
19
+ switch (r.type) {
20
+ case 'string':
21
+ return 'String';
22
+ case 'integer':
23
+ return 'Integer';
24
+ case 'number':
25
+ return 'Float';
26
+ case 'boolean':
27
+ return 'T::Boolean';
28
+ case 'unknown':
29
+ return 'T.untyped';
30
+ }
31
+ },
32
+ array: (_ref, items) => `T::Array[${items}]`,
33
+ model: (r) => `WorkOS::${className(r.name)}`,
34
+ enum: () => 'String',
35
+ union: (r, variants) => {
36
+ if (r.compositionKind === 'allOf') return variants[0] ?? 'T.untyped';
37
+ const unique = [...new Set(variants)];
38
+ if (unique.length === 1) return unique[0];
39
+ return `T.any(${unique.join(', ')})`;
40
+ },
41
+ nullable: (_ref, inner) => `T.nilable(${inner})`,
42
+ literal: (r) =>
43
+ typeof r.value === 'string'
44
+ ? 'String'
45
+ : r.value === null
46
+ ? 'NilClass'
47
+ : typeof r.value === 'number'
48
+ ? Number.isInteger(r.value)
49
+ ? 'Integer'
50
+ : 'Float'
51
+ : 'T::Boolean',
52
+ map: (_ref, value) => `T::Hash[String, ${value}]`,
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Generate .rbi files for Sorbet type checking.
58
+ */
59
+ export function generateRbiFiles(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
60
+ const files: GeneratedFile[] = [];
61
+
62
+ const modelNames = new Set(spec.models.map((m) => m.name));
63
+ const _enumNames = new Set(spec.enums.map((e) => e.name));
64
+
65
+ // 1. Generate model RBI files
66
+ const models = (spec.models as Model[]).filter((m) => !isListWrapperModel(m) && !isListMetadataModel(m));
67
+
68
+ for (const model of models) {
69
+ const cls = className(model.name);
70
+ const lines: string[] = [];
71
+ lines.push('# typed: strong');
72
+ lines.push('');
73
+ lines.push('module WorkOS');
74
+ lines.push(` class ${cls}`);
75
+
76
+ // Constructor
77
+ lines.push(' sig { params(json: T.any(String, T::Hash[Symbol, T.untyped])).void }');
78
+ lines.push(' def initialize(json); end');
79
+ lines.push('');
80
+
81
+ // Field accessors
82
+ const seenFieldNames = new Set<string>();
83
+ for (const f of model.fields) {
84
+ const fname = fieldName(f.name);
85
+ if (seenFieldNames.has(fname)) continue;
86
+ seenFieldNames.add(fname);
87
+ const sorbetType = f.required ? mapSorbetType(f.type) : `T.nilable(${unwrapNilable(mapSorbetType(f.type))})`;
88
+ lines.push(` sig { returns(${sorbetType}) }`);
89
+ lines.push(` def ${fname}; end`);
90
+ lines.push('');
91
+ lines.push(` sig { params(value: ${sorbetType}).returns(${sorbetType}) }`);
92
+ lines.push(` def ${fname}=(value); end`);
93
+ lines.push('');
94
+ }
95
+
96
+ // to_h and to_json
97
+ lines.push(' sig { returns(T::Hash[Symbol, T.untyped]) }');
98
+ lines.push(' def to_h; end');
99
+ lines.push('');
100
+ lines.push(' sig { params(args: T.untyped).returns(String) }');
101
+ lines.push(' def to_json(*args); end');
102
+
103
+ lines.push(' end');
104
+ lines.push('end');
105
+
106
+ files.push({
107
+ path: `rbi/workos/${fileName(model.name)}.rbi`,
108
+ content: lines.join('\n'),
109
+ integrateTarget: true,
110
+ overwriteExisting: true,
111
+ });
112
+ }
113
+
114
+ // 2. Generate service RBI files
115
+ const groups = groupByMount(ctx);
116
+ const lookup = buildResolvedLookup(ctx);
117
+ const modelByName = new Map<string, Model>();
118
+ for (const m of spec.models as Model[]) modelByName.set(m.name, m);
119
+ const listWrapperModels = new Map<string, Model>();
120
+ for (const m of spec.models as Model[]) {
121
+ if (isListWrapperModel(m)) listWrapperModels.set(m.name, m);
122
+ }
123
+
124
+ for (const [mountTarget, group] of groups) {
125
+ const cls = className(mountTarget);
126
+ const lines: string[] = [];
127
+ lines.push('# typed: strong');
128
+ lines.push('');
129
+ lines.push('module WorkOS');
130
+ lines.push(` class ${cls}`);
131
+
132
+ lines.push(' sig { params(client: WorkOS::BaseClient).void }');
133
+ lines.push(' def initialize(client); end');
134
+ lines.push('');
135
+
136
+ const emittedMethods = new Set<string>();
137
+
138
+ for (const op of group.operations) {
139
+ const ownerService =
140
+ group.resolvedOps.find((r) => r.operation === op)?.service ??
141
+ spec.services.find((s) => s.operations.includes(op)) ??
142
+ spec.services[0];
143
+ const method = resolveMethodName(op, ownerService, ctx);
144
+ if (emittedMethods.has(method)) continue;
145
+
146
+ const resolved = lookupResolved(op, lookup);
147
+ if (resolved?.urlBuilder) {
148
+ emittedMethods.add(method);
149
+ continue;
150
+ }
151
+ emittedMethods.add(method);
152
+
153
+ const hiddenParams = buildHiddenParams(resolved);
154
+ const groupedParamNames = collectGroupedParamNames(op);
155
+ const queryParams = (op.queryParams ?? []).filter((q) => !groupedParamNames.has(q.name));
156
+ const bodyFields = getRequestBodyFieldsFlat(op, hiddenParams, modelByName);
157
+
158
+ // Build parameter list for sig
159
+ const sigParams: string[] = [];
160
+ const seen = new Set<string>();
161
+
162
+ for (const p of op.pathParams ?? []) {
163
+ const n = safeParamName(p.name);
164
+ if (seen.has(n)) continue;
165
+ seen.add(n);
166
+ sigParams.push(`${n}: ${mapSorbetType(p.type)}`);
167
+ }
168
+ for (const f of bodyFields) {
169
+ if (hiddenParams.has(f.name)) continue;
170
+ if (!f.required) continue;
171
+ const n = fieldName(f.name);
172
+ if (seen.has(n)) continue;
173
+ seen.add(n);
174
+ sigParams.push(`${n}: ${mapSorbetType(f.type)}`);
175
+ }
176
+ for (const q of queryParams) {
177
+ if (hiddenParams.has(q.name)) continue;
178
+ if (!q.required) continue;
179
+ const n = safeParamName(q.name);
180
+ if (seen.has(n)) continue;
181
+ seen.add(n);
182
+ sigParams.push(`${n}: ${mapSorbetType(q.type)}`);
183
+ }
184
+ for (const f of bodyFields) {
185
+ if (hiddenParams.has(f.name)) continue;
186
+ if (f.required) continue;
187
+ const n = fieldName(f.name);
188
+ if (seen.has(n)) continue;
189
+ seen.add(n);
190
+ sigParams.push(`${n}: T.nilable(${unwrapNilable(mapSorbetType(f.type))})`);
191
+ }
192
+ for (const q of queryParams) {
193
+ if (hiddenParams.has(q.name)) continue;
194
+ if (q.required) continue;
195
+ const n = safeParamName(q.name);
196
+ if (seen.has(n)) continue;
197
+ seen.add(n);
198
+ sigParams.push(`${n}: T.nilable(${unwrapNilable(mapSorbetType(q.type))})`);
199
+ }
200
+ sigParams.push('request_options: T::Hash[Symbol, T.untyped]');
201
+
202
+ // Return type
203
+ const retType = mapSorbetReturnType(op.response, listWrapperModels, modelNames);
204
+
205
+ lines.push(' sig do');
206
+ lines.push(' params(');
207
+ for (let i = 0; i < sigParams.length; i++) {
208
+ const sep = i === sigParams.length - 1 ? '' : ',';
209
+ lines.push(` ${sigParams[i]}${sep}`);
210
+ }
211
+ lines.push(` ).returns(${retType})`);
212
+ lines.push(' end');
213
+ lines.push(` def ${method}(${sigParams.map((p) => p.split(':')[0].trim() + ':').join(', ')}); end`);
214
+ lines.push('');
215
+ }
216
+
217
+ lines.push(' end');
218
+ lines.push('end');
219
+
220
+ files.push({
221
+ path: `rbi/workos/${fileName(mountTarget)}.rbi`,
222
+ content: lines.join('\n'),
223
+ integrateTarget: true,
224
+ overwriteExisting: true,
225
+ });
226
+ }
227
+
228
+ // 3. Generate client RBI file
229
+ {
230
+ const lines: string[] = [];
231
+ lines.push('# typed: strong');
232
+ lines.push('');
233
+ lines.push('module WorkOS');
234
+ lines.push(' class Client < BaseClient');
235
+
236
+ for (const [mountTarget] of groups) {
237
+ const cls = className(mountTarget);
238
+ const prop = mountTarget
239
+ .replace(/-/g, '_')
240
+ .replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`)
241
+ .replace(/^_/, '');
242
+ lines.push(` sig { returns(WorkOS::${cls}) }`);
243
+ lines.push(` def ${prop}; end`);
244
+ lines.push('');
245
+ }
246
+
247
+ lines.push(' end');
248
+ lines.push('end');
249
+
250
+ files.push({
251
+ path: 'rbi/workos/client.rbi',
252
+ content: lines.join('\n'),
253
+ integrateTarget: true,
254
+ overwriteExisting: true,
255
+ });
256
+ }
257
+
258
+ return files;
259
+ }
260
+
261
+ /** Unwrap T.nilable(...) if already wrapped, to avoid double-wrapping. */
262
+ function unwrapNilable(type: string): string {
263
+ const match = type.match(/^T\.nilable\((.+)\)$/);
264
+ return match ? match[1] : type;
265
+ }
266
+
267
+ /** Map a response TypeRef to a Sorbet return type. */
268
+ function mapSorbetReturnType(ref: TypeRef, listWrapperModels: Map<string, Model>, modelNames: Set<string>): string {
269
+ if (ref.kind === 'model' && listWrapperModels.has(ref.name)) {
270
+ return 'WorkOS::Types::ListStruct';
271
+ }
272
+ if (ref.kind === 'model' && modelNames.has(ref.name)) {
273
+ return `WorkOS::${className(ref.name)}`;
274
+ }
275
+ if (ref.kind === 'array' && ref.items.kind === 'model' && modelNames.has(ref.items.name)) {
276
+ return `T::Array[WorkOS::${className(ref.items.name)}]`;
277
+ }
278
+ if (ref.kind === 'nullable') {
279
+ return `T.nilable(${mapSorbetReturnType(ref.inner, listWrapperModels, modelNames)})`;
280
+ }
281
+ if (ref.kind === 'primitive' && ref.type === 'unknown') {
282
+ return 'NilClass';
283
+ }
284
+ return mapSorbetType(ref);
285
+ }
286
+
287
+ /** Get body fields (flat) for RBI sig generation. */
288
+ function getRequestBodyFieldsFlat(
289
+ op: { requestBody?: TypeRef },
290
+ hiddenParams: Set<string>,
291
+ modelByName: Map<string, Model>,
292
+ ): { name: string; required: boolean; type: TypeRef }[] {
293
+ void hiddenParams;
294
+ const ref = op.requestBody;
295
+ if (!ref) return [];
296
+ if (ref.kind === 'model') {
297
+ const model = modelByName.get(ref.name);
298
+ if (!model) return [];
299
+ return model.fields.map((f) => ({ name: f.name, required: f.required, type: f.type }));
300
+ }
301
+ if (ref.kind === 'nullable') {
302
+ return getRequestBodyFieldsFlat({ requestBody: ref.inner }, hiddenParams, modelByName);
303
+ }
304
+ if (ref.kind === 'union') {
305
+ for (const v of ref.variants) {
306
+ if (v.kind === 'model') {
307
+ const model = modelByName.get(v.name);
308
+ if (model) return model.fields.map((f) => ({ name: f.name, required: f.required, type: f.type }));
309
+ }
310
+ }
311
+ }
312
+ return [];
313
+ }