@workos/oagen-emitters 0.15.2 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +48 -1
  4. package/dist/index.d.mts +51 -2
  5. package/dist/index.d.mts.map +1 -1
  6. package/dist/index.mjs +852 -2
  7. package/dist/index.mjs.map +1 -0
  8. package/dist/{plugin-Xkr83G9A.mjs → plugin-CpO8rePT.mjs} +1219 -493
  9. package/dist/plugin-CpO8rePT.mjs.map +1 -0
  10. package/dist/plugin.mjs +1 -1
  11. package/package.json +7 -7
  12. package/src/dotnet/naming.ts +1 -1
  13. package/src/go/naming.ts +1 -1
  14. package/src/index.ts +15 -0
  15. package/src/node/enums.ts +17 -4
  16. package/src/node/index.ts +264 -4
  17. package/src/node/live-surface.ts +309 -0
  18. package/src/node/models.ts +69 -3
  19. package/src/node/naming.ts +204 -23
  20. package/src/node/resources.ts +39 -3
  21. package/src/node/tests.ts +29 -3
  22. package/src/node/utils.ts +140 -22
  23. package/src/snippets/dotnet.ts +159 -0
  24. package/src/snippets/go.ts +148 -0
  25. package/src/snippets/index.ts +8 -0
  26. package/src/snippets/kotlin.ts +144 -0
  27. package/src/snippets/php.ts +149 -0
  28. package/src/snippets/plugin.ts +36 -0
  29. package/src/snippets/python.ts +135 -0
  30. package/src/snippets/ruby.ts +152 -0
  31. package/src/snippets/rust.ts +189 -0
  32. package/test/node/enums.test.ts +239 -2
  33. package/test/node/live-surface.test.ts +771 -1
  34. package/test/node/models.test.ts +738 -3
  35. package/test/node/naming.test.ts +159 -0
  36. package/test/node/resources.test.ts +464 -0
  37. package/test/node/utils.test.ts +157 -2
  38. package/test/snippets/_helpers.ts +67 -0
  39. package/test/snippets/dotnet.test.ts +49 -0
  40. package/test/snippets/go.test.ts +94 -0
  41. package/test/snippets/kotlin.test.ts +53 -0
  42. package/test/snippets/php.test.ts +48 -0
  43. package/test/snippets/python.test.ts +73 -0
  44. package/test/snippets/ruby.test.ts +339 -0
  45. package/test/snippets/rust.test.ts +76 -0
  46. package/dist/plugin-Xkr83G9A.mjs.map +0 -1
@@ -0,0 +1,339 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { ApiSpec, EmitterContext, Model, Operation, ResolvedOperation, Service } from '@workos/oagen';
3
+ import { defaultSdkBehavior, runSnippetEmitters, toPascalCase, toSnakeCase } from '@workos/oagen';
4
+ import { rubySnippetEmitter } from '../../src/snippets/ruby.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
+ function buildResolvedOps(services: Service[]): ResolvedOperation[] {
19
+ const ops: ResolvedOperation[] = [];
20
+ for (const service of services) {
21
+ const mountOn = toPascalCase(service.name);
22
+ for (const op of service.operations) {
23
+ ops.push({
24
+ operation: op,
25
+ service,
26
+ methodName: toSnakeCase(op.name),
27
+ mountOn,
28
+ defaults: {},
29
+ inferFromClient: [],
30
+ urlBuilder: false,
31
+ });
32
+ }
33
+ }
34
+ return ops;
35
+ }
36
+
37
+ function makeCtx(spec: ApiSpec): EmitterContext {
38
+ return {
39
+ namespace: 'workos',
40
+ namespacePascal: 'WorkOS',
41
+ spec,
42
+ resolvedOperations: buildResolvedOps(spec.services),
43
+ };
44
+ }
45
+
46
+ function makeOp(overrides: Partial<Operation>): Operation {
47
+ return {
48
+ name: 'listOrganizations',
49
+ httpMethod: 'get',
50
+ path: '/organizations',
51
+ pathParams: [],
52
+ queryParams: [],
53
+ headerParams: [],
54
+ requestBody: undefined,
55
+ response: { kind: 'model', name: 'Organization' },
56
+ errors: [],
57
+ injectIdempotencyKey: false,
58
+ ...overrides,
59
+ };
60
+ }
61
+
62
+ function runRuby(spec: ApiSpec): string[] {
63
+ const results = runSnippetEmitters([rubySnippetEmitter], makeCtx(spec));
64
+ return results.map((r) => r.content);
65
+ }
66
+
67
+ describe('snippets/ruby', () => {
68
+ it('returns no snippets for an empty spec', () => {
69
+ expect(runRuby(makeSpec([]))).toEqual([]);
70
+ });
71
+
72
+ it('renders a no-arg list call', () => {
73
+ const services: Service[] = [
74
+ {
75
+ name: 'Organizations',
76
+ operations: [makeOp({ name: 'list_organizations' })],
77
+ },
78
+ ];
79
+ const [content] = runRuby(makeSpec(services));
80
+ expect(content).toBe(
81
+ [
82
+ 'require "workos"',
83
+ '',
84
+ 'WorkOS.configure do |config|',
85
+ ' config.api_key = "sk_example_123456789"',
86
+ 'end',
87
+ '',
88
+ 'WorkOS.client.organizations.list_organizations',
89
+ '',
90
+ ].join('\n'),
91
+ );
92
+ });
93
+
94
+ it('renders a GET with a required path param as a keyword arg', () => {
95
+ const services: Service[] = [
96
+ {
97
+ name: 'Organizations',
98
+ operations: [
99
+ makeOp({
100
+ name: 'get_organization',
101
+ httpMethod: 'get',
102
+ path: '/organizations/{id}',
103
+ pathParams: [
104
+ {
105
+ name: 'id',
106
+ type: { kind: 'primitive', type: 'string' },
107
+ required: true,
108
+ example: 'org_01EHZNVPK3SFK441A1RGBFSHRT',
109
+ },
110
+ ],
111
+ }),
112
+ ],
113
+ },
114
+ ];
115
+ const [content] = runRuby(makeSpec(services));
116
+ expect(content).toContain('WorkOS.client.organizations.get_organization(id: "org_01EHZNVPK3SFK441A1RGBFSHRT")');
117
+ });
118
+
119
+ it('renders a POST with body kwargs, expanding the request body model fields', () => {
120
+ const models: Model[] = [
121
+ {
122
+ name: 'CreateOrganizationRequest',
123
+ fields: [
124
+ {
125
+ name: 'name',
126
+ type: { kind: 'primitive', type: 'string' },
127
+ required: true,
128
+ example: 'Foo Corp',
129
+ },
130
+ {
131
+ name: 'domain_data',
132
+ type: {
133
+ kind: 'array',
134
+ items: { kind: 'model', name: 'OrganizationDomainData' },
135
+ },
136
+ required: true,
137
+ },
138
+ {
139
+ name: 'metadata',
140
+ type: { kind: 'map', valueType: { kind: 'primitive', type: 'string' } },
141
+ required: false,
142
+ },
143
+ ],
144
+ },
145
+ {
146
+ name: 'OrganizationDomainData',
147
+ fields: [
148
+ {
149
+ name: 'domain',
150
+ type: { kind: 'primitive', type: 'string' },
151
+ required: true,
152
+ example: 'foo-corp.com',
153
+ },
154
+ {
155
+ name: 'state',
156
+ type: { kind: 'primitive', type: 'string' },
157
+ required: true,
158
+ example: 'pending',
159
+ },
160
+ ],
161
+ },
162
+ ];
163
+ const services: Service[] = [
164
+ {
165
+ name: 'Organizations',
166
+ operations: [
167
+ makeOp({
168
+ name: 'create_organization',
169
+ httpMethod: 'post',
170
+ path: '/organizations',
171
+ requestBody: { kind: 'model', name: 'CreateOrganizationRequest' },
172
+ response: { kind: 'model', name: 'Organization' },
173
+ }),
174
+ ],
175
+ },
176
+ ];
177
+ const [content] = runRuby(makeSpec(services, models));
178
+ // Optional `metadata` is omitted from the snippet — only required fields show.
179
+ expect(content).toContain('WorkOS.client.organizations.create_organization(');
180
+ expect(content).toContain('name: "Foo Corp"');
181
+ expect(content).toContain('domain_data:');
182
+ expect(content).toContain('domain: "foo-corp.com"');
183
+ expect(content).toContain('state: "pending"');
184
+ expect(content).not.toContain('metadata');
185
+ });
186
+
187
+ it('renames body fields that collide with path params', () => {
188
+ const models: Model[] = [
189
+ {
190
+ name: 'UpdateOrganizationRequest',
191
+ fields: [
192
+ {
193
+ name: 'id',
194
+ type: { kind: 'primitive', type: 'string' },
195
+ required: true,
196
+ example: 'override_id',
197
+ },
198
+ ],
199
+ },
200
+ ];
201
+ const services: Service[] = [
202
+ {
203
+ name: 'Organizations',
204
+ operations: [
205
+ makeOp({
206
+ name: 'update_organization',
207
+ httpMethod: 'put',
208
+ path: '/organizations/{id}',
209
+ pathParams: [
210
+ {
211
+ name: 'id',
212
+ type: { kind: 'primitive', type: 'string' },
213
+ required: true,
214
+ example: 'org_123',
215
+ },
216
+ ],
217
+ requestBody: { kind: 'model', name: 'UpdateOrganizationRequest' },
218
+ }),
219
+ ],
220
+ },
221
+ ];
222
+ const [content] = runRuby(makeSpec(services, models));
223
+ expect(content).toContain('id: "org_123"');
224
+ expect(content).toContain('body_id: "override_id"');
225
+ });
226
+
227
+ it('skips URL-builder operations', () => {
228
+ const services: Service[] = [
229
+ {
230
+ name: 'SSO',
231
+ operations: [
232
+ makeOp({
233
+ name: 'get_authorization_url',
234
+ httpMethod: 'get',
235
+ path: '/sso/authorize',
236
+ }),
237
+ ],
238
+ },
239
+ ];
240
+ const ctx = makeCtx(makeSpec(services));
241
+ ctx.resolvedOperations![0]!.urlBuilder = true;
242
+ const results = runSnippetEmitters([rubySnippetEmitter], ctx);
243
+ expect(results).toEqual([]);
244
+ });
245
+
246
+ it('hides params injected via defaults/inferFromClient', () => {
247
+ const models: Model[] = [
248
+ {
249
+ name: 'AuthenticateRequest',
250
+ fields: [
251
+ { name: 'grant_type', type: { kind: 'primitive', type: 'string' }, required: true },
252
+ { name: 'client_id', type: { kind: 'primitive', type: 'string' }, required: true },
253
+ {
254
+ name: 'email',
255
+ type: { kind: 'primitive', type: 'string' },
256
+ required: true,
257
+ example: 'user@example.com',
258
+ },
259
+ ],
260
+ },
261
+ ];
262
+ const services: Service[] = [
263
+ {
264
+ name: 'UserManagement',
265
+ operations: [
266
+ makeOp({
267
+ name: 'authenticate',
268
+ httpMethod: 'post',
269
+ path: '/user_management/authenticate',
270
+ requestBody: { kind: 'model', name: 'AuthenticateRequest' },
271
+ }),
272
+ ],
273
+ },
274
+ ];
275
+ const ctx = makeCtx(makeSpec(services, models));
276
+ ctx.resolvedOperations![0]!.defaults = { grant_type: 'password' };
277
+ ctx.resolvedOperations![0]!.inferFromClient = ['client_id'];
278
+
279
+ const results = runSnippetEmitters([rubySnippetEmitter], ctx);
280
+ const content = results[0]!.content;
281
+ expect(content).not.toContain('grant_type');
282
+ expect(content).not.toContain('client_id');
283
+ expect(content).toContain('email: "user@example.com"');
284
+ });
285
+
286
+ it('emits one snippet per split wrapper using the wrapper method name', () => {
287
+ const models: Model[] = [
288
+ {
289
+ name: 'PasswordAuthenticateRequest',
290
+ fields: [
291
+ { name: 'grant_type', type: { kind: 'primitive', type: 'string' }, required: true },
292
+ {
293
+ name: 'email',
294
+ type: { kind: 'primitive', type: 'string' },
295
+ required: true,
296
+ example: 'user@example.com',
297
+ },
298
+ {
299
+ name: 'password',
300
+ type: { kind: 'primitive', type: 'string' },
301
+ required: true,
302
+ example: 'hunter2',
303
+ },
304
+ ],
305
+ },
306
+ ];
307
+ const services: Service[] = [
308
+ {
309
+ name: 'UserManagement',
310
+ operations: [
311
+ makeOp({
312
+ name: 'authenticate',
313
+ httpMethod: 'post',
314
+ path: '/user_management/authenticate',
315
+ requestBody: { kind: 'model', name: 'PasswordAuthenticateRequest' },
316
+ }),
317
+ ],
318
+ },
319
+ ];
320
+ const ctx = makeCtx(makeSpec(services, models));
321
+ ctx.resolvedOperations![0]!.wrappers = [
322
+ {
323
+ name: 'authenticate_with_password',
324
+ targetVariant: 'PasswordAuthenticateRequest',
325
+ defaults: { grant_type: 'password' },
326
+ inferFromClient: ['client_id', 'client_secret'],
327
+ exposedParams: ['email', 'password'],
328
+ optionalParams: [],
329
+ responseModelName: 'AuthenticationResponse',
330
+ },
331
+ ];
332
+
333
+ const results = runSnippetEmitters([rubySnippetEmitter], ctx);
334
+ const content = results[0]!.content;
335
+ expect(content).toContain('WorkOS.client.user_management.authenticate_with_password(');
336
+ expect(content).toContain('email: "user@example.com"');
337
+ expect(content).toContain('password: "hunter2"');
338
+ });
339
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Model, Service } from '@workos/oagen';
3
+ import { runSnippetEmitters } from '@workos/oagen';
4
+ import { rustSnippetEmitter } from '../../src/snippets/rust.js';
5
+ import { makeCtx, makeOp, makeSpec, makeStringField } from './_helpers.js';
6
+
7
+ function runRust(services: Service[], models: Model[] = []): string {
8
+ const results = runSnippetEmitters([rustSnippetEmitter], makeCtx(makeSpec(services, models)));
9
+ return results[0]!.content;
10
+ }
11
+
12
+ describe('snippets/rust', () => {
13
+ it('renders a #[tokio::main] async main returning workos::Error', () => {
14
+ const content = runRust([{ name: 'Organizations', operations: [makeOp({ name: 'list_organizations' })] }]);
15
+ expect(content).toContain('use workos::Client;');
16
+ expect(content).toContain('#[tokio::main]');
17
+ expect(content).toContain('async fn main() -> Result<(), workos::Error> {');
18
+ expect(content).toContain('.api_key("sk_example_123456789")');
19
+ expect(content).toContain('.client_id("client_123456789")');
20
+ expect(content).toContain('.organizations()');
21
+ expect(content).toContain('.list_organizations()');
22
+ expect(content).toContain('.await?;');
23
+ });
24
+
25
+ it('uses a typed Params struct with .into() for String body fields', () => {
26
+ const content = runRust(
27
+ [
28
+ {
29
+ name: 'Organizations',
30
+ operations: [
31
+ makeOp({
32
+ name: 'create_organization',
33
+ httpMethod: 'post',
34
+ path: '/organizations',
35
+ requestBody: { kind: 'model', name: 'CreateOrgReq' },
36
+ }),
37
+ ],
38
+ },
39
+ ],
40
+ [
41
+ {
42
+ name: 'CreateOrgReq',
43
+ fields: [makeStringField('name', 'Foo Corp')],
44
+ },
45
+ ],
46
+ );
47
+ expect(content).toContain('use workos::organizations::CreateOrganizationParams;');
48
+ expect(content).toContain('CreateOrganizationParams {');
49
+ expect(content).toContain('name: "Foo Corp".into(),');
50
+ expect(content).toContain('..Default::default()');
51
+ });
52
+
53
+ it('renders required path params as positional &str arguments', () => {
54
+ const content = runRust([
55
+ {
56
+ name: 'Organizations',
57
+ operations: [
58
+ makeOp({
59
+ name: 'get_organization',
60
+ httpMethod: 'get',
61
+ path: '/organizations/{id}',
62
+ pathParams: [
63
+ {
64
+ name: 'id',
65
+ type: { kind: 'primitive', type: 'string' },
66
+ required: true,
67
+ example: 'org_123',
68
+ },
69
+ ],
70
+ }),
71
+ ],
72
+ },
73
+ ]);
74
+ expect(content).toContain('.get_organization("org_123")');
75
+ });
76
+ });