@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.
- 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 +15 -0
- package/README.md +35 -224
- package/dist/index.d.mts +12 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -12737
- 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 +336 -0
- package/oagen.config.ts +5 -343
- package/package.json +10 -34
- package/smoke/sdk-dotnet.ts +45 -12
- package/src/dotnet/client.ts +89 -0
- package/src/dotnet/enums.ts +323 -0
- package/src/dotnet/fixtures.ts +236 -0
- package/src/dotnet/index.ts +248 -0
- package/src/dotnet/manifest.ts +36 -0
- package/src/dotnet/models.ts +320 -0
- package/src/dotnet/naming.ts +368 -0
- package/src/dotnet/resources.ts +943 -0
- package/src/dotnet/tests.ts +713 -0
- package/src/dotnet/type-map.ts +228 -0
- package/src/dotnet/wrappers.ts +197 -0
- package/src/go/client.ts +35 -3
- package/src/go/enums.ts +4 -0
- package/src/go/index.ts +15 -7
- package/src/go/models.ts +6 -1
- package/src/go/naming.ts +5 -17
- 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 +15 -0
- package/src/kotlin/client.ts +58 -0
- package/src/kotlin/enums.ts +189 -0
- package/src/kotlin/index.ts +92 -0
- package/src/kotlin/manifest.ts +55 -0
- package/src/kotlin/models.ts +486 -0
- package/src/kotlin/naming.ts +229 -0
- package/src/kotlin/overrides.ts +25 -0
- package/src/kotlin/resources.ts +998 -0
- package/src/kotlin/tests.ts +1133 -0
- package/src/kotlin/type-map.ts +123 -0
- package/src/kotlin/wrappers.ts +168 -0
- package/src/node/client.ts +84 -7
- package/src/node/field-plan.ts +12 -14
- package/src/node/fixtures.ts +39 -3
- package/src/node/index.ts +1 -0
- package/src/node/models.ts +281 -37
- package/src/node/resources.ts +319 -95
- package/src/node/tests.ts +108 -29
- 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/client.ts +11 -3
- package/src/php/models.ts +0 -33
- package/src/php/naming.ts +2 -21
- package/src/php/resources.ts +275 -19
- package/src/php/tests.ts +118 -18
- package/src/php/type-map.ts +16 -2
- package/src/php/wrappers.ts +7 -2
- package/src/plugin.ts +50 -0
- package/src/python/client.ts +50 -32
- package/src/python/enums.ts +35 -10
- package/src/python/index.ts +35 -27
- package/src/python/models.ts +139 -2
- package/src/python/naming.ts +2 -22
- 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 +357 -16
- package/src/shared/naming-utils.ts +83 -0
- package/src/shared/non-spec-services.ts +13 -0
- package/src/shared/resolved-ops.ts +75 -1
- package/src/shared/wrapper-utils.ts +12 -1
- package/test/dotnet/client.test.ts +121 -0
- package/test/dotnet/enums.test.ts +193 -0
- package/test/dotnet/errors.test.ts +9 -0
- package/test/dotnet/manifest.test.ts +82 -0
- package/test/dotnet/models.test.ts +258 -0
- package/test/dotnet/resources.test.ts +387 -0
- package/test/dotnet/tests.test.ts +202 -0
- 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 +135 -0
- package/test/kotlin/resources.test.ts +210 -0
- package/test/kotlin/tests.test.ts +176 -0
- package/test/node/client.test.ts +74 -0
- package/test/node/models.test.ts +134 -1
- package/test/node/resources.test.ts +343 -34
- package/test/node/utils.test.ts +140 -0
- package/test/php/client.test.ts +2 -1
- package/test/php/models.test.ts +5 -4
- package/test/php/resources.test.ts +103 -0
- package/test/php/tests.test.ts +67 -0
- 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/ruby/rbi.ts
ADDED
|
@@ -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
|
+
}
|