@workos/oagen-emitters 0.2.0 → 0.3.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/.husky/pre-commit +1 -0
- package/.oxfmtrc.json +8 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +129 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +11943 -2728
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/go.md +338 -0
- package/docs/sdk-architecture/php.md +315 -0
- package/docs/sdk-architecture/python.md +511 -0
- package/oagen.config.ts +298 -2
- package/package.json +9 -5
- package/scripts/generate-php.js +13 -0
- package/scripts/git-push-with-published-oagen.sh +21 -0
- package/smoke/sdk-dotnet.ts +17 -3
- package/smoke/sdk-elixir.ts +17 -3
- package/smoke/sdk-go.ts +137 -46
- package/smoke/sdk-kotlin.ts +23 -4
- package/smoke/sdk-node.ts +15 -3
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- package/smoke/sdk-ruby.ts +17 -3
- package/smoke/sdk-rust.ts +16 -3
- package/src/go/client.ts +141 -0
- package/src/go/enums.ts +196 -0
- package/src/go/fixtures.ts +212 -0
- package/src/go/index.ts +81 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +191 -0
- package/src/go/resources.ts +827 -0
- package/src/go/tests.ts +751 -0
- package/src/go/type-map.ts +82 -0
- package/src/go/wrappers.ts +261 -0
- package/src/index.ts +3 -0
- package/src/node/client.ts +167 -122
- package/src/node/enums.ts +13 -4
- package/src/node/errors.ts +42 -233
- package/src/node/field-plan.ts +726 -0
- package/src/node/fixtures.ts +15 -5
- package/src/node/index.ts +65 -16
- package/src/node/models.ts +264 -96
- package/src/node/naming.ts +52 -25
- package/src/node/resources.ts +621 -172
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +71 -27
- package/src/node/type-map.ts +4 -2
- package/src/node/utils.ts +56 -64
- package/src/node/wrappers.ts +151 -0
- package/src/php/client.ts +171 -0
- package/src/php/enums.ts +67 -0
- package/src/php/errors.ts +9 -0
- package/src/php/fixtures.ts +181 -0
- package/src/php/index.ts +96 -0
- package/src/php/manifest.ts +36 -0
- package/src/php/models.ts +310 -0
- package/src/php/naming.ts +298 -0
- package/src/php/resources.ts +561 -0
- package/src/php/tests.ts +533 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +151 -0
- package/src/python/client.ts +337 -0
- package/src/python/enums.ts +313 -0
- package/src/python/fixtures.ts +196 -0
- package/src/python/index.ts +95 -0
- package/src/python/manifest.ts +38 -0
- package/src/python/models.ts +688 -0
- package/src/python/naming.ts +209 -0
- package/src/python/resources.ts +1322 -0
- package/src/python/tests.ts +1335 -0
- package/src/python/type-map.ts +93 -0
- package/src/python/wrappers.ts +191 -0
- package/src/shared/model-utils.ts +255 -0
- package/src/shared/naming-utils.ts +107 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +59 -0
- package/test/go/client.test.ts +92 -0
- package/test/go/enums.test.ts +132 -0
- package/test/go/errors.test.ts +9 -0
- package/test/go/models.test.ts +265 -0
- package/test/go/resources.test.ts +408 -0
- package/test/go/tests.test.ts +143 -0
- package/test/node/client.test.ts +199 -94
- package/test/node/enums.test.ts +75 -3
- package/test/node/errors.test.ts +2 -41
- package/test/node/models.test.ts +109 -20
- package/test/node/naming.test.ts +37 -4
- package/test/node/resources.test.ts +662 -30
- package/test/node/serializers.test.ts +36 -7
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +94 -0
- package/test/php/enums.test.ts +173 -0
- package/test/php/errors.test.ts +9 -0
- package/test/php/models.test.ts +497 -0
- package/test/php/resources.test.ts +644 -0
- package/test/php/tests.test.ts +118 -0
- package/test/python/client.test.ts +200 -0
- package/test/python/enums.test.ts +228 -0
- package/test/python/errors.test.ts +16 -0
- package/test/python/manifest.test.ts +74 -0
- package/test/python/models.test.ts +716 -0
- package/test/python/resources.test.ts +617 -0
- package/test/python/tests.test.ts +202 -0
- package/src/node/common.ts +0 -273
- package/src/node/config.ts +0 -71
- package/src/node/serializers.ts +0 -744
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateResources } from '../../src/python/resources.js';
|
|
3
|
+
import type { EmitterContext, ApiSpec, Service, Model } from '@workos/oagen';
|
|
4
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
5
|
+
|
|
6
|
+
const emptySpec: ApiSpec = {
|
|
7
|
+
name: 'Test',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
baseUrl: '',
|
|
10
|
+
services: [],
|
|
11
|
+
models: [],
|
|
12
|
+
enums: [],
|
|
13
|
+
sdk: defaultSdkBehavior(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ctx: EmitterContext = {
|
|
17
|
+
namespace: 'workos',
|
|
18
|
+
namespacePascal: 'WorkOS',
|
|
19
|
+
spec: emptySpec,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('generateResources', () => {
|
|
23
|
+
it('returns empty for no services', () => {
|
|
24
|
+
expect(generateResources([], ctx)).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('generates a resource class with methods', () => {
|
|
28
|
+
const models: Model[] = [
|
|
29
|
+
{
|
|
30
|
+
name: 'Organization',
|
|
31
|
+
fields: [
|
|
32
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
33
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const services: Service[] = [
|
|
39
|
+
{
|
|
40
|
+
name: 'Organizations',
|
|
41
|
+
operations: [
|
|
42
|
+
{
|
|
43
|
+
name: 'getOrganization',
|
|
44
|
+
httpMethod: 'get',
|
|
45
|
+
path: '/organizations/{id}',
|
|
46
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
47
|
+
queryParams: [],
|
|
48
|
+
headerParams: [],
|
|
49
|
+
response: { kind: 'model', name: 'Organization' },
|
|
50
|
+
errors: [],
|
|
51
|
+
injectIdempotencyKey: false,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'deleteOrganization',
|
|
55
|
+
httpMethod: 'delete',
|
|
56
|
+
path: '/organizations/{id}',
|
|
57
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
58
|
+
queryParams: [],
|
|
59
|
+
headerParams: [],
|
|
60
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
61
|
+
errors: [],
|
|
62
|
+
injectIdempotencyKey: false,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const ctxWithServices: EmitterContext = {
|
|
69
|
+
...ctx,
|
|
70
|
+
spec: { ...emptySpec, services, models },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const files = generateResources(services, ctxWithServices);
|
|
74
|
+
expect(files.length).toBe(1);
|
|
75
|
+
expect(files[0].path).toBe('src/workos/organizations/_resource.py');
|
|
76
|
+
|
|
77
|
+
const content = files[0].content;
|
|
78
|
+
|
|
79
|
+
// Class definition
|
|
80
|
+
expect(content).toContain('class Organizations:');
|
|
81
|
+
expect(content).toContain('def __init__(self, client: "WorkOSClient") -> None:');
|
|
82
|
+
|
|
83
|
+
// GET method with path param
|
|
84
|
+
expect(content).toContain('def get_organization(');
|
|
85
|
+
expect(content).toContain('id: str,');
|
|
86
|
+
expect(content).toContain('f"organizations/{id}"');
|
|
87
|
+
expect(content).toContain('model=Organization');
|
|
88
|
+
// Public request methods (no underscore prefix)
|
|
89
|
+
expect(content).toContain('self._client.request(');
|
|
90
|
+
|
|
91
|
+
// DELETE method returns None
|
|
92
|
+
expect(content).toContain('def delete_organization(');
|
|
93
|
+
expect(content).toContain(') -> None:');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('generates paginated list method', () => {
|
|
97
|
+
const models: Model[] = [
|
|
98
|
+
{
|
|
99
|
+
name: 'Organization',
|
|
100
|
+
fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const services: Service[] = [
|
|
105
|
+
{
|
|
106
|
+
name: 'Organizations',
|
|
107
|
+
operations: [
|
|
108
|
+
{
|
|
109
|
+
name: 'listOrganizations',
|
|
110
|
+
httpMethod: 'get',
|
|
111
|
+
path: '/organizations',
|
|
112
|
+
pathParams: [],
|
|
113
|
+
queryParams: [
|
|
114
|
+
{
|
|
115
|
+
name: 'limit',
|
|
116
|
+
type: { kind: 'primitive', type: 'integer' },
|
|
117
|
+
required: false,
|
|
118
|
+
description: 'Upper limit on the number of objects to return, between `1` and `100`.',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'after',
|
|
122
|
+
type: { kind: 'primitive', type: 'string' },
|
|
123
|
+
required: false,
|
|
124
|
+
description:
|
|
125
|
+
'An object ID that defines your place in the list. When the ID is not present, you are at the end of the list.',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'order',
|
|
129
|
+
type: { kind: 'primitive', type: 'string' },
|
|
130
|
+
required: false,
|
|
131
|
+
description: 'Order the results by the creation time.',
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
headerParams: [],
|
|
135
|
+
response: { kind: 'model', name: 'OrganizationList' },
|
|
136
|
+
errors: [],
|
|
137
|
+
injectIdempotencyKey: false,
|
|
138
|
+
pagination: {
|
|
139
|
+
strategy: 'cursor',
|
|
140
|
+
param: 'after',
|
|
141
|
+
dataPath: 'data',
|
|
142
|
+
itemType: { kind: 'model', name: 'Organization' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const ctxWithServices: EmitterContext = {
|
|
150
|
+
...ctx,
|
|
151
|
+
spec: { ...emptySpec, services, models },
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const files = generateResources(services, ctxWithServices);
|
|
155
|
+
expect(files.length).toBe(1);
|
|
156
|
+
|
|
157
|
+
const content = files[0].content;
|
|
158
|
+
expect(content).toContain('def list_organizations(');
|
|
159
|
+
expect(content).toContain('limit: Optional[int] = None,');
|
|
160
|
+
expect(content).toContain('after: Optional[str] = None,');
|
|
161
|
+
expect(content).toContain(') -> SyncPage[Organization]:');
|
|
162
|
+
expect(content).toContain('request_page(');
|
|
163
|
+
expect(content).toContain('model=Organization');
|
|
164
|
+
expect(content).toContain('limit: Upper limit on the number of objects to return, between `1` and `100`.');
|
|
165
|
+
expect(content).toContain(
|
|
166
|
+
'after: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list.',
|
|
167
|
+
);
|
|
168
|
+
expect(content).toContain('order: Order the results by the creation time.');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('indents multiline argument descriptions in docstrings', () => {
|
|
172
|
+
const models: Model[] = [
|
|
173
|
+
{
|
|
174
|
+
name: 'GenerateLinkRequest',
|
|
175
|
+
fields: [
|
|
176
|
+
{
|
|
177
|
+
name: 'intent',
|
|
178
|
+
type: { kind: 'primitive', type: 'string' },
|
|
179
|
+
required: false,
|
|
180
|
+
description: [
|
|
181
|
+
'The intent of the Admin Portal.',
|
|
182
|
+
'- `sso` - Launch Admin Portal for creating SSO connections',
|
|
183
|
+
'- `dsync` - Launch Admin Portal for creating Directory Sync connections',
|
|
184
|
+
].join('\n'),
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'organization',
|
|
188
|
+
type: { kind: 'primitive', type: 'string' },
|
|
189
|
+
required: true,
|
|
190
|
+
description: 'An organization identifier.',
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'PortalLinkResponse',
|
|
196
|
+
fields: [{ name: 'link', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const services: Service[] = [
|
|
201
|
+
{
|
|
202
|
+
name: 'AdminPortal',
|
|
203
|
+
operations: [
|
|
204
|
+
{
|
|
205
|
+
name: 'generateLink',
|
|
206
|
+
httpMethod: 'post',
|
|
207
|
+
path: '/portal/generate_link',
|
|
208
|
+
pathParams: [],
|
|
209
|
+
queryParams: [],
|
|
210
|
+
headerParams: [],
|
|
211
|
+
requestBody: { kind: 'model', name: 'GenerateLinkRequest' },
|
|
212
|
+
response: { kind: 'model', name: 'PortalLinkResponse' },
|
|
213
|
+
description: 'Generate a Portal Link scoped to an Organization.',
|
|
214
|
+
errors: [],
|
|
215
|
+
injectIdempotencyKey: false,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const ctxWithServices: EmitterContext = {
|
|
222
|
+
...ctx,
|
|
223
|
+
spec: { ...emptySpec, services, models },
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const files = generateResources(services, ctxWithServices);
|
|
227
|
+
const content = files[0].content;
|
|
228
|
+
|
|
229
|
+
expect(content).toContain('intent: The intent of the Admin Portal.');
|
|
230
|
+
expect(content).toContain(' - `sso` - Launch Admin Portal for creating SSO connections');
|
|
231
|
+
expect(content).toContain(
|
|
232
|
+
' - `dsync` - Launch Admin Portal for creating Directory Sync connections',
|
|
233
|
+
);
|
|
234
|
+
expect(content).toContain('organization: An organization identifier.');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('unwraps list wrapper models in paginated methods', () => {
|
|
238
|
+
const models: Model[] = [
|
|
239
|
+
{
|
|
240
|
+
name: 'Organization',
|
|
241
|
+
fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'OrganizationList',
|
|
245
|
+
fields: [
|
|
246
|
+
{
|
|
247
|
+
name: 'data',
|
|
248
|
+
type: { kind: 'array', items: { kind: 'model', name: 'Organization' } },
|
|
249
|
+
required: true,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: 'list_metadata',
|
|
253
|
+
type: { kind: 'model', name: 'ListMetadata' },
|
|
254
|
+
required: true,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: 'object',
|
|
258
|
+
type: { kind: 'primitive', type: 'string' },
|
|
259
|
+
required: true,
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
const services: Service[] = [
|
|
266
|
+
{
|
|
267
|
+
name: 'Organizations',
|
|
268
|
+
operations: [
|
|
269
|
+
{
|
|
270
|
+
name: 'listOrganizations',
|
|
271
|
+
httpMethod: 'get',
|
|
272
|
+
path: '/organizations',
|
|
273
|
+
pathParams: [],
|
|
274
|
+
queryParams: [],
|
|
275
|
+
headerParams: [],
|
|
276
|
+
response: { kind: 'model', name: 'OrganizationList' },
|
|
277
|
+
errors: [],
|
|
278
|
+
injectIdempotencyKey: false,
|
|
279
|
+
pagination: {
|
|
280
|
+
strategy: 'cursor',
|
|
281
|
+
param: 'after',
|
|
282
|
+
dataPath: 'data',
|
|
283
|
+
itemType: { kind: 'model', name: 'OrganizationList' },
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
const ctxWithServices: EmitterContext = {
|
|
291
|
+
...ctx,
|
|
292
|
+
spec: { ...emptySpec, services, models },
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const files = generateResources(services, ctxWithServices);
|
|
296
|
+
const content = files[0].content;
|
|
297
|
+
|
|
298
|
+
// Should use item model, not list wrapper
|
|
299
|
+
expect(content).toContain(') -> SyncPage[Organization]:');
|
|
300
|
+
expect(content).toContain('model=Organization');
|
|
301
|
+
expect(content).not.toContain('model=OrganizationList');
|
|
302
|
+
expect(content).not.toContain('SyncPage[OrganizationList]');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('generates DELETE with body when requestBody is present', () => {
|
|
306
|
+
const models: Model[] = [
|
|
307
|
+
{
|
|
308
|
+
name: 'RemoveRoleRequest',
|
|
309
|
+
fields: [
|
|
310
|
+
{ name: 'role_slug', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
311
|
+
{ name: 'resource_id', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
const services: Service[] = [
|
|
317
|
+
{
|
|
318
|
+
name: 'Authorization',
|
|
319
|
+
operations: [
|
|
320
|
+
{
|
|
321
|
+
name: 'removeRole',
|
|
322
|
+
httpMethod: 'delete',
|
|
323
|
+
path: '/authorization/roles/{user_id}',
|
|
324
|
+
pathParams: [{ name: 'user_id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
325
|
+
queryParams: [],
|
|
326
|
+
headerParams: [],
|
|
327
|
+
requestBody: { kind: 'model', name: 'RemoveRoleRequest' },
|
|
328
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
329
|
+
errors: [],
|
|
330
|
+
injectIdempotencyKey: false,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
const ctxWithServices: EmitterContext = {
|
|
337
|
+
...ctx,
|
|
338
|
+
spec: { ...emptySpec, services, models },
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const files = generateResources(services, ctxWithServices);
|
|
342
|
+
const content = files[0].content;
|
|
343
|
+
|
|
344
|
+
expect(content).toContain(') -> None:');
|
|
345
|
+
expect(content).toContain('role_slug: str,');
|
|
346
|
+
expect(content).toContain('"role_slug": role_slug');
|
|
347
|
+
expect(content).toContain('body=body,');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('calls .to_dict() on model-typed body fields', () => {
|
|
351
|
+
const models: Model[] = [
|
|
352
|
+
{
|
|
353
|
+
name: 'AuditLogEvent',
|
|
354
|
+
fields: [{ name: 'action', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: 'AuditLogSchemaTarget',
|
|
358
|
+
fields: [{ name: 'type', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: 'CreateEventRequest',
|
|
362
|
+
fields: [
|
|
363
|
+
{ name: 'event', type: { kind: 'model', name: 'AuditLogEvent' }, required: true },
|
|
364
|
+
{
|
|
365
|
+
name: 'targets',
|
|
366
|
+
type: { kind: 'array', items: { kind: 'model', name: 'AuditLogSchemaTarget' } },
|
|
367
|
+
required: true,
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'EventResult',
|
|
373
|
+
fields: [{ name: 'success', type: { kind: 'primitive', type: 'boolean' }, required: true }],
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
const services: Service[] = [
|
|
378
|
+
{
|
|
379
|
+
name: 'AuditLogs',
|
|
380
|
+
operations: [
|
|
381
|
+
{
|
|
382
|
+
name: 'createEvent',
|
|
383
|
+
httpMethod: 'post',
|
|
384
|
+
path: '/audit_logs/events',
|
|
385
|
+
pathParams: [],
|
|
386
|
+
queryParams: [],
|
|
387
|
+
headerParams: [],
|
|
388
|
+
requestBody: { kind: 'model', name: 'CreateEventRequest' },
|
|
389
|
+
response: { kind: 'model', name: 'EventResult' },
|
|
390
|
+
errors: [],
|
|
391
|
+
injectIdempotencyKey: false,
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
const ctxWithServices: EmitterContext = {
|
|
398
|
+
...ctx,
|
|
399
|
+
spec: { ...emptySpec, services, models },
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const files = generateResources(services, ctxWithServices);
|
|
403
|
+
const content = files[0].content;
|
|
404
|
+
|
|
405
|
+
// Model field should call .to_dict() directly
|
|
406
|
+
expect(content).toContain('"event": event.to_dict()');
|
|
407
|
+
// Array of models should use list comprehension calling .to_dict()
|
|
408
|
+
expect(content).toContain('"targets": [item.to_dict() for item in targets]');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('generates idempotent POST with idempotency_key', () => {
|
|
412
|
+
const models: Model[] = [
|
|
413
|
+
{
|
|
414
|
+
name: 'Organization',
|
|
415
|
+
fields: [
|
|
416
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
417
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: 'CreateOrganizationRequest',
|
|
422
|
+
fields: [{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
423
|
+
},
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
const services: Service[] = [
|
|
427
|
+
{
|
|
428
|
+
name: 'Organizations',
|
|
429
|
+
operations: [
|
|
430
|
+
{
|
|
431
|
+
name: 'createOrganization',
|
|
432
|
+
httpMethod: 'post',
|
|
433
|
+
path: '/organizations',
|
|
434
|
+
pathParams: [],
|
|
435
|
+
queryParams: [],
|
|
436
|
+
headerParams: [],
|
|
437
|
+
requestBody: { kind: 'model', name: 'CreateOrganizationRequest' },
|
|
438
|
+
response: { kind: 'model', name: 'Organization' },
|
|
439
|
+
errors: [],
|
|
440
|
+
injectIdempotencyKey: true,
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
];
|
|
445
|
+
|
|
446
|
+
const ctxWithServices: EmitterContext = {
|
|
447
|
+
...ctx,
|
|
448
|
+
spec: { ...emptySpec, services, models },
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const files = generateResources(services, ctxWithServices);
|
|
452
|
+
const content = files[0].content;
|
|
453
|
+
expect(content).toContain('idempotency_key: Optional[str] = None,');
|
|
454
|
+
expect(content).toContain('idempotency_key=idempotency_key,');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('adds deprecated annotation to operations', () => {
|
|
458
|
+
const models: Model[] = [
|
|
459
|
+
{
|
|
460
|
+
name: 'Organization',
|
|
461
|
+
fields: [
|
|
462
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
463
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
464
|
+
],
|
|
465
|
+
},
|
|
466
|
+
];
|
|
467
|
+
|
|
468
|
+
const services: Service[] = [
|
|
469
|
+
{
|
|
470
|
+
name: 'Organizations',
|
|
471
|
+
operations: [
|
|
472
|
+
{
|
|
473
|
+
name: 'getOrganization',
|
|
474
|
+
httpMethod: 'get',
|
|
475
|
+
path: '/organizations/{id}',
|
|
476
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
477
|
+
queryParams: [],
|
|
478
|
+
headerParams: [],
|
|
479
|
+
response: { kind: 'model', name: 'Organization' },
|
|
480
|
+
errors: [],
|
|
481
|
+
injectIdempotencyKey: false,
|
|
482
|
+
deprecated: true,
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
},
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
const ctxWithServices: EmitterContext = {
|
|
489
|
+
...ctx,
|
|
490
|
+
spec: { ...emptySpec, services, models },
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const files = generateResources(services, ctxWithServices);
|
|
494
|
+
const content = files[0].content;
|
|
495
|
+
|
|
496
|
+
// Docstring should contain .. deprecated::
|
|
497
|
+
expect(content).toContain('.. deprecated::');
|
|
498
|
+
expect(content).toContain('This operation is deprecated.');
|
|
499
|
+
|
|
500
|
+
// Body should contain warnings.warn
|
|
501
|
+
expect(content).toContain('warnings.warn("get_organization is deprecated", DeprecationWarning, stacklevel=2)');
|
|
502
|
+
|
|
503
|
+
// Import warnings should be present
|
|
504
|
+
expect(content).toContain('import warnings');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('does not import warnings when no operations are deprecated', () => {
|
|
508
|
+
const models: Model[] = [
|
|
509
|
+
{
|
|
510
|
+
name: 'Organization',
|
|
511
|
+
fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
512
|
+
},
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
const services: Service[] = [
|
|
516
|
+
{
|
|
517
|
+
name: 'Organizations',
|
|
518
|
+
operations: [
|
|
519
|
+
{
|
|
520
|
+
name: 'getOrganization',
|
|
521
|
+
httpMethod: 'get',
|
|
522
|
+
path: '/organizations/{id}',
|
|
523
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
524
|
+
queryParams: [],
|
|
525
|
+
headerParams: [],
|
|
526
|
+
response: { kind: 'model', name: 'Organization' },
|
|
527
|
+
errors: [],
|
|
528
|
+
injectIdempotencyKey: false,
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
},
|
|
532
|
+
];
|
|
533
|
+
|
|
534
|
+
const ctxWithServices: EmitterContext = {
|
|
535
|
+
...ctx,
|
|
536
|
+
spec: { ...emptySpec, services, models },
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const files = generateResources(services, ctxWithServices);
|
|
540
|
+
const content = files[0].content;
|
|
541
|
+
expect(content).not.toContain('import warnings');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('marks deprecated parameters with (deprecated) prefix in Args docstring', () => {
|
|
545
|
+
const models: Model[] = [
|
|
546
|
+
{
|
|
547
|
+
name: 'Organization',
|
|
548
|
+
fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
name: 'UpdateOrgRequest',
|
|
552
|
+
fields: [
|
|
553
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
554
|
+
{
|
|
555
|
+
name: 'old_field',
|
|
556
|
+
type: { kind: 'primitive', type: 'string' },
|
|
557
|
+
required: false,
|
|
558
|
+
deprecated: true,
|
|
559
|
+
description: 'Legacy field',
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
},
|
|
563
|
+
];
|
|
564
|
+
|
|
565
|
+
const services: Service[] = [
|
|
566
|
+
{
|
|
567
|
+
name: 'Organizations',
|
|
568
|
+
operations: [
|
|
569
|
+
{
|
|
570
|
+
name: 'updateOrganization',
|
|
571
|
+
httpMethod: 'put',
|
|
572
|
+
path: '/organizations/{id}',
|
|
573
|
+
pathParams: [
|
|
574
|
+
{
|
|
575
|
+
name: 'id',
|
|
576
|
+
type: { kind: 'primitive', type: 'string' },
|
|
577
|
+
required: true,
|
|
578
|
+
deprecated: true,
|
|
579
|
+
description: 'The org ID',
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
queryParams: [
|
|
583
|
+
{
|
|
584
|
+
name: 'legacy_param',
|
|
585
|
+
type: { kind: 'primitive', type: 'string' },
|
|
586
|
+
required: false,
|
|
587
|
+
deprecated: true,
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
headerParams: [],
|
|
591
|
+
requestBody: { kind: 'model', name: 'UpdateOrgRequest' },
|
|
592
|
+
response: { kind: 'model', name: 'Organization' },
|
|
593
|
+
errors: [],
|
|
594
|
+
injectIdempotencyKey: false,
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
},
|
|
598
|
+
];
|
|
599
|
+
|
|
600
|
+
const ctxWithServices: EmitterContext = {
|
|
601
|
+
...ctx,
|
|
602
|
+
spec: { ...emptySpec, services, models },
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const files = generateResources(services, ctxWithServices);
|
|
606
|
+
const content = files[0].content;
|
|
607
|
+
|
|
608
|
+
// Deprecated path param with description
|
|
609
|
+
expect(content).toContain('id: (deprecated) The org ID');
|
|
610
|
+
|
|
611
|
+
// Deprecated body field with description
|
|
612
|
+
expect(content).toContain('old_field: (deprecated) Legacy field');
|
|
613
|
+
|
|
614
|
+
// Deprecated query param without description
|
|
615
|
+
expect(content).toContain('legacy_param: (deprecated)');
|
|
616
|
+
});
|
|
617
|
+
});
|