@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/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
|
+
}
|