@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
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import type { Service, EmitterContext, GeneratedFile, Operation, TypeRef, Parameter, Model } from '@workos/oagen';
|
|
2
|
+
import { planOperation } from '@workos/oagen';
|
|
3
|
+
import { className, fieldName, fileName, methodName, safeParamName, resolveMethodName } from './naming.js';
|
|
4
|
+
import { mapTypeRefForYard } from './type-map.js';
|
|
5
|
+
import {
|
|
6
|
+
buildResolvedLookup,
|
|
7
|
+
lookupResolved,
|
|
8
|
+
groupByMount,
|
|
9
|
+
getOpDefaults,
|
|
10
|
+
getOpInferFromClient,
|
|
11
|
+
buildHiddenParams,
|
|
12
|
+
collectGroupedParamNames,
|
|
13
|
+
} from '../shared/resolved-ops.js';
|
|
14
|
+
import { isListWrapperModel } from '../shared/model-utils.js';
|
|
15
|
+
import { generateWrapperMethods, collectWrapperResponseModels } from './wrappers.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate Ruby resource (service) classes from IR services.
|
|
19
|
+
*
|
|
20
|
+
* Produces one `.rb` file per mount target under `lib/workos/`.
|
|
21
|
+
*/
|
|
22
|
+
export function generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[] {
|
|
23
|
+
const files: GeneratedFile[] = [];
|
|
24
|
+
|
|
25
|
+
const groups = groupByMount(ctx);
|
|
26
|
+
const lookup = buildResolvedLookup(ctx);
|
|
27
|
+
const modelNames = new Set(ctx.spec.models.map((m) => m.name));
|
|
28
|
+
const enumNames = new Set(ctx.spec.enums.map((e) => e.name));
|
|
29
|
+
const modelByName = new Map<string, Model>();
|
|
30
|
+
for (const m of ctx.spec.models as Model[]) modelByName.set(m.name, m);
|
|
31
|
+
|
|
32
|
+
// Build a map of model.name -> isListWrapper to detect pagination.
|
|
33
|
+
const listWrapperModels = new Map<string, Model>();
|
|
34
|
+
for (const m of ctx.spec.models as Model[]) {
|
|
35
|
+
if (isListWrapperModel(m)) listWrapperModels.set(m.name, m);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const [mountTarget, group] of groups) {
|
|
39
|
+
const cls = className(mountTarget);
|
|
40
|
+
const file = fileName(mountTarget);
|
|
41
|
+
|
|
42
|
+
const operations = group.operations;
|
|
43
|
+
if (operations.length === 0) continue;
|
|
44
|
+
|
|
45
|
+
const requires = new Set<string>();
|
|
46
|
+
requires.add('json');
|
|
47
|
+
|
|
48
|
+
const lines: string[] = [];
|
|
49
|
+
const methodBodies: string[] = [];
|
|
50
|
+
|
|
51
|
+
const emittedMethodNames = new Set<string>();
|
|
52
|
+
|
|
53
|
+
// We look for each operation's "home" service within this group.
|
|
54
|
+
for (const op of operations) {
|
|
55
|
+
// Find the service that owns this op (via resolvedOps -> service mapping).
|
|
56
|
+
const ownerService =
|
|
57
|
+
group.resolvedOps.find((r) => r.operation === op)?.service ??
|
|
58
|
+
services.find((s) => s.operations.includes(op)) ??
|
|
59
|
+
services[0];
|
|
60
|
+
const method = resolveMethodName(op, ownerService, ctx);
|
|
61
|
+
if (emittedMethodNames.has(method)) continue;
|
|
62
|
+
|
|
63
|
+
const resolved = lookupResolved(op, lookup);
|
|
64
|
+
// Skip url-builder operations: these are spec-marked client-side URL
|
|
65
|
+
// constructors (no HTTP), and the Ruby SDK provides them via
|
|
66
|
+
// hand-maintained inline @oagen-ignore extensions on the relevant
|
|
67
|
+
// service class instead of generating an HTTP wrapper that would
|
|
68
|
+
// incorrectly hit the API.
|
|
69
|
+
if (resolved?.urlBuilder) {
|
|
70
|
+
emittedMethodNames.add(method);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
emittedMethodNames.add(method);
|
|
75
|
+
|
|
76
|
+
const defaults = getOpDefaults(resolved);
|
|
77
|
+
const inferFromClient = new Set(getOpInferFromClient(resolved));
|
|
78
|
+
const hiddenParams = buildHiddenParams(resolved);
|
|
79
|
+
|
|
80
|
+
const body = emitMethod({
|
|
81
|
+
op,
|
|
82
|
+
method,
|
|
83
|
+
defaults,
|
|
84
|
+
inferFromClient,
|
|
85
|
+
hiddenParams,
|
|
86
|
+
enumNames,
|
|
87
|
+
modelNames,
|
|
88
|
+
modelByName,
|
|
89
|
+
listWrapperModels,
|
|
90
|
+
requires,
|
|
91
|
+
});
|
|
92
|
+
methodBodies.push(body);
|
|
93
|
+
|
|
94
|
+
// Emit union split wrapper methods (e.g., authenticate_with_password).
|
|
95
|
+
if (resolved?.wrappers && resolved.wrappers.length > 0) {
|
|
96
|
+
const wrapperBodies = generateWrapperMethods(resolved, ctx, modelNames, requires);
|
|
97
|
+
for (let i = 0; i < resolved.wrappers.length; i++) {
|
|
98
|
+
const w = resolved.wrappers[i];
|
|
99
|
+
if (emittedMethodNames.has(w.name)) continue;
|
|
100
|
+
emittedMethodNames.add(w.name);
|
|
101
|
+
methodBodies.push(wrapperBodies[i]);
|
|
102
|
+
if (
|
|
103
|
+
w.responseModelName &&
|
|
104
|
+
modelNames.has(w.responseModelName) &&
|
|
105
|
+
!listWrapperModels.has(w.responseModelName)
|
|
106
|
+
) {
|
|
107
|
+
requires.add(fileName(w.responseModelName));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Also ensure any additional response models are added to requires set.
|
|
111
|
+
for (const m of collectWrapperResponseModels(resolved)) {
|
|
112
|
+
if (modelNames.has(m) && !listWrapperModels.has(m)) requires.add(fileName(m));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Zeitwerk autoloads every WorkOS::* constant; only stdlib requires.
|
|
118
|
+
if (requires.has('json')) {
|
|
119
|
+
lines.push(`require 'json'`);
|
|
120
|
+
lines.push('');
|
|
121
|
+
}
|
|
122
|
+
lines.push('module WorkOS');
|
|
123
|
+
lines.push(` class ${cls}`);
|
|
124
|
+
lines.push(' def initialize(client)');
|
|
125
|
+
lines.push(' @client = client');
|
|
126
|
+
lines.push(' end');
|
|
127
|
+
for (const body of methodBodies) {
|
|
128
|
+
lines.push('');
|
|
129
|
+
lines.push(body);
|
|
130
|
+
}
|
|
131
|
+
lines.push(' end');
|
|
132
|
+
lines.push('end');
|
|
133
|
+
|
|
134
|
+
files.push({
|
|
135
|
+
path: `lib/workos/${file}.rb`,
|
|
136
|
+
content: lines.join('\n'),
|
|
137
|
+
integrateTarget: true,
|
|
138
|
+
overwriteExisting: true,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return files;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Build a single Ruby method from an Operation. */
|
|
146
|
+
function emitMethod(args: {
|
|
147
|
+
op: Operation;
|
|
148
|
+
method: string;
|
|
149
|
+
defaults: Record<string, string | number | boolean>;
|
|
150
|
+
inferFromClient: Set<string>;
|
|
151
|
+
hiddenParams: Set<string>;
|
|
152
|
+
enumNames: Set<string>;
|
|
153
|
+
modelNames: Set<string>;
|
|
154
|
+
modelByName: Map<string, Model>;
|
|
155
|
+
listWrapperModels: Map<string, Model>;
|
|
156
|
+
requires: Set<string>;
|
|
157
|
+
}): string {
|
|
158
|
+
const {
|
|
159
|
+
op,
|
|
160
|
+
method,
|
|
161
|
+
defaults,
|
|
162
|
+
inferFromClient,
|
|
163
|
+
hiddenParams,
|
|
164
|
+
enumNames,
|
|
165
|
+
modelNames,
|
|
166
|
+
modelByName,
|
|
167
|
+
listWrapperModels,
|
|
168
|
+
requires,
|
|
169
|
+
} = args;
|
|
170
|
+
void enumNames;
|
|
171
|
+
|
|
172
|
+
const plan = planOperation(op);
|
|
173
|
+
const lines: string[] = [];
|
|
174
|
+
|
|
175
|
+
// Collect params: path params positional, others keyword.
|
|
176
|
+
const pathParams = op.pathParams ?? [];
|
|
177
|
+
const groupedParamNames = collectGroupedParamNames(op);
|
|
178
|
+
const queryParams = (op.queryParams ?? []).filter((q) => !groupedParamNames.has(q.name));
|
|
179
|
+
|
|
180
|
+
// Request body params: if body is a model, expand its fields.
|
|
181
|
+
const bodyFields = getRequestBodyFields(op, hiddenParams, modelByName);
|
|
182
|
+
|
|
183
|
+
// Detect path/body name collisions and build a rename map for body fields.
|
|
184
|
+
// When a body field's snake_case name matches a path param, prefix with "body_"
|
|
185
|
+
// so the Ruby method exposes distinct keyword args.
|
|
186
|
+
const pathParamNames = new Set(pathParams.map((p) => safeParamName(p.name)));
|
|
187
|
+
const bodyFieldRenames = new Map<string, string>();
|
|
188
|
+
for (const f of bodyFields) {
|
|
189
|
+
if (hiddenParams.has(f.name)) continue;
|
|
190
|
+
const n = fieldName(f.name);
|
|
191
|
+
if (pathParamNames.has(n)) {
|
|
192
|
+
bodyFieldRenames.set(f.name, `body_${n}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Resolve the Ruby kwarg name for a body field, applying renames if needed. */
|
|
197
|
+
const bodyKwargName = (wireName: string): string => {
|
|
198
|
+
return bodyFieldRenames.get(wireName) ?? fieldName(wireName);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Method signature. Deduplicate param names across path/body/query.
|
|
202
|
+
const sigParts: string[] = [];
|
|
203
|
+
const seenParamNames = new Set<string>();
|
|
204
|
+
|
|
205
|
+
// Path params: always required, snake_case.
|
|
206
|
+
for (const p of pathParams) {
|
|
207
|
+
const n = safeParamName(p.name);
|
|
208
|
+
if (seenParamNames.has(n)) continue;
|
|
209
|
+
seenParamNames.add(n);
|
|
210
|
+
sigParts.push(`${n}:`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Required body/query params next.
|
|
214
|
+
for (const f of bodyFields) {
|
|
215
|
+
if (hiddenParams.has(f.name)) continue;
|
|
216
|
+
if (!f.required) continue;
|
|
217
|
+
const n = bodyKwargName(f.name);
|
|
218
|
+
if (seenParamNames.has(n)) continue;
|
|
219
|
+
seenParamNames.add(n);
|
|
220
|
+
sigParts.push(`${n}:`);
|
|
221
|
+
}
|
|
222
|
+
for (const q of queryParams) {
|
|
223
|
+
if (hiddenParams.has(q.name)) continue;
|
|
224
|
+
if (!q.required) continue;
|
|
225
|
+
const n = safeParamName(q.name);
|
|
226
|
+
if (seenParamNames.has(n)) continue;
|
|
227
|
+
seenParamNames.add(n);
|
|
228
|
+
sigParts.push(`${n}:`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Required parameter group kwargs.
|
|
232
|
+
for (const group of op.parameterGroups ?? []) {
|
|
233
|
+
if (group.optional) continue;
|
|
234
|
+
const n = fieldName(group.name);
|
|
235
|
+
if (seenParamNames.has(n)) continue;
|
|
236
|
+
seenParamNames.add(n);
|
|
237
|
+
sigParts.push(`${n}:`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Optional body/query params.
|
|
241
|
+
for (const f of bodyFields) {
|
|
242
|
+
if (hiddenParams.has(f.name)) continue;
|
|
243
|
+
if (f.required) continue;
|
|
244
|
+
const n = bodyKwargName(f.name);
|
|
245
|
+
if (seenParamNames.has(n)) continue;
|
|
246
|
+
seenParamNames.add(n);
|
|
247
|
+
sigParts.push(`${n}: nil`);
|
|
248
|
+
}
|
|
249
|
+
for (const q of queryParams) {
|
|
250
|
+
if (hiddenParams.has(q.name)) continue;
|
|
251
|
+
if (q.required) continue;
|
|
252
|
+
const n = safeParamName(q.name);
|
|
253
|
+
if (seenParamNames.has(n)) continue;
|
|
254
|
+
seenParamNames.add(n);
|
|
255
|
+
const defaultVal = q.name === 'order' ? rubyStringLit('desc') : 'nil';
|
|
256
|
+
sigParts.push(`${n}: ${defaultVal}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Optional parameter group kwargs.
|
|
260
|
+
for (const group of op.parameterGroups ?? []) {
|
|
261
|
+
if (!group.optional) continue;
|
|
262
|
+
const n = fieldName(group.name);
|
|
263
|
+
if (seenParamNames.has(n)) continue;
|
|
264
|
+
seenParamNames.add(n);
|
|
265
|
+
sigParts.push(`${n}: nil`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Always accept request_options.
|
|
269
|
+
sigParts.push('request_options: {}');
|
|
270
|
+
|
|
271
|
+
// YARD docs.
|
|
272
|
+
const doc = buildYardDoc(op, pathParams, queryParams, bodyFields, hiddenParams, bodyFieldRenames, listWrapperModels);
|
|
273
|
+
for (const line of doc) lines.push(` ${line}`);
|
|
274
|
+
|
|
275
|
+
// Signature.
|
|
276
|
+
if (sigParts.length === 0) {
|
|
277
|
+
lines.push(` def ${method}`);
|
|
278
|
+
} else if (sigParts.length === 1 && sigParts[0].length < 60) {
|
|
279
|
+
lines.push(` def ${method}(${sigParts[0]})`);
|
|
280
|
+
} else {
|
|
281
|
+
lines.push(` def ${method}(`);
|
|
282
|
+
for (let i = 0; i < sigParts.length; i++) {
|
|
283
|
+
const sep = i === sigParts.length - 1 ? '' : ',';
|
|
284
|
+
lines.push(` ${sigParts[i]}${sep}`);
|
|
285
|
+
}
|
|
286
|
+
lines.push(' )');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Emit deprecation warning for deprecated operations.
|
|
290
|
+
if (op.deprecated) {
|
|
291
|
+
lines.push(` warn "[DEPRECATION] \\\`${method}\\\` is deprecated.", uplevel: 1`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Body: construct params / body / path
|
|
295
|
+
const rubyPath = interpolateRubyPath(op.path, pathParams);
|
|
296
|
+
|
|
297
|
+
// Query params hash.
|
|
298
|
+
// For methods with a request body (POST/PUT/PATCH), exclude query params
|
|
299
|
+
// that also appear as body fields — they belong in the body only.
|
|
300
|
+
const method_http = op.httpMethod.toLowerCase();
|
|
301
|
+
const hasBodyMethod = !['get', 'head', 'delete'].includes(method_http);
|
|
302
|
+
const bodyFieldNameSet = new Set(bodyFields.map((f) => f.name));
|
|
303
|
+
const qEntries = queryParams.filter(
|
|
304
|
+
(q) => !hiddenParams.has(q.name) && !(hasBodyMethod && bodyFieldNameSet.has(q.name)),
|
|
305
|
+
);
|
|
306
|
+
const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
|
|
307
|
+
// Groups go to query only for operations without a request body (GET/DELETE).
|
|
308
|
+
// For POST/PUT/PATCH, groups are dispatched into the body below.
|
|
309
|
+
const groupsGoToQuery = hasGroups && !hasBodyMethod;
|
|
310
|
+
const hasQuery = qEntries.length > 0 || groupsGoToQuery;
|
|
311
|
+
if (hasQuery) {
|
|
312
|
+
lines.push(' params = {');
|
|
313
|
+
for (let i = 0; i < qEntries.length; i++) {
|
|
314
|
+
const q = qEntries[i];
|
|
315
|
+
const sep = i === qEntries.length - 1 && !groupsGoToQuery ? '' : ',';
|
|
316
|
+
lines.push(` ${rubyStringLit(q.name)} => ${safeParamName(q.name)}${sep}`);
|
|
317
|
+
}
|
|
318
|
+
lines.push(' }.compact');
|
|
319
|
+
|
|
320
|
+
if (groupsGoToQuery) {
|
|
321
|
+
// Parameter group dispatch: merge grouped params into the query hash
|
|
322
|
+
for (const group of op.parameterGroups ?? []) {
|
|
323
|
+
const prop = fieldName(group.name);
|
|
324
|
+
if (group.optional) {
|
|
325
|
+
lines.push(` if ${prop}`);
|
|
326
|
+
lines.push(` case ${prop}[:type]`);
|
|
327
|
+
} else {
|
|
328
|
+
lines.push(` case ${prop}[:type]`);
|
|
329
|
+
}
|
|
330
|
+
for (const variant of group.variants) {
|
|
331
|
+
lines.push(` when ${rubyStringLit(variant.name)}`);
|
|
332
|
+
for (const p of variant.parameters) {
|
|
333
|
+
lines.push(` params[${rubyStringLit(p.name)}] = ${prop}[:${fieldName(p.name)}]`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
lines.push(' end');
|
|
337
|
+
if (group.optional) {
|
|
338
|
+
lines.push(' end');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Request body
|
|
345
|
+
const hasBody = bodyFields.length > 0 && !['get', 'head'].includes(method_http);
|
|
346
|
+
|
|
347
|
+
if (hasBody) {
|
|
348
|
+
const bodyEntries: string[] = [];
|
|
349
|
+
for (const [k, v] of Object.entries(defaults)) {
|
|
350
|
+
const lit = typeof v === 'string' ? rubyStringLit(v) : String(v);
|
|
351
|
+
bodyEntries.push(`${rubyStringLit(k)} => ${lit}`);
|
|
352
|
+
}
|
|
353
|
+
for (const fc of inferFromClient) {
|
|
354
|
+
const clientProp = fc === 'client_secret' ? 'api_key' : fc;
|
|
355
|
+
const optKey = fc === 'client_secret' ? 'api_key' : fc;
|
|
356
|
+
bodyEntries.push(`${rubyStringLit(fc)} => (request_options[:${optKey}] || @client.${clientProp})`);
|
|
357
|
+
}
|
|
358
|
+
for (const f of bodyFields) {
|
|
359
|
+
if (hiddenParams.has(f.name)) continue;
|
|
360
|
+
bodyEntries.push(`${rubyStringLit(f.name)} => ${bodyKwargName(f.name)}`);
|
|
361
|
+
}
|
|
362
|
+
lines.push(' body = {');
|
|
363
|
+
for (let i = 0; i < bodyEntries.length; i++) {
|
|
364
|
+
const sep = i === bodyEntries.length - 1 ? '' : ',';
|
|
365
|
+
lines.push(` ${bodyEntries[i]}${sep}`);
|
|
366
|
+
}
|
|
367
|
+
lines.push(' }.compact');
|
|
368
|
+
|
|
369
|
+
// Parameter group dispatch into body for POST/PUT/PATCH so sensitive
|
|
370
|
+
// fields (passwords, role slugs) never leak into the URL query string.
|
|
371
|
+
// DELETE groups are already handled via query above (groupsGoToQuery).
|
|
372
|
+
if (hasGroups && hasBodyMethod) {
|
|
373
|
+
for (const group of op.parameterGroups ?? []) {
|
|
374
|
+
const prop = fieldName(group.name);
|
|
375
|
+
if (group.optional) {
|
|
376
|
+
lines.push(` if ${prop}`);
|
|
377
|
+
lines.push(` case ${prop}[:type]`);
|
|
378
|
+
} else {
|
|
379
|
+
lines.push(` case ${prop}[:type]`);
|
|
380
|
+
}
|
|
381
|
+
for (const variant of group.variants) {
|
|
382
|
+
lines.push(` when ${rubyStringLit(variant.name)}`);
|
|
383
|
+
for (const p of variant.parameters) {
|
|
384
|
+
lines.push(` body[${rubyStringLit(p.name)}] = ${prop}[:${fieldName(p.name)}]`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
lines.push(' end');
|
|
388
|
+
if (group.optional) {
|
|
389
|
+
lines.push(' end');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Make the request via the unified @client.request helper.
|
|
396
|
+
const requestArgs: string[] = [];
|
|
397
|
+
requestArgs.push(`method: :${method_http}`);
|
|
398
|
+
requestArgs.push(`path: ${rubyPath}`);
|
|
399
|
+
requestArgs.push('auth: true');
|
|
400
|
+
if (hasQuery) requestArgs.push('params: params');
|
|
401
|
+
if (hasBody) requestArgs.push('body: body');
|
|
402
|
+
requestArgs.push('request_options: request_options');
|
|
403
|
+
|
|
404
|
+
lines.push(' response = @client.request(');
|
|
405
|
+
for (let i = 0; i < requestArgs.length; i++) {
|
|
406
|
+
const sep = i === requestArgs.length - 1 ? '' : ',';
|
|
407
|
+
lines.push(` ${requestArgs[i]}${sep}`);
|
|
408
|
+
}
|
|
409
|
+
lines.push(' )');
|
|
410
|
+
|
|
411
|
+
// Response handling
|
|
412
|
+
void plan;
|
|
413
|
+
// Build the list of local kwarg names that should be forwarded when the
|
|
414
|
+
// method recursively calls itself for the next page (excluding the cursor
|
|
415
|
+
// param, which is overridden).
|
|
416
|
+
const forwardableParams: string[] = [];
|
|
417
|
+
const bodyNames = new Set(bodyFields.map((f) => bodyKwargName(f.name)));
|
|
418
|
+
for (const p of pathParams) forwardableParams.push(safeParamName(p.name));
|
|
419
|
+
for (const f of bodyFields) {
|
|
420
|
+
if (hiddenParams.has(f.name)) continue;
|
|
421
|
+
forwardableParams.push(bodyKwargName(f.name));
|
|
422
|
+
}
|
|
423
|
+
for (const q of queryParams) {
|
|
424
|
+
if (hiddenParams.has(q.name)) continue;
|
|
425
|
+
const name = safeParamName(q.name);
|
|
426
|
+
if (bodyNames.has(name) || forwardableParams.includes(name)) continue;
|
|
427
|
+
forwardableParams.push(name);
|
|
428
|
+
}
|
|
429
|
+
// Include parameter group kwargs so they are forwarded in pagination fetch_next.
|
|
430
|
+
for (const group of op.parameterGroups ?? []) {
|
|
431
|
+
const name = fieldName(group.name);
|
|
432
|
+
if (!forwardableParams.includes(name)) {
|
|
433
|
+
forwardableParams.push(name);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const responseLines = emitResponseHandling(op, listWrapperModels, modelNames, method, forwardableParams);
|
|
437
|
+
for (const line of responseLines) lines.push(` ${line}`);
|
|
438
|
+
|
|
439
|
+
lines.push(' end');
|
|
440
|
+
|
|
441
|
+
// Ensure we require the response model file if needed.
|
|
442
|
+
// Skip list-wrapper models (they are not emitted as files).
|
|
443
|
+
const respModel = findPrimaryResponseModel(op.response);
|
|
444
|
+
if (respModel && modelNames.has(respModel) && !listWrapperModels.has(respModel)) {
|
|
445
|
+
requires.add(fileName(respModel));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return lines.join('\n');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Build the response parsing expression(s). */
|
|
452
|
+
function emitResponseHandling(
|
|
453
|
+
op: Operation,
|
|
454
|
+
listWrapperModels: Map<string, Model>,
|
|
455
|
+
modelNames: Set<string>,
|
|
456
|
+
currentMethod: string,
|
|
457
|
+
forwardableParams: string[],
|
|
458
|
+
): string[] {
|
|
459
|
+
const ref = op.response;
|
|
460
|
+
|
|
461
|
+
// Build a filters hash from the forwarded params (excluding cursor).
|
|
462
|
+
const filterEntries = forwardableParams
|
|
463
|
+
.filter((p) => p !== 'after' && p !== 'request_options')
|
|
464
|
+
.map((p) => `${p}: ${p}`)
|
|
465
|
+
.join(', ');
|
|
466
|
+
const _filtersArg = filterEntries ? `, filters: { ${filterEntries} }` : '';
|
|
467
|
+
|
|
468
|
+
// Pagination / list wrapper: use ListStruct.from_response with auto-paging wired.
|
|
469
|
+
if (ref.kind === 'model' && listWrapperModels.has(ref.name)) {
|
|
470
|
+
const wrapper = listWrapperModels.get(ref.name)!;
|
|
471
|
+
const dataField = wrapper.fields.find((f) => f.name === 'data');
|
|
472
|
+
const itemCls =
|
|
473
|
+
dataField && dataField.type.kind === 'array' && dataField.type.items.kind === 'model'
|
|
474
|
+
? `WorkOS::${className(dataField.type.items.name)}`
|
|
475
|
+
: null;
|
|
476
|
+
|
|
477
|
+
const cursorLocal = safeParamName('after');
|
|
478
|
+
const hasCursorInSignature = forwardableParams.includes(cursorLocal);
|
|
479
|
+
|
|
480
|
+
const out: string[] = [];
|
|
481
|
+
|
|
482
|
+
if (hasCursorInSignature) {
|
|
483
|
+
// Build a fetch_next lambda that accepts a cursor string and replays
|
|
484
|
+
// the current call with the new cursor.
|
|
485
|
+
out.push(`fetch_next = ->(cursor) {`);
|
|
486
|
+
out.push(` ${currentMethod}(`);
|
|
487
|
+
const allForwards = [...forwardableParams, 'request_options'];
|
|
488
|
+
for (let i = 0; i < allForwards.length; i++) {
|
|
489
|
+
const param = allForwards[i];
|
|
490
|
+
const sep = i === allForwards.length - 1 ? '' : ',';
|
|
491
|
+
const value = param === cursorLocal ? 'cursor' : param;
|
|
492
|
+
out.push(` ${param}: ${value}${sep}`);
|
|
493
|
+
}
|
|
494
|
+
out.push(` )`);
|
|
495
|
+
out.push(`}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const fromArgs: string[] = [];
|
|
499
|
+
fromArgs.push('response');
|
|
500
|
+
if (itemCls) fromArgs.push(`model: ${itemCls}`);
|
|
501
|
+
if (filterEntries) fromArgs.push(`filters: { ${filterEntries} }`);
|
|
502
|
+
if (hasCursorInSignature) fromArgs.push('fetch_next: fetch_next');
|
|
503
|
+
|
|
504
|
+
if (fromArgs.length <= 2) {
|
|
505
|
+
out.push(`WorkOS::Types::ListStruct.from_response(${fromArgs.join(', ')})`);
|
|
506
|
+
} else {
|
|
507
|
+
out.push(`WorkOS::Types::ListStruct.from_response(`);
|
|
508
|
+
for (let i = 0; i < fromArgs.length; i++) {
|
|
509
|
+
const sep = i === fromArgs.length - 1 ? '' : ',';
|
|
510
|
+
out.push(` ${fromArgs[i]}${sep}`);
|
|
511
|
+
}
|
|
512
|
+
out.push(`)`);
|
|
513
|
+
}
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (ref.kind === 'model' && modelNames.has(ref.name)) {
|
|
518
|
+
const cls = `WorkOS::${className(ref.name)}`;
|
|
519
|
+
return [
|
|
520
|
+
`result = ${cls}.new(response.body)`,
|
|
521
|
+
`result.last_response = WorkOS::Types::ApiResponse.new(http_status: response.code.to_i, http_headers: response.each_header.to_h, request_id: response["x-request-id"])`,
|
|
522
|
+
`result`,
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Paginated endpoint whose IR response is typed as array (the IR lost the
|
|
527
|
+
// wrapper envelope). When op.pagination exists, the real HTTP response is
|
|
528
|
+
// { data: [...], list_metadata: {...} } — generate ListStruct handling.
|
|
529
|
+
if (ref.kind === 'array' && op.pagination) {
|
|
530
|
+
const itemCls =
|
|
531
|
+
ref.items.kind === 'model' && modelNames.has(ref.items.name) ? `WorkOS::${className(ref.items.name)}` : null;
|
|
532
|
+
|
|
533
|
+
const cursorLocal = safeParamName('after');
|
|
534
|
+
const hasCursorInSignature = forwardableParams.includes(cursorLocal);
|
|
535
|
+
|
|
536
|
+
const out: string[] = [];
|
|
537
|
+
|
|
538
|
+
if (hasCursorInSignature) {
|
|
539
|
+
out.push(`fetch_next = ->(cursor) {`);
|
|
540
|
+
out.push(` ${currentMethod}(`);
|
|
541
|
+
const allForwards = [...forwardableParams, 'request_options'];
|
|
542
|
+
for (let i = 0; i < allForwards.length; i++) {
|
|
543
|
+
const param = allForwards[i];
|
|
544
|
+
const sep = i === allForwards.length - 1 ? '' : ',';
|
|
545
|
+
const value = param === cursorLocal ? 'cursor' : param;
|
|
546
|
+
out.push(` ${param}: ${value}${sep}`);
|
|
547
|
+
}
|
|
548
|
+
out.push(` )`);
|
|
549
|
+
out.push(`}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const fromArgs: string[] = [];
|
|
553
|
+
fromArgs.push('response');
|
|
554
|
+
if (itemCls) fromArgs.push(`model: ${itemCls}`);
|
|
555
|
+
if (filterEntries) fromArgs.push(`filters: { ${filterEntries} }`);
|
|
556
|
+
if (hasCursorInSignature) fromArgs.push('fetch_next: fetch_next');
|
|
557
|
+
|
|
558
|
+
if (fromArgs.length <= 2) {
|
|
559
|
+
out.push(`WorkOS::Types::ListStruct.from_response(${fromArgs.join(', ')})`);
|
|
560
|
+
} else {
|
|
561
|
+
out.push(`WorkOS::Types::ListStruct.from_response(`);
|
|
562
|
+
for (let i = 0; i < fromArgs.length; i++) {
|
|
563
|
+
const sep = i === fromArgs.length - 1 ? '' : ',';
|
|
564
|
+
out.push(` ${fromArgs[i]}${sep}`);
|
|
565
|
+
}
|
|
566
|
+
out.push(`)`);
|
|
567
|
+
}
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (ref.kind === 'array' && ref.items.kind === 'model' && modelNames.has(ref.items.name)) {
|
|
572
|
+
const itemCls = `WorkOS::${className(ref.items.name)}`;
|
|
573
|
+
return [`parsed = JSON.parse(response.body)`, `(parsed || []).map { |item| ${itemCls}.new(item) }`];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (ref.kind === 'nullable') {
|
|
577
|
+
return emitResponseHandling(
|
|
578
|
+
{ ...op, response: ref.inner },
|
|
579
|
+
listWrapperModels,
|
|
580
|
+
modelNames,
|
|
581
|
+
currentMethod,
|
|
582
|
+
forwardableParams,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Unknown/void response — return nil
|
|
587
|
+
if (ref.kind === 'primitive' && ref.type === 'unknown') {
|
|
588
|
+
return ['nil'];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Default: return parsed JSON
|
|
592
|
+
return [`JSON.parse(response.body)`];
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Find the primary model name (if any) in a response TypeRef. */
|
|
596
|
+
function findPrimaryResponseModel(ref: TypeRef): string | null {
|
|
597
|
+
switch (ref.kind) {
|
|
598
|
+
case 'model':
|
|
599
|
+
return ref.name;
|
|
600
|
+
case 'nullable':
|
|
601
|
+
return findPrimaryResponseModel(ref.inner);
|
|
602
|
+
case 'array':
|
|
603
|
+
return findPrimaryResponseModel(ref.items);
|
|
604
|
+
case 'union': {
|
|
605
|
+
for (const v of ref.variants) {
|
|
606
|
+
const n = findPrimaryResponseModel(v);
|
|
607
|
+
if (n) return n;
|
|
608
|
+
}
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
default:
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Get the body fields, expanded from model refs. Handles nested/model refs and unions. */
|
|
617
|
+
function getRequestBodyFields(
|
|
618
|
+
op: Operation,
|
|
619
|
+
hiddenParams: Set<string>,
|
|
620
|
+
modelByName: Map<string, Model>,
|
|
621
|
+
): { name: string; required: boolean; type: TypeRef; description?: string; deprecated?: boolean }[] {
|
|
622
|
+
void hiddenParams;
|
|
623
|
+
const ref = op.requestBody;
|
|
624
|
+
if (!ref) return [];
|
|
625
|
+
|
|
626
|
+
if (ref.kind === 'model') {
|
|
627
|
+
const model = modelByName.get(ref.name);
|
|
628
|
+
if (!model) return [];
|
|
629
|
+
return model.fields.map((f) => ({
|
|
630
|
+
name: f.name,
|
|
631
|
+
required: f.required,
|
|
632
|
+
type: f.type,
|
|
633
|
+
description: f.description,
|
|
634
|
+
deprecated: f.deprecated,
|
|
635
|
+
}));
|
|
636
|
+
}
|
|
637
|
+
if (ref.kind === 'nullable') {
|
|
638
|
+
return getRequestBodyFields({ ...op, requestBody: ref.inner }, hiddenParams, modelByName);
|
|
639
|
+
}
|
|
640
|
+
// Unions: merge fields from ALL model variants so every possible body param
|
|
641
|
+
// is exposed. Fields that appear in every variant keep their original
|
|
642
|
+
// requiredness; fields that appear in only some variants become optional.
|
|
643
|
+
if (ref.kind === 'union') {
|
|
644
|
+
const variantFieldSets: Map<
|
|
645
|
+
string,
|
|
646
|
+
{ name: string; required: boolean; type: TypeRef; description?: string; deprecated?: boolean }
|
|
647
|
+
>[] = [];
|
|
648
|
+
for (const v of ref.variants) {
|
|
649
|
+
if (v.kind === 'model') {
|
|
650
|
+
const model = modelByName.get(v.name);
|
|
651
|
+
if (model) {
|
|
652
|
+
const fieldMap = new Map<
|
|
653
|
+
string,
|
|
654
|
+
{ name: string; required: boolean; type: TypeRef; description?: string; deprecated?: boolean }
|
|
655
|
+
>();
|
|
656
|
+
for (const f of model.fields) {
|
|
657
|
+
fieldMap.set(f.name, {
|
|
658
|
+
name: f.name,
|
|
659
|
+
required: f.required,
|
|
660
|
+
type: f.type,
|
|
661
|
+
description: f.description,
|
|
662
|
+
deprecated: f.deprecated,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
variantFieldSets.push(fieldMap);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (variantFieldSets.length === 0) return [];
|
|
670
|
+
|
|
671
|
+
// Collect all field names in order (preserving first-seen order).
|
|
672
|
+
const allFieldNames: string[] = [];
|
|
673
|
+
const seen = new Set<string>();
|
|
674
|
+
for (const fieldMap of variantFieldSets) {
|
|
675
|
+
for (const name of fieldMap.keys()) {
|
|
676
|
+
if (!seen.has(name)) {
|
|
677
|
+
seen.add(name);
|
|
678
|
+
allFieldNames.push(name);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return allFieldNames.map((name) => {
|
|
684
|
+
// Use the first variant that has this field as the canonical source.
|
|
685
|
+
const canonical = variantFieldSets.find((fm) => fm.has(name))!.get(name)!;
|
|
686
|
+
// A field is required only if it appears and is required in EVERY variant.
|
|
687
|
+
const requiredInAll = variantFieldSets.every((fm) => {
|
|
688
|
+
const f = fm.get(name);
|
|
689
|
+
return f && f.required;
|
|
690
|
+
});
|
|
691
|
+
return { ...canonical, required: requiredInAll };
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Convert an OpenAPI path like /orgs/{id}/users/{uid} to a Ruby interpolated string. */
|
|
698
|
+
function interpolateRubyPath(path: string, pathParams: Parameter[]): string {
|
|
699
|
+
if (pathParams.length === 0) {
|
|
700
|
+
return `'${path}'`;
|
|
701
|
+
}
|
|
702
|
+
let result = path;
|
|
703
|
+
for (const p of pathParams) {
|
|
704
|
+
const placeholder = `{${p.name}}`;
|
|
705
|
+
result = result.split(placeholder).join(`#{WorkOS::Util.encode_path(${safeParamName(p.name)})}`);
|
|
706
|
+
}
|
|
707
|
+
return `"${result}"`;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Collapse multi-line description text into a single YARD-safe line. */
|
|
711
|
+
function oneLine(desc: string | undefined): string {
|
|
712
|
+
if (!desc) return '';
|
|
713
|
+
return desc.replace(/\r/g, ' ').replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function buildYardDoc(
|
|
717
|
+
op: Operation,
|
|
718
|
+
pathParams: Parameter[],
|
|
719
|
+
queryParams: Parameter[],
|
|
720
|
+
bodyFields: { name: string; required: boolean; type: TypeRef; description?: string; deprecated?: boolean }[],
|
|
721
|
+
hiddenParams: Set<string>,
|
|
722
|
+
bodyFieldRenames?: Map<string, string>,
|
|
723
|
+
listWrapperModels?: Map<string, Model>,
|
|
724
|
+
): string[] {
|
|
725
|
+
const lines: string[] = [];
|
|
726
|
+
const summary = op.description ?? `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
727
|
+
const firstLine = summary.split('\n')[0] ?? '';
|
|
728
|
+
lines.push(`# ${firstLine}`);
|
|
729
|
+
if (op.deprecated) {
|
|
730
|
+
lines.push('# @deprecated');
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Track emitted param names to avoid duplicates (e.g. code in both body and query).
|
|
734
|
+
const emittedParamNames = new Set<string>();
|
|
735
|
+
|
|
736
|
+
for (const p of pathParams) {
|
|
737
|
+
const n = safeParamName(p.name);
|
|
738
|
+
if (emittedParamNames.has(n)) continue;
|
|
739
|
+
emittedParamNames.add(n);
|
|
740
|
+
const type = mapTypeRefForYard(p.type);
|
|
741
|
+
const deprecatedPrefix = p.deprecated ? '(deprecated) ' : '';
|
|
742
|
+
lines.push(`# @param ${n} [${type}] ${deprecatedPrefix}${oneLine(p.description)}`.trim());
|
|
743
|
+
}
|
|
744
|
+
for (const f of bodyFields) {
|
|
745
|
+
if (hiddenParams.has(f.name)) continue;
|
|
746
|
+
const paramName = bodyFieldRenames?.get(f.name) ?? fieldName(f.name);
|
|
747
|
+
if (emittedParamNames.has(paramName)) continue;
|
|
748
|
+
emittedParamNames.add(paramName);
|
|
749
|
+
const type = mapTypeRefForYard(f.type);
|
|
750
|
+
// Only append nil suffix for optional params whose type doesn't already include nil.
|
|
751
|
+
const alreadyNilable = type.split(', ').includes('nil');
|
|
752
|
+
const suffix = f.required || alreadyNilable ? '' : ', nil';
|
|
753
|
+
const deprecatedPrefix = f.deprecated ? '(deprecated) ' : '';
|
|
754
|
+
lines.push(`# @param ${paramName} [${type}${suffix}] ${deprecatedPrefix}${oneLine(f.description)}`.trim());
|
|
755
|
+
}
|
|
756
|
+
for (const q of queryParams) {
|
|
757
|
+
if (hiddenParams.has(q.name)) continue;
|
|
758
|
+
const n = safeParamName(q.name);
|
|
759
|
+
if (emittedParamNames.has(n)) continue;
|
|
760
|
+
emittedParamNames.add(n);
|
|
761
|
+
const type = mapTypeRefForYard(q.type);
|
|
762
|
+
const alreadyNilable = type.split(', ').includes('nil');
|
|
763
|
+
const suffix = q.required || alreadyNilable ? '' : ', nil';
|
|
764
|
+
const deprecatedPrefix = q.deprecated ? '(deprecated) ' : '';
|
|
765
|
+
lines.push(`# @param ${n} [${type}${suffix}] ${deprecatedPrefix}${oneLine(q.description)}`.trim());
|
|
766
|
+
}
|
|
767
|
+
lines.push(`# @param request_options [Hash] (see WorkOS::Types::RequestOptions)`);
|
|
768
|
+
|
|
769
|
+
// Return type: void for unknown-primitive, ListStruct for list wrappers and
|
|
770
|
+
// paginated array endpoints (with element type annotation).
|
|
771
|
+
const ref = op.response;
|
|
772
|
+
if (ref.kind === 'primitive' && ref.type === 'unknown') {
|
|
773
|
+
lines.push(`# @return [void]`);
|
|
774
|
+
} else if (ref.kind === 'model' && listWrapperModels?.has(ref.name)) {
|
|
775
|
+
const wrapper = listWrapperModels.get(ref.name)!;
|
|
776
|
+
const dataField = wrapper.fields.find((f: { name: string; type: TypeRef }) => f.name === 'data');
|
|
777
|
+
const elementType =
|
|
778
|
+
dataField && dataField.type.kind === 'array' && dataField.type.items.kind === 'model'
|
|
779
|
+
? `WorkOS::${className(dataField.type.items.name)}`
|
|
780
|
+
: null;
|
|
781
|
+
const suffix = elementType ? `<${elementType}>` : '';
|
|
782
|
+
lines.push(`# @return [WorkOS::Types::ListStruct${suffix}]`);
|
|
783
|
+
} else if (ref.kind === 'array' && op.pagination) {
|
|
784
|
+
const elementType = ref.items.kind === 'model' ? `WorkOS::${className(ref.items.name)}` : null;
|
|
785
|
+
const suffix = elementType ? `<${elementType}>` : '';
|
|
786
|
+
lines.push(`# @return [WorkOS::Types::ListStruct${suffix}]`);
|
|
787
|
+
} else {
|
|
788
|
+
const retType = mapTypeRefForYard(ref);
|
|
789
|
+
lines.push(`# @return [${retType}]`);
|
|
790
|
+
}
|
|
791
|
+
return lines;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
void methodName;
|
|
795
|
+
|
|
796
|
+
/** Render a Ruby single-quoted string literal, escaping embedded quotes and backslashes. */
|
|
797
|
+
function rubyStringLit(s: string): string {
|
|
798
|
+
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
799
|
+
}
|