@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
@@ -1,7 +1,8 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import type { EmitterContext, ApiSpec, Model } from '@workos/oagen';
2
+ import type { EmitterContext, ApiSpec, Model, Service } from '@workos/oagen';
3
3
  import { defaultSdkBehavior } from '@workos/oagen';
4
- import { modelHasNewFields } from '../../src/node/utils.js';
4
+ import { modelHasNewFields, createServiceDirResolver } from '../../src/node/utils.js';
5
+ import { emptyLiveSurface, setActiveLiveSurface } from '../../src/node/live-surface.js';
5
6
 
6
7
  const emptySpec: ApiSpec = {
7
8
  name: 'Test',
@@ -87,3 +88,157 @@ describe('modelHasNewFields', () => {
87
88
  expect(modelHasNewFields(model, ctxWithSurface)).toBe(true);
88
89
  });
89
90
  });
91
+
92
+ describe('createServiceDirResolver owned-service dependency reassignment', () => {
93
+ const retentionModel: Model = {
94
+ name: 'AuditLogsRetention',
95
+ fields: [{ name: 'retention_period_in_days', type: { kind: 'primitive', type: 'integer' }, required: true }],
96
+ };
97
+
98
+ function retentionOp(name: string, path: string) {
99
+ return {
100
+ name,
101
+ httpMethod: 'get' as const,
102
+ path,
103
+ pathParams: [],
104
+ queryParams: [],
105
+ headerParams: [],
106
+ response: { kind: 'model' as const, name: 'AuditLogsRetention' },
107
+ errors: [],
108
+ injectIdempotencyKey: false,
109
+ };
110
+ }
111
+
112
+ // Organizations comes first, so first-reference-wins assignment parks the
113
+ // model in `organizations/` even though only AuditLogs is owned this run.
114
+ const services: Service[] = [
115
+ { name: 'Organizations', operations: [retentionOp('getRetention', '/organizations/{id}/retention')] },
116
+ { name: 'AuditLogs', operations: [retentionOp('getAuditLogsRetention', '/audit_logs/retention')] },
117
+ ];
118
+
119
+ function makeCtx(): EmitterContext {
120
+ return {
121
+ ...ctx,
122
+ spec: { ...emptySpec, models: [retentionModel], services },
123
+ emitterOptions: { ownedServices: ['AuditLogs'] },
124
+ } as EmitterContext;
125
+ }
126
+
127
+ it('reassigns dependency models of owned services out of unemittable directories', () => {
128
+ const surface = emptyLiveSurface();
129
+ surface.files.add('src/workos.ts'); // existing SDK
130
+ setActiveLiveSurface(surface);
131
+ try {
132
+ const testCtx = makeCtx();
133
+ const { modelToService, resolveDir } = createServiceDirResolver([retentionModel], services, testCtx);
134
+ expect(resolveDir(modelToService.get('AuditLogsRetention'))).toBe('audit-logs');
135
+ } finally {
136
+ setActiveLiveSurface(emptyLiveSurface());
137
+ }
138
+ });
139
+
140
+ it('leaves the assignment alone when the interface already exists on disk', () => {
141
+ const surface = emptyLiveSurface();
142
+ surface.files.add('src/workos.ts');
143
+ surface.files.add('src/organizations/interfaces/audit-logs-retention.interface.ts');
144
+ setActiveLiveSurface(surface);
145
+ try {
146
+ const testCtx = makeCtx();
147
+ const { modelToService, resolveDir } = createServiceDirResolver([retentionModel], services, testCtx);
148
+ expect(resolveDir(modelToService.get('AuditLogsRetention'))).toBe('organizations');
149
+ } finally {
150
+ setActiveLiveSurface(emptyLiveSurface());
151
+ }
152
+ });
153
+
154
+ it('does not reassign when no services are owned', () => {
155
+ const surface = emptyLiveSurface();
156
+ surface.files.add('src/workos.ts');
157
+ setActiveLiveSurface(surface);
158
+ try {
159
+ const testCtx = { ...makeCtx(), emitterOptions: {} } as EmitterContext;
160
+ const { modelToService, resolveDir } = createServiceDirResolver([retentionModel], services, testCtx);
161
+ expect(resolveDir(modelToService.get('AuditLogsRetention'))).toBe('organizations');
162
+ } finally {
163
+ setActiveLiveSurface(emptyLiveSurface());
164
+ }
165
+ });
166
+
167
+ it('does not reassign in greenfield mode where every directory is emittable', () => {
168
+ const testCtx = makeCtx();
169
+ const { modelToService, resolveDir } = createServiceDirResolver([retentionModel], services, testCtx);
170
+ expect(resolveDir(modelToService.get('AuditLogsRetention'))).toBe('organizations');
171
+ });
172
+
173
+ it('reassigns the full transitive closure when ops are re-mounted onto an owned service', () => {
174
+ // Real instance: GET/PUT /organizations/{organizationId}/audit_logs_retention
175
+ // live on the IR Organizations service but are MOUNTED on AuditLogs via
176
+ // resolvedOperations. Walking only IR services misses them entirely, so
177
+ // `AuditLogsRetention` (and everything it references) stays assigned to
178
+ // the unemittable organizations dir and is never emitted anywhere.
179
+ const nestedModel: Model = {
180
+ name: 'RetentionPolicy',
181
+ fields: [{ name: 'kind', type: { kind: 'primitive', type: 'string' }, required: true }],
182
+ };
183
+ const retentionWithNested: Model = {
184
+ ...retentionModel,
185
+ fields: [
186
+ ...retentionModel.fields,
187
+ { name: 'policy', type: { kind: 'model', name: 'RetentionPolicy' }, required: true },
188
+ ],
189
+ };
190
+ const listOrgsOp = {
191
+ name: 'listOrganizations',
192
+ httpMethod: 'get' as const,
193
+ path: '/organizations',
194
+ pathParams: [],
195
+ queryParams: [],
196
+ headerParams: [],
197
+ response: { kind: 'primitive' as const, type: 'unknown' as const },
198
+ errors: [],
199
+ injectIdempotencyKey: false,
200
+ };
201
+ const mountedRetentionOp = retentionOp(
202
+ 'getAuditLogsRetention',
203
+ '/organizations/{organizationId}/audit_logs_retention',
204
+ );
205
+ const orgService: Service = { name: 'Organizations', operations: [listOrgsOp, mountedRetentionOp] };
206
+ const mountedServices: Service[] = [orgService];
207
+
208
+ const surface = emptyLiveSurface();
209
+ surface.files.add('src/workos.ts'); // existing SDK
210
+ setActiveLiveSurface(surface);
211
+ try {
212
+ const testCtx = {
213
+ ...ctx,
214
+ spec: { ...emptySpec, models: [retentionWithNested, nestedModel], services: mountedServices },
215
+ emitterOptions: { ownedServices: ['AuditLogs'] },
216
+ resolvedOperations: [
217
+ {
218
+ operation: listOrgsOp,
219
+ service: orgService,
220
+ methodName: 'list_organizations',
221
+ mountOn: 'Organizations',
222
+ },
223
+ {
224
+ operation: mountedRetentionOp,
225
+ service: orgService,
226
+ methodName: 'get_audit_logs_retention',
227
+ mountOn: 'AuditLogs',
228
+ },
229
+ ],
230
+ } as unknown as EmitterContext;
231
+ const { modelToService, resolveDir } = createServiceDirResolver(
232
+ [retentionWithNested, nestedModel],
233
+ mountedServices,
234
+ testCtx,
235
+ );
236
+ expect(resolveDir(modelToService.get('AuditLogsRetention'))).toBe('audit-logs');
237
+ // Nested dependency N follows M into the owned dir — the closure must
238
+ // not stop at the directly-referenced model.
239
+ expect(resolveDir(modelToService.get('RetentionPolicy'))).toBe('audit-logs');
240
+ } finally {
241
+ setActiveLiveSurface(emptyLiveSurface());
242
+ }
243
+ });
244
+ });
@@ -0,0 +1,67 @@
1
+ import type { ApiSpec, EmitterContext, Field, Model, Operation, ResolvedOperation, Service } from '@workos/oagen';
2
+ import { defaultSdkBehavior, toPascalCase, toSnakeCase } from '@workos/oagen';
3
+
4
+ export function makeSpec(services: Service[], models: Model[] = []): ApiSpec {
5
+ return {
6
+ name: 'Test',
7
+ version: '1.0.0',
8
+ baseUrl: '',
9
+ services,
10
+ models,
11
+ enums: [],
12
+ sdk: defaultSdkBehavior(),
13
+ };
14
+ }
15
+
16
+ export function buildResolvedOps(services: Service[]): ResolvedOperation[] {
17
+ const ops: ResolvedOperation[] = [];
18
+ for (const service of services) {
19
+ const mountOn = toPascalCase(service.name);
20
+ for (const op of service.operations) {
21
+ ops.push({
22
+ operation: op,
23
+ service,
24
+ methodName: toSnakeCase(op.name),
25
+ mountOn,
26
+ defaults: {},
27
+ inferFromClient: [],
28
+ urlBuilder: false,
29
+ });
30
+ }
31
+ }
32
+ return ops;
33
+ }
34
+
35
+ export function makeCtx(spec: ApiSpec): EmitterContext {
36
+ return {
37
+ namespace: 'workos',
38
+ namespacePascal: 'WorkOS',
39
+ spec,
40
+ resolvedOperations: buildResolvedOps(spec.services),
41
+ };
42
+ }
43
+
44
+ export function makeOp(overrides: Partial<Operation>): Operation {
45
+ return {
46
+ name: 'listOrganizations',
47
+ httpMethod: 'get',
48
+ path: '/organizations',
49
+ pathParams: [],
50
+ queryParams: [],
51
+ headerParams: [],
52
+ requestBody: undefined,
53
+ response: { kind: 'model', name: 'Organization' },
54
+ errors: [],
55
+ injectIdempotencyKey: false,
56
+ ...overrides,
57
+ };
58
+ }
59
+
60
+ export function makeStringField(name: string, example?: string, required = true): Field {
61
+ return {
62
+ name,
63
+ type: { kind: 'primitive', type: 'string' },
64
+ required,
65
+ ...(example !== undefined ? { example } : {}),
66
+ };
67
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Model, Service } from '@workos/oagen';
3
+ import { runSnippetEmitters } from '@workos/oagen';
4
+ import { dotnetSnippetEmitter } from '../../src/snippets/dotnet.js';
5
+ import { makeCtx, makeOp, makeSpec, makeStringField } from './_helpers.js';
6
+
7
+ function runDotnet(services: Service[], models: Model[] = []): string {
8
+ const results = runSnippetEmitters([dotnetSnippetEmitter], makeCtx(makeSpec(services, models)));
9
+ return results[0]!.content;
10
+ }
11
+
12
+ describe('snippets/dotnet', () => {
13
+ it('renders WorkOSClient with WorkOSOptions and an Async-suffixed call', () => {
14
+ const content = runDotnet([{ name: 'Organizations', operations: [makeOp({ name: 'list_organizations' })] }]);
15
+ expect(content).toContain('using WorkOS;');
16
+ expect(content).toContain('var client = new WorkOSClient(new WorkOSOptions');
17
+ expect(content).toContain('ApiKey = "sk_example_123456789"');
18
+ expect(content).toContain('ClientId = "client_123456789"');
19
+ // Mount target `Organizations`; method `list_organizations` → ListAsync.
20
+ expect(content).toContain('await client.Organizations.ListAsync();');
21
+ });
22
+
23
+ it('builds the typed Options class for body args', () => {
24
+ const content = runDotnet(
25
+ [
26
+ {
27
+ name: 'Organizations',
28
+ operations: [
29
+ makeOp({
30
+ name: 'create_organization',
31
+ httpMethod: 'post',
32
+ path: '/organizations',
33
+ requestBody: { kind: 'model', name: 'CreateOrgReq' },
34
+ }),
35
+ ],
36
+ },
37
+ ],
38
+ [
39
+ {
40
+ name: 'CreateOrgReq',
41
+ fields: [makeStringField('name', 'Foo Corp')],
42
+ },
43
+ ],
44
+ );
45
+ expect(content).toContain('await client.Organizations.CreateAsync(');
46
+ expect(content).toContain('new OrganizationsCreateOptions');
47
+ expect(content).toContain('Name = "Foo Corp",');
48
+ });
49
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Model, Service } from '@workos/oagen';
3
+ import { runSnippetEmitters } from '@workos/oagen';
4
+ import { goSnippetEmitter } from '../../src/snippets/go.js';
5
+ import { makeCtx, makeOp, makeSpec, makeStringField } from './_helpers.js';
6
+
7
+ function runGo(services: Service[], models: Model[] = []): string {
8
+ const results = runSnippetEmitters([goSnippetEmitter], makeCtx(makeSpec(services, models)));
9
+ return results[0]!.content;
10
+ }
11
+
12
+ describe('snippets/go', () => {
13
+ it('renders a package main with context import and client init', () => {
14
+ const content = runGo([{ name: 'Organizations', operations: [makeOp({ name: 'list_organizations' })] }]);
15
+ expect(content).toContain('package main');
16
+ expect(content).toContain('"context"');
17
+ expect(content).toContain('"github.com/workos/workos-go/v9"');
18
+ expect(content).toContain('client := workos.NewClient("sk_example_123456789")');
19
+ });
20
+
21
+ it('trims the mount-target resource from the method name', () => {
22
+ // Mount target is `Organizations`; the resolved method `create_organization`
23
+ // becomes `CreateOrganization` then trims to `Create`.
24
+ const content = runGo(
25
+ [
26
+ {
27
+ name: 'Organizations',
28
+ operations: [
29
+ makeOp({
30
+ name: 'create_organization',
31
+ httpMethod: 'post',
32
+ path: '/organizations',
33
+ requestBody: { kind: 'model', name: 'CreateOrgReq' },
34
+ }),
35
+ ],
36
+ },
37
+ ],
38
+ [{ name: 'CreateOrgReq', fields: [makeStringField('name', 'Foo Corp')] }],
39
+ );
40
+ expect(content).toContain('client.Organizations().Create(');
41
+ expect(content).not.toContain('CreateOrganization(');
42
+ });
43
+
44
+ it('renders required body fields inside a typed opts struct', () => {
45
+ const content = runGo(
46
+ [
47
+ {
48
+ name: 'Organizations',
49
+ operations: [
50
+ makeOp({
51
+ name: 'create_organization',
52
+ httpMethod: 'post',
53
+ path: '/organizations',
54
+ requestBody: { kind: 'model', name: 'CreateOrgReq' },
55
+ }),
56
+ ],
57
+ },
58
+ ],
59
+ [
60
+ {
61
+ name: 'CreateOrgReq',
62
+ fields: [makeStringField('name', 'Foo Corp'), makeStringField('description', undefined, false)],
63
+ },
64
+ ],
65
+ );
66
+ expect(content).toContain('&workos.OrganizationsCreateParams{');
67
+ expect(content).toContain('Name: "Foo Corp",');
68
+ expect(content).not.toContain('Description:');
69
+ });
70
+
71
+ it('passes required path params positionally before the opts struct', () => {
72
+ const content = runGo([
73
+ {
74
+ name: 'Organizations',
75
+ operations: [
76
+ makeOp({
77
+ name: 'get_organization',
78
+ httpMethod: 'get',
79
+ path: '/organizations/{id}',
80
+ pathParams: [
81
+ {
82
+ name: 'id',
83
+ type: { kind: 'primitive', type: 'string' },
84
+ required: true,
85
+ example: 'org_123',
86
+ },
87
+ ],
88
+ }),
89
+ ],
90
+ },
91
+ ]);
92
+ expect(content).toContain('client.Organizations().Get(context.Background(), "org_123")');
93
+ });
94
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Model, Service } from '@workos/oagen';
3
+ import { runSnippetEmitters } from '@workos/oagen';
4
+ import { kotlinSnippetEmitter } from '../../src/snippets/kotlin.js';
5
+ import { makeCtx, makeOp, makeSpec, makeStringField } from './_helpers.js';
6
+
7
+ function runKotlin(services: Service[], models: Model[] = []): string {
8
+ const results = runSnippetEmitters([kotlinSnippetEmitter], makeCtx(makeSpec(services, models)));
9
+ return results[0]!.content;
10
+ }
11
+
12
+ describe('snippets/kotlin (Java-syntax output)', () => {
13
+ it('emits the .java file extension and Java client init', () => {
14
+ const results = runSnippetEmitters(
15
+ [kotlinSnippetEmitter],
16
+ makeCtx(makeSpec([{ name: 'Organizations', operations: [makeOp({ name: 'list_organizations' })] }])),
17
+ );
18
+ expect(results[0]!.fileExtension).toBe('java');
19
+ expect(results[0]!.language).toBe('java');
20
+ expect(results[0]!.content).toContain('import com.workos.WorkOS;');
21
+ expect(results[0]!.content).toContain('WorkOS workos = new WorkOS("sk_example_123456789");');
22
+ expect(results[0]!.content).toContain('workos.organizations.listOrganizations();');
23
+ });
24
+
25
+ it('builds the typed Options class via builder() for body args', () => {
26
+ const content = runKotlin(
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('import com.workos.organizations.OrganizationsApi.CreateOrganizationOptions;');
48
+ expect(content).toContain('CreateOrganizationOptions options = CreateOrganizationOptions.builder()');
49
+ expect(content).toContain('.name("Foo Corp")');
50
+ expect(content).toContain('.build();');
51
+ expect(content).toContain('workos.organizations.createOrganization(options);');
52
+ });
53
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Model, Service } from '@workos/oagen';
3
+ import { runSnippetEmitters } from '@workos/oagen';
4
+ import { phpSnippetEmitter } from '../../src/snippets/php.js';
5
+ import { makeCtx, makeOp, makeSpec, makeStringField } from './_helpers.js';
6
+
7
+ function runPhp(services: Service[], models: Model[] = []): string {
8
+ const results = runSnippetEmitters([phpSnippetEmitter], makeCtx(makeSpec(services, models)));
9
+ return results[0]!.content;
10
+ }
11
+
12
+ describe('snippets/php', () => {
13
+ it('renders the modern WorkOS client constructor with named args', () => {
14
+ const content = runPhp([{ name: 'Organizations', operations: [makeOp({ name: 'list_organizations' })] }]);
15
+ expect(content).toContain('<?php');
16
+ expect(content).toContain('use WorkOS\\WorkOS;');
17
+ expect(content).toContain("apiKey: 'sk_example_123456789'");
18
+ expect(content).toContain("clientId: 'client_123456789'");
19
+ expect(content).toContain('$workos->organizations()->listOrganizations();');
20
+ });
21
+
22
+ it('camelCases body field kwargs and uses PHP named args', () => {
23
+ const content = runPhp(
24
+ [
25
+ {
26
+ name: 'Organizations',
27
+ operations: [
28
+ makeOp({
29
+ name: 'create_organization',
30
+ httpMethod: 'post',
31
+ path: '/organizations',
32
+ requestBody: { kind: 'model', name: 'CreateOrgReq' },
33
+ }),
34
+ ],
35
+ },
36
+ ],
37
+ [
38
+ {
39
+ name: 'CreateOrgReq',
40
+ fields: [makeStringField('name', 'Foo Corp'), makeStringField('external_id', 'ext_123')],
41
+ },
42
+ ],
43
+ );
44
+ expect(content).toContain('$workos->organizations()->createOrganization(');
45
+ expect(content).toContain("name: 'Foo Corp'");
46
+ expect(content).toContain("externalId: 'ext_123'");
47
+ });
48
+ });
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Model, Service } from '@workos/oagen';
3
+ import { runSnippetEmitters } from '@workos/oagen';
4
+ import { pythonSnippetEmitter } from '../../src/snippets/python.js';
5
+ import { makeCtx, makeOp, makeSpec, makeStringField } from './_helpers.js';
6
+
7
+ function runPython(services: Service[], models: Model[] = []): string {
8
+ const results = runSnippetEmitters([pythonSnippetEmitter], makeCtx(makeSpec(services, models)));
9
+ return results[0]!.content;
10
+ }
11
+
12
+ describe('snippets/python', () => {
13
+ it('renders a no-arg call with the WorkOSClient constructor', () => {
14
+ const content = runPython([{ name: 'Organizations', operations: [makeOp({ name: 'list_organizations' })] }]);
15
+ expect(content).toContain('from workos import WorkOSClient');
16
+ expect(content).toContain('client = WorkOSClient(api_key="sk_example_123456789", client_id="client_123456789")');
17
+ expect(content).toContain('client.organizations.list_organizations()');
18
+ });
19
+
20
+ it('renames Python builtin path params with a trailing underscore', () => {
21
+ const content = runPython([
22
+ {
23
+ name: 'Organizations',
24
+ operations: [
25
+ makeOp({
26
+ name: 'get_organization',
27
+ path: '/organizations/{id}',
28
+ pathParams: [
29
+ {
30
+ name: 'id',
31
+ type: { kind: 'primitive', type: 'string' },
32
+ required: true,
33
+ example: 'org_123',
34
+ },
35
+ ],
36
+ }),
37
+ ],
38
+ },
39
+ ]);
40
+ // Python builtins ('id', 'type') get an underscore via safeParamName,
41
+ // so the snippet exposes `id_=` instead of shadowing the builtin.
42
+ expect(content).toContain('client.organizations.get_organization(id_="org_123")');
43
+ });
44
+
45
+ it('expands required body fields as kwargs', () => {
46
+ const content = runPython(
47
+ [
48
+ {
49
+ name: 'Organizations',
50
+ operations: [
51
+ makeOp({
52
+ name: 'create_organization',
53
+ httpMethod: 'post',
54
+ path: '/organizations',
55
+ requestBody: { kind: 'model', name: 'CreateOrgReq' },
56
+ }),
57
+ ],
58
+ },
59
+ ],
60
+ [
61
+ {
62
+ name: 'CreateOrgReq',
63
+ fields: [
64
+ makeStringField('name', 'Foo Corp'),
65
+ makeStringField('description', undefined, false), // optional
66
+ ],
67
+ },
68
+ ],
69
+ );
70
+ expect(content).toContain('name="Foo Corp"');
71
+ expect(content).not.toContain('description');
72
+ });
73
+ });