@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,386 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { EmitterContext, ApiSpec, Service, Operation, Model, ResolvedOperation } from '@workos/oagen';
|
|
3
|
+
import { defaultSdkBehavior, toSnakeCase, toPascalCase } from '@workos/oagen';
|
|
4
|
+
import { generateResources } from '../../src/ruby/resources.js';
|
|
5
|
+
|
|
6
|
+
function makeSpec(services: Service[], models: Model[] = []): ApiSpec {
|
|
7
|
+
return {
|
|
8
|
+
name: 'Test',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
baseUrl: '',
|
|
11
|
+
services,
|
|
12
|
+
models,
|
|
13
|
+
enums: [],
|
|
14
|
+
sdk: defaultSdkBehavior(),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Build resolvedOperations from services so groupByMount works. */
|
|
19
|
+
function buildResolvedOps(services: Service[]): ResolvedOperation[] {
|
|
20
|
+
const ops: ResolvedOperation[] = [];
|
|
21
|
+
for (const service of services) {
|
|
22
|
+
const mountOn = toPascalCase(service.name);
|
|
23
|
+
for (const op of service.operations) {
|
|
24
|
+
ops.push({
|
|
25
|
+
operation: op,
|
|
26
|
+
service,
|
|
27
|
+
methodName: toSnakeCase(op.name),
|
|
28
|
+
mountOn,
|
|
29
|
+
defaults: {},
|
|
30
|
+
inferFromClient: [],
|
|
31
|
+
urlBuilder: false,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return ops;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeCtx(spec: ApiSpec): EmitterContext {
|
|
39
|
+
return {
|
|
40
|
+
namespace: 'workos',
|
|
41
|
+
namespacePascal: 'WorkOS',
|
|
42
|
+
spec,
|
|
43
|
+
resolvedOperations: buildResolvedOps(spec.services),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeOp(overrides: Partial<Operation>): Operation {
|
|
48
|
+
return {
|
|
49
|
+
name: 'listOrganizations',
|
|
50
|
+
httpMethod: 'get',
|
|
51
|
+
path: '/organizations',
|
|
52
|
+
pathParams: [],
|
|
53
|
+
queryParams: [],
|
|
54
|
+
headerParams: [],
|
|
55
|
+
requestBody: undefined,
|
|
56
|
+
response: { kind: 'model', name: 'Organization' },
|
|
57
|
+
errors: [],
|
|
58
|
+
injectIdempotencyKey: false,
|
|
59
|
+
...overrides,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('ruby/resources', () => {
|
|
64
|
+
it('returns empty for no services', () => {
|
|
65
|
+
const spec = makeSpec([]);
|
|
66
|
+
expect(generateResources([], makeCtx(spec))).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── P0-1: request_options forwarding ────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
it('forwards request_options to the unified request helper', () => {
|
|
72
|
+
const services: Service[] = [
|
|
73
|
+
{
|
|
74
|
+
name: 'Organizations',
|
|
75
|
+
operations: [
|
|
76
|
+
makeOp({
|
|
77
|
+
name: 'getOrganization',
|
|
78
|
+
httpMethod: 'get',
|
|
79
|
+
path: '/organizations/{id}',
|
|
80
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
const spec = makeSpec(services);
|
|
86
|
+
const files = generateResources(services, makeCtx(spec));
|
|
87
|
+
const content = files[0].content;
|
|
88
|
+
|
|
89
|
+
// Uses the unified @client.request helper
|
|
90
|
+
expect(content).toContain('@client.request(');
|
|
91
|
+
expect(content).toContain('method: :get');
|
|
92
|
+
expect(content).toContain('request_options: request_options');
|
|
93
|
+
// Should NOT use the two-layer execute_request(X_request(...)) pattern
|
|
94
|
+
expect(content).not.toContain('execute_request(');
|
|
95
|
+
expect(content).not.toContain('get_request(');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('forwards request_options in POST methods', () => {
|
|
99
|
+
const services: Service[] = [
|
|
100
|
+
{
|
|
101
|
+
name: 'Organizations',
|
|
102
|
+
operations: [
|
|
103
|
+
makeOp({
|
|
104
|
+
name: 'createOrganization',
|
|
105
|
+
httpMethod: 'post',
|
|
106
|
+
path: '/organizations',
|
|
107
|
+
requestBody: { kind: 'model', name: 'CreateOrganizationRequest' },
|
|
108
|
+
}),
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
const spec = makeSpec(services, [
|
|
113
|
+
{
|
|
114
|
+
name: 'CreateOrganizationRequest',
|
|
115
|
+
fields: [{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
116
|
+
},
|
|
117
|
+
]);
|
|
118
|
+
const files = generateResources(services, makeCtx(spec));
|
|
119
|
+
const content = files[0].content;
|
|
120
|
+
|
|
121
|
+
expect(content).toContain('@client.request(');
|
|
122
|
+
expect(content).toContain('method: :post');
|
|
123
|
+
expect(content).toContain('body: body');
|
|
124
|
+
expect(content).toContain('request_options: request_options');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ── P0-2: pagination cursor direction ──────────────────────────────────
|
|
128
|
+
|
|
129
|
+
it('uses after cursor for fetch_next lambda (not before)', () => {
|
|
130
|
+
const listModel: Model = {
|
|
131
|
+
name: 'OrganizationList',
|
|
132
|
+
fields: [
|
|
133
|
+
{
|
|
134
|
+
name: 'data',
|
|
135
|
+
type: { kind: 'array', items: { kind: 'model', name: 'Organization' } },
|
|
136
|
+
required: true,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'list_metadata',
|
|
140
|
+
type: { kind: 'model', name: 'ListMetadata' },
|
|
141
|
+
required: true,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
const services: Service[] = [
|
|
146
|
+
{
|
|
147
|
+
name: 'Organizations',
|
|
148
|
+
operations: [
|
|
149
|
+
makeOp({
|
|
150
|
+
name: 'listOrganizations',
|
|
151
|
+
httpMethod: 'get',
|
|
152
|
+
path: '/organizations',
|
|
153
|
+
queryParams: [
|
|
154
|
+
{ name: 'before', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
155
|
+
{ name: 'after', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
156
|
+
{ name: 'limit', type: { kind: 'primitive', type: 'integer' }, required: false },
|
|
157
|
+
],
|
|
158
|
+
response: { kind: 'model', name: 'OrganizationList' },
|
|
159
|
+
pagination: {
|
|
160
|
+
strategy: 'cursor',
|
|
161
|
+
param: 'before',
|
|
162
|
+
dataPath: 'data',
|
|
163
|
+
itemType: { kind: 'model', name: 'Organization' },
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
const spec = makeSpec(services, [
|
|
170
|
+
listModel,
|
|
171
|
+
{
|
|
172
|
+
name: 'ListMetadata',
|
|
173
|
+
fields: [
|
|
174
|
+
{ name: 'before', type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } }, required: false },
|
|
175
|
+
{ name: 'after', type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } }, required: false },
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
]);
|
|
179
|
+
const files = generateResources(services, makeCtx(spec));
|
|
180
|
+
const content = files[0].content;
|
|
181
|
+
|
|
182
|
+
// fetch_next lambda receives cursor string; the recursive call passes after: cursor
|
|
183
|
+
expect(content).toContain('after: cursor');
|
|
184
|
+
// Must NOT pass before: cursor in the recursive call
|
|
185
|
+
expect(content).not.toMatch(/before: cursor/);
|
|
186
|
+
// Should use ListStruct.from_response
|
|
187
|
+
expect(content).toContain('ListStruct.from_response(');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── P0-3: paginated response shape detection ──────────────────────────
|
|
191
|
+
|
|
192
|
+
it('generates ListStruct for paginated endpoints with array response type', () => {
|
|
193
|
+
const services: Service[] = [
|
|
194
|
+
{
|
|
195
|
+
name: 'UserManagement',
|
|
196
|
+
operations: [
|
|
197
|
+
makeOp({
|
|
198
|
+
name: 'listSessions',
|
|
199
|
+
httpMethod: 'get',
|
|
200
|
+
path: '/user_management/users/{id}/sessions',
|
|
201
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
202
|
+
queryParams: [
|
|
203
|
+
{ name: 'before', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
204
|
+
{ name: 'after', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
205
|
+
{ name: 'limit', type: { kind: 'primitive', type: 'integer' }, required: false },
|
|
206
|
+
],
|
|
207
|
+
// Response is typed as array in IR, but endpoint is actually paginated
|
|
208
|
+
response: { kind: 'array', items: { kind: 'model', name: 'Session' } },
|
|
209
|
+
pagination: {
|
|
210
|
+
strategy: 'cursor',
|
|
211
|
+
param: 'after',
|
|
212
|
+
dataPath: 'data',
|
|
213
|
+
itemType: { kind: 'model', name: 'Session' },
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
const spec = makeSpec(services);
|
|
220
|
+
const files = generateResources(services, makeCtx(spec));
|
|
221
|
+
const content = files[0].content;
|
|
222
|
+
|
|
223
|
+
// Should generate ListStruct.from_response, not bare array mapping
|
|
224
|
+
expect(content).toContain('ListStruct.from_response(');
|
|
225
|
+
// Should NOT be treating response as bare array
|
|
226
|
+
expect(content).not.toContain('(parsed || []).map');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('preserves bare array handling for non-paginated array endpoints', () => {
|
|
230
|
+
const services: Service[] = [
|
|
231
|
+
{
|
|
232
|
+
name: 'UserManagement',
|
|
233
|
+
operations: [
|
|
234
|
+
makeOp({
|
|
235
|
+
name: 'getUserIdentities',
|
|
236
|
+
httpMethod: 'get',
|
|
237
|
+
path: '/user_management/users/{id}/identities',
|
|
238
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
239
|
+
// Array response, no pagination
|
|
240
|
+
response: { kind: 'array', items: { kind: 'model', name: 'Identity' } },
|
|
241
|
+
}),
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
const spec = makeSpec(services, [
|
|
246
|
+
{ name: 'Identity', fields: [{ name: 'type', type: { kind: 'primitive', type: 'string' }, required: true }] },
|
|
247
|
+
]);
|
|
248
|
+
const files = generateResources(services, makeCtx(spec));
|
|
249
|
+
const content = files[0].content;
|
|
250
|
+
|
|
251
|
+
// Should be bare array mapping (no pagination)
|
|
252
|
+
expect(content).toContain('(parsed || []).map');
|
|
253
|
+
expect(content).not.toContain('ListStruct');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ── P0-4: DELETE with body ─────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
it('generates body for DELETE endpoints with requestBody', () => {
|
|
259
|
+
const services: Service[] = [
|
|
260
|
+
{
|
|
261
|
+
name: 'Authorization',
|
|
262
|
+
operations: [
|
|
263
|
+
makeOp({
|
|
264
|
+
name: 'removeRole',
|
|
265
|
+
httpMethod: 'delete',
|
|
266
|
+
path: '/authorization/memberships/{id}/role_assignments',
|
|
267
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
268
|
+
requestBody: { kind: 'model', name: 'RemoveRoleRequest' },
|
|
269
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
270
|
+
}),
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
const spec = makeSpec(services, [
|
|
275
|
+
{
|
|
276
|
+
name: 'RemoveRoleRequest',
|
|
277
|
+
fields: [
|
|
278
|
+
{ name: 'role_slug', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
279
|
+
{ name: 'resource_id', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
]);
|
|
283
|
+
const files = generateResources(services, makeCtx(spec));
|
|
284
|
+
const content = files[0].content;
|
|
285
|
+
|
|
286
|
+
// Should construct a body hash
|
|
287
|
+
expect(content).toContain('body = {');
|
|
288
|
+
expect(content).toContain("'role_slug' => role_slug");
|
|
289
|
+
// Should pass body to the request helper
|
|
290
|
+
expect(content).toContain('@client.request(');
|
|
291
|
+
expect(content).toContain('body: body');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('does not generate body for DELETE without requestBody', () => {
|
|
295
|
+
const services: Service[] = [
|
|
296
|
+
{
|
|
297
|
+
name: 'Items',
|
|
298
|
+
operations: [
|
|
299
|
+
makeOp({
|
|
300
|
+
name: 'deleteItem',
|
|
301
|
+
httpMethod: 'delete',
|
|
302
|
+
path: '/items/{id}',
|
|
303
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
304
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
305
|
+
}),
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
const spec = makeSpec(services);
|
|
310
|
+
const files = generateResources(services, makeCtx(spec));
|
|
311
|
+
const content = files[0].content;
|
|
312
|
+
|
|
313
|
+
// No body construction
|
|
314
|
+
expect(content).not.toContain('body = {');
|
|
315
|
+
expect(content).toContain('method: :delete');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ── P0-5: path/body parameter name collision ───────────────────────────
|
|
319
|
+
|
|
320
|
+
it('disambiguates colliding path and body parameter names', () => {
|
|
321
|
+
const services: Service[] = [
|
|
322
|
+
{
|
|
323
|
+
name: 'Authorization',
|
|
324
|
+
operations: [
|
|
325
|
+
makeOp({
|
|
326
|
+
name: 'createRolePermission',
|
|
327
|
+
httpMethod: 'post',
|
|
328
|
+
path: '/authorization/roles/{slug}/permissions',
|
|
329
|
+
pathParams: [{ name: 'slug', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
330
|
+
requestBody: { kind: 'model', name: 'CreateRolePermissionRequest' },
|
|
331
|
+
}),
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
const spec = makeSpec(services, [
|
|
336
|
+
{
|
|
337
|
+
name: 'CreateRolePermissionRequest',
|
|
338
|
+
fields: [{ name: 'slug', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
339
|
+
},
|
|
340
|
+
]);
|
|
341
|
+
const files = generateResources(services, makeCtx(spec));
|
|
342
|
+
const content = files[0].content;
|
|
343
|
+
|
|
344
|
+
// Should have both slug: (path) and body_slug: (body) in signature
|
|
345
|
+
expect(content).toContain('slug:');
|
|
346
|
+
expect(content).toContain('body_slug:');
|
|
347
|
+
|
|
348
|
+
// Path interpolation uses slug (the path param) with Util.encode_path
|
|
349
|
+
expect(content).toContain('WorkOS::Util.encode_path(slug)');
|
|
350
|
+
|
|
351
|
+
// Body hash uses body_slug for the wire name "slug"
|
|
352
|
+
expect(content).toContain("'slug' => body_slug");
|
|
353
|
+
|
|
354
|
+
// YARD doc should mention both params
|
|
355
|
+
expect(content).toContain('@param slug');
|
|
356
|
+
expect(content).toContain('@param body_slug');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('does not rename body fields that do not collide with path params', () => {
|
|
360
|
+
const services: Service[] = [
|
|
361
|
+
{
|
|
362
|
+
name: 'Organizations',
|
|
363
|
+
operations: [
|
|
364
|
+
makeOp({
|
|
365
|
+
name: 'createOrganization',
|
|
366
|
+
httpMethod: 'post',
|
|
367
|
+
path: '/organizations',
|
|
368
|
+
requestBody: { kind: 'model', name: 'CreateOrganizationRequest' },
|
|
369
|
+
}),
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
];
|
|
373
|
+
const spec = makeSpec(services, [
|
|
374
|
+
{
|
|
375
|
+
name: 'CreateOrganizationRequest',
|
|
376
|
+
fields: [{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
377
|
+
},
|
|
378
|
+
]);
|
|
379
|
+
const files = generateResources(services, makeCtx(spec));
|
|
380
|
+
const content = files[0].content;
|
|
381
|
+
|
|
382
|
+
// name: should be used directly (no body_ prefix)
|
|
383
|
+
expect(content).toContain('name:');
|
|
384
|
+
expect(content).not.toContain('body_name');
|
|
385
|
+
});
|
|
386
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { EmitterContext, ResolvedOperation, Model, Operation } from '@workos/oagen';
|
|
3
|
+
import {
|
|
4
|
+
assertUniqueResolvedMethods,
|
|
5
|
+
buildResolvedLookup,
|
|
6
|
+
collectBodyFieldTypes,
|
|
7
|
+
} from '../../src/shared/resolved-ops.js';
|
|
8
|
+
|
|
9
|
+
function makeResolvedOperation(
|
|
10
|
+
httpMethod: string,
|
|
11
|
+
path: string,
|
|
12
|
+
methodName: string,
|
|
13
|
+
mountOn = 'Authorization',
|
|
14
|
+
): ResolvedOperation {
|
|
15
|
+
return {
|
|
16
|
+
operation: {
|
|
17
|
+
name: methodName,
|
|
18
|
+
httpMethod: httpMethod as any,
|
|
19
|
+
path,
|
|
20
|
+
pathParams: [],
|
|
21
|
+
queryParams: [],
|
|
22
|
+
headerParams: [],
|
|
23
|
+
response: { kind: 'primitive', type: 'string' },
|
|
24
|
+
errors: [],
|
|
25
|
+
injectIdempotencyKey: false,
|
|
26
|
+
},
|
|
27
|
+
service: {
|
|
28
|
+
name: mountOn,
|
|
29
|
+
operations: [],
|
|
30
|
+
},
|
|
31
|
+
methodName,
|
|
32
|
+
mountOn,
|
|
33
|
+
defaults: {},
|
|
34
|
+
inferFromClient: [],
|
|
35
|
+
urlBuilder: false,
|
|
36
|
+
} as unknown as ResolvedOperation;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeCtx(resolvedOperations: ResolvedOperation[]): EmitterContext {
|
|
40
|
+
return {
|
|
41
|
+
namespace: 'workos',
|
|
42
|
+
namespacePascal: 'WorkOS',
|
|
43
|
+
spec: {
|
|
44
|
+
name: 'Test',
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
baseUrl: 'https://api.example.com',
|
|
47
|
+
services: [],
|
|
48
|
+
models: [],
|
|
49
|
+
enums: [],
|
|
50
|
+
sdk: {} as any,
|
|
51
|
+
},
|
|
52
|
+
resolvedOperations,
|
|
53
|
+
} as EmitterContext;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('shared/resolved-ops', () => {
|
|
57
|
+
it('allows duplicate method names when they target the same path', () => {
|
|
58
|
+
const ctx = makeCtx([
|
|
59
|
+
makeResolvedOperation('put', '/organizations/{id}', 'update_organization'),
|
|
60
|
+
makeResolvedOperation('patch', '/organizations/{id}', 'update_organization'),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
expect(() => assertUniqueResolvedMethods(ctx)).not.toThrow();
|
|
64
|
+
expect(buildResolvedLookup(ctx).size).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('throws when two different paths in the same mount resolve to the same method', () => {
|
|
68
|
+
const ctx = makeCtx([
|
|
69
|
+
makeResolvedOperation(
|
|
70
|
+
'get',
|
|
71
|
+
'/authorization/organization_memberships/{organization_membership_id}/resources/{resource_id}/permissions',
|
|
72
|
+
'list_resource_permissions',
|
|
73
|
+
),
|
|
74
|
+
makeResolvedOperation(
|
|
75
|
+
'get',
|
|
76
|
+
'/authorization/organization_memberships/{organization_membership_id}/resources/{resource_type_slug}/{external_id}/permissions',
|
|
77
|
+
'list_resource_permissions',
|
|
78
|
+
),
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
expect(() => assertUniqueResolvedMethods(ctx)).toThrow(
|
|
82
|
+
/Resolved operation name collision for Authorization\.list_resource_permissions/,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('collects body field types for grouped body params', () => {
|
|
87
|
+
const op: Operation = {
|
|
88
|
+
name: 'createOrganizationMembership',
|
|
89
|
+
httpMethod: 'post',
|
|
90
|
+
path: '/user_management/organization_memberships',
|
|
91
|
+
pathParams: [],
|
|
92
|
+
queryParams: [],
|
|
93
|
+
headerParams: [],
|
|
94
|
+
requestBody: { kind: 'model', name: 'CreateOrganizationMembershipRequest' },
|
|
95
|
+
response: { kind: 'primitive', type: 'string' },
|
|
96
|
+
errors: [],
|
|
97
|
+
injectIdempotencyKey: false,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const models: Model[] = [
|
|
101
|
+
{
|
|
102
|
+
name: 'CreateOrganizationMembershipRequest',
|
|
103
|
+
fields: [
|
|
104
|
+
{ name: 'role_slug', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
105
|
+
{
|
|
106
|
+
name: 'role_slugs',
|
|
107
|
+
type: { kind: 'array', items: { kind: 'primitive', type: 'string' } },
|
|
108
|
+
required: false,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const fieldTypes = collectBodyFieldTypes(op, models);
|
|
115
|
+
|
|
116
|
+
expect(fieldTypes.get('role_slug')).toEqual({ kind: 'primitive', type: 'string' });
|
|
117
|
+
expect(fieldTypes.get('role_slugs')).toEqual({
|
|
118
|
+
kind: 'array',
|
|
119
|
+
items: { kind: 'primitive', type: 'string' },
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|