@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,202 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateTests } from '../../src/dotnet/tests.js';
|
|
3
|
+
import { primeEnumAliases } from '../../src/dotnet/enums.js';
|
|
4
|
+
import type { EmitterContext, ApiSpec, Service, Model } from '@workos/oagen';
|
|
5
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
6
|
+
|
|
7
|
+
const models: Model[] = [
|
|
8
|
+
{
|
|
9
|
+
name: 'Organization',
|
|
10
|
+
fields: [
|
|
11
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
12
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const services: Service[] = [
|
|
18
|
+
{
|
|
19
|
+
name: 'Organizations',
|
|
20
|
+
operations: [
|
|
21
|
+
{
|
|
22
|
+
name: 'getOrganization',
|
|
23
|
+
httpMethod: 'get',
|
|
24
|
+
path: '/organizations/{id}',
|
|
25
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
26
|
+
queryParams: [],
|
|
27
|
+
headerParams: [],
|
|
28
|
+
response: { kind: 'model', name: 'Organization' },
|
|
29
|
+
errors: [],
|
|
30
|
+
injectIdempotencyKey: false,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'deleteOrganization',
|
|
34
|
+
httpMethod: 'delete',
|
|
35
|
+
path: '/organizations/{id}',
|
|
36
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
37
|
+
queryParams: [],
|
|
38
|
+
headerParams: [],
|
|
39
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
40
|
+
errors: [],
|
|
41
|
+
injectIdempotencyKey: false,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const spec: ApiSpec = {
|
|
48
|
+
name: 'TestAPI',
|
|
49
|
+
version: '1.0.0',
|
|
50
|
+
baseUrl: 'https://api.workos.com',
|
|
51
|
+
services,
|
|
52
|
+
models,
|
|
53
|
+
enums: [],
|
|
54
|
+
sdk: defaultSdkBehavior(),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const ctx: EmitterContext = {
|
|
58
|
+
namespace: 'workos',
|
|
59
|
+
namespacePascal: 'WorkOS',
|
|
60
|
+
spec,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe('dotnet/tests', () => {
|
|
64
|
+
it('generates per-service test files', () => {
|
|
65
|
+
primeEnumAliases([]);
|
|
66
|
+
const files = generateTests(spec, ctx);
|
|
67
|
+
const testFile = files.find((f) => f.path === 'Tests/OrganizationsServiceTest.cs');
|
|
68
|
+
expect(testFile).toBeDefined();
|
|
69
|
+
|
|
70
|
+
const content = testFile!.content;
|
|
71
|
+
expect(content).toContain('namespace WorkOSTests');
|
|
72
|
+
expect(content).toContain('public class OrganizationsServiceTest');
|
|
73
|
+
expect(content).toContain('HttpMock');
|
|
74
|
+
expect(content).toContain('[Fact]');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('generates GET operation test with fixture', () => {
|
|
78
|
+
primeEnumAliases([]);
|
|
79
|
+
const files = generateTests(spec, ctx);
|
|
80
|
+
const testFile = files.find((f) => f.path === 'Tests/OrganizationsServiceTest.cs')!;
|
|
81
|
+
const content = testFile.content;
|
|
82
|
+
|
|
83
|
+
expect(content).toContain('TestGet');
|
|
84
|
+
expect(content).toContain('ReadAllText');
|
|
85
|
+
expect(content).toContain('MockResponse');
|
|
86
|
+
expect(content).toContain('Assert.NotNull(result)');
|
|
87
|
+
expect(content).toContain('AssertRequestWasMade');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('generates DELETE operation test', () => {
|
|
91
|
+
primeEnumAliases([]);
|
|
92
|
+
const files = generateTests(spec, ctx);
|
|
93
|
+
const testFile = files.find((f) => f.path === 'Tests/OrganizationsServiceTest.cs')!;
|
|
94
|
+
const content = testFile.content;
|
|
95
|
+
|
|
96
|
+
expect(content).toContain('TestDelete');
|
|
97
|
+
expect(content).toContain('HttpMethod.Delete');
|
|
98
|
+
expect(content).toContain('NoContent');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('generates error tests (401, 404, 422, 429, 500)', () => {
|
|
102
|
+
primeEnumAliases([]);
|
|
103
|
+
const files = generateTests(spec, ctx);
|
|
104
|
+
const testFile = files.find((f) => f.path === 'Tests/OrganizationsServiceTest.cs')!;
|
|
105
|
+
const content = testFile.content;
|
|
106
|
+
|
|
107
|
+
expect(content).toContain('TestError401');
|
|
108
|
+
expect(content).toContain('AuthenticationException');
|
|
109
|
+
expect(content).toContain('TestError404');
|
|
110
|
+
expect(content).toContain('NotFoundException');
|
|
111
|
+
expect(content).toContain('TestError422');
|
|
112
|
+
expect(content).toContain('UnprocessableEntityException');
|
|
113
|
+
expect(content).toContain('TestError429');
|
|
114
|
+
expect(content).toContain('RateLimitExceededException');
|
|
115
|
+
expect(content).toContain('TestError500');
|
|
116
|
+
expect(content).toContain('ServerException');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('generates fixture JSON files', () => {
|
|
120
|
+
primeEnumAliases([]);
|
|
121
|
+
const files = generateTests(spec, ctx);
|
|
122
|
+
const fixture = files.find((f) => f.path === 'testdata/organization.json');
|
|
123
|
+
expect(fixture).toBeDefined();
|
|
124
|
+
expect(fixture!.headerPlacement).toBe('skip');
|
|
125
|
+
|
|
126
|
+
const data = JSON.parse(fixture!.content);
|
|
127
|
+
expect(data).toHaveProperty('id');
|
|
128
|
+
expect(data).toHaveProperty('name');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('does not generate static test infrastructure', () => {
|
|
132
|
+
primeEnumAliases([]);
|
|
133
|
+
const files = generateTests(spec, ctx);
|
|
134
|
+
const paths = files.map((f) => f.path);
|
|
135
|
+
|
|
136
|
+
// HttpMock and other static helpers are @oagen-ignore-file in target SDK
|
|
137
|
+
expect(paths.find((p) => p.includes('HttpMock'))).toBeUndefined();
|
|
138
|
+
expect(paths.find((p) => p.includes('WorkOSClientTest'))).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('generates auto-pagination tests for paginated operations', () => {
|
|
142
|
+
const paginatedModels: Model[] = [
|
|
143
|
+
...models,
|
|
144
|
+
{
|
|
145
|
+
name: 'OrganizationList',
|
|
146
|
+
fields: [
|
|
147
|
+
{
|
|
148
|
+
name: 'data',
|
|
149
|
+
type: { kind: 'array', items: { kind: 'model', name: 'Organization' } },
|
|
150
|
+
required: true,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'list_metadata',
|
|
154
|
+
type: { kind: 'model', name: 'ListMetadata' },
|
|
155
|
+
required: true,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const paginatedServices: Service[] = [
|
|
162
|
+
{
|
|
163
|
+
name: 'Organizations',
|
|
164
|
+
operations: [
|
|
165
|
+
{
|
|
166
|
+
name: 'listOrganizations',
|
|
167
|
+
httpMethod: 'get',
|
|
168
|
+
path: '/organizations',
|
|
169
|
+
pathParams: [],
|
|
170
|
+
queryParams: [],
|
|
171
|
+
headerParams: [],
|
|
172
|
+
response: { kind: 'model', name: 'OrganizationList' },
|
|
173
|
+
errors: [],
|
|
174
|
+
injectIdempotencyKey: false,
|
|
175
|
+
pagination: {
|
|
176
|
+
strategy: 'cursor',
|
|
177
|
+
param: 'after',
|
|
178
|
+
dataPath: 'data',
|
|
179
|
+
itemType: { kind: 'model', name: 'Organization' },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
const paginatedSpec: ApiSpec = {
|
|
187
|
+
...spec,
|
|
188
|
+
services: paginatedServices,
|
|
189
|
+
models: paginatedModels,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
primeEnumAliases([]);
|
|
193
|
+
const files = generateTests(paginatedSpec, { ...ctx, spec: paginatedSpec });
|
|
194
|
+
const testFile = files.find((f) => f.path === 'Tests/OrganizationsServiceTest.cs')!;
|
|
195
|
+
const content = testFile.content;
|
|
196
|
+
|
|
197
|
+
// Auto-paging test
|
|
198
|
+
expect(content).toContain('AutoPagingAsync');
|
|
199
|
+
expect(content).toContain('MockSequentialResponses');
|
|
200
|
+
expect(content).toContain('await foreach');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verify the public entrypoint exports the plugin bundle
|
|
5
|
+
* and all intended direct symbols (emitters + extractors).
|
|
6
|
+
*/
|
|
7
|
+
describe('public entrypoint (@workos/oagen-emitters)', () => {
|
|
8
|
+
it('exports workosEmittersPlugin', async () => {
|
|
9
|
+
const mod = await import('../src/index.js');
|
|
10
|
+
expect(mod.workosEmittersPlugin).toBeDefined();
|
|
11
|
+
expect(mod.workosEmittersPlugin.emitters).toBeDefined();
|
|
12
|
+
expect(mod.workosEmittersPlugin.extractors).toBeDefined();
|
|
13
|
+
expect(mod.workosEmittersPlugin.smokeRunners).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('exports all individual emitters', async () => {
|
|
17
|
+
const mod = await import('../src/index.js');
|
|
18
|
+
const expectedEmitters = [
|
|
19
|
+
'nodeEmitter',
|
|
20
|
+
'pythonEmitter',
|
|
21
|
+
'phpEmitter',
|
|
22
|
+
'goEmitter',
|
|
23
|
+
'dotnetEmitter',
|
|
24
|
+
'kotlinEmitter',
|
|
25
|
+
'rubyEmitter',
|
|
26
|
+
];
|
|
27
|
+
for (const name of expectedEmitters) {
|
|
28
|
+
expect(mod).toHaveProperty(name);
|
|
29
|
+
expect((mod as Record<string, unknown>)[name]).toHaveProperty('language');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('exports all individual extractors', async () => {
|
|
34
|
+
const mod = await import('../src/index.js');
|
|
35
|
+
const expectedExtractors = [
|
|
36
|
+
'nodeExtractor',
|
|
37
|
+
'rubyExtractor',
|
|
38
|
+
'pythonExtractor',
|
|
39
|
+
'phpExtractor',
|
|
40
|
+
'goExtractor',
|
|
41
|
+
'rustExtractor',
|
|
42
|
+
'kotlinExtractor',
|
|
43
|
+
'dotnetExtractor',
|
|
44
|
+
'elixirExtractor',
|
|
45
|
+
];
|
|
46
|
+
for (const name of expectedExtractors) {
|
|
47
|
+
expect(mod).toHaveProperty(name);
|
|
48
|
+
expect((mod as Record<string, unknown>)[name]).toHaveProperty('language');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('plugin bundle emitters match individual emitter exports', async () => {
|
|
53
|
+
const mod = await import('../src/index.js');
|
|
54
|
+
const pluginLanguages = mod.workosEmittersPlugin.emitters!.map((e) => e.language);
|
|
55
|
+
const directEmitters = [
|
|
56
|
+
mod.nodeEmitter,
|
|
57
|
+
mod.pythonEmitter,
|
|
58
|
+
mod.phpEmitter,
|
|
59
|
+
mod.goEmitter,
|
|
60
|
+
mod.dotnetEmitter,
|
|
61
|
+
mod.kotlinEmitter,
|
|
62
|
+
mod.rubyEmitter,
|
|
63
|
+
];
|
|
64
|
+
for (const emitter of directEmitters) {
|
|
65
|
+
expect(pluginLanguages).toContain(emitter.language);
|
|
66
|
+
}
|
|
67
|
+
expect(pluginLanguages).toHaveLength(directEmitters.length);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('plugin bundle extractors match individual extractor exports', async () => {
|
|
71
|
+
const mod = await import('../src/index.js');
|
|
72
|
+
const pluginLanguages = mod.workosEmittersPlugin.extractors!.map((e) => e.language);
|
|
73
|
+
const directExtractors = [
|
|
74
|
+
mod.nodeExtractor,
|
|
75
|
+
mod.rubyExtractor,
|
|
76
|
+
mod.pythonExtractor,
|
|
77
|
+
mod.phpExtractor,
|
|
78
|
+
mod.goExtractor,
|
|
79
|
+
mod.rustExtractor,
|
|
80
|
+
mod.kotlinExtractor,
|
|
81
|
+
mod.dotnetExtractor,
|
|
82
|
+
mod.elixirExtractor,
|
|
83
|
+
];
|
|
84
|
+
for (const extractor of directExtractors) {
|
|
85
|
+
expect(pluginLanguages).toContain(extractor.language);
|
|
86
|
+
}
|
|
87
|
+
expect(pluginLanguages).toHaveLength(directExtractors.length);
|
|
88
|
+
});
|
|
89
|
+
});
|
package/test/go/client.test.ts
CHANGED
|
@@ -55,9 +55,9 @@ describe('go/client', () => {
|
|
|
55
55
|
const content = workosFile.content;
|
|
56
56
|
|
|
57
57
|
expect(content).toContain('package workos');
|
|
58
|
-
expect(content).toContain('organizations *
|
|
58
|
+
expect(content).toContain('organizations *OrganizationService');
|
|
59
59
|
expect(content).toContain('func NewClient(apiKey string, opts ...ClientOption) *Client {');
|
|
60
|
-
expect(content).toContain('func (c *Client) Organizations() *
|
|
60
|
+
expect(content).toContain('func (c *Client) Organizations() *OrganizationService {');
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
it('does not emit static options or HTTP infrastructure', () => {
|
|
@@ -84,9 +84,9 @@ describe('go/client', () => {
|
|
|
84
84
|
const workosFile = files.find((f) => f.path === 'workos.go')!;
|
|
85
85
|
const content = workosFile.content;
|
|
86
86
|
|
|
87
|
-
expect(content).toContain('apiKeys *
|
|
88
|
-
expect(content).toContain('sso *
|
|
89
|
-
expect(content).toContain('func (c *Client) APIKeys() *
|
|
90
|
-
expect(content).toContain('func (c *Client) SSO() *
|
|
87
|
+
expect(content).toContain('apiKeys *APIKeyService');
|
|
88
|
+
expect(content).toContain('sso *SSOService');
|
|
89
|
+
expect(content).toContain('func (c *Client) APIKeys() *APIKeyService {');
|
|
90
|
+
expect(content).toContain('func (c *Client) SSO() *SSOService {');
|
|
91
91
|
});
|
|
92
92
|
});
|
|
@@ -114,12 +114,12 @@ describe('go/resources', () => {
|
|
|
114
114
|
expect(files.length).toBeGreaterThanOrEqual(1);
|
|
115
115
|
const content = files[0].content;
|
|
116
116
|
expect(content).toContain('package workos');
|
|
117
|
-
expect(content).toContain('type
|
|
117
|
+
expect(content).toContain('type OrganizationService struct {');
|
|
118
118
|
expect(content).toContain('Limit *int `url:"limit,omitempty" json:"-"`');
|
|
119
|
-
expect(content).toContain('func (s *
|
|
120
|
-
expect(content).toContain('func (s *
|
|
121
|
-
expect(content).toContain('func (s *
|
|
122
|
-
expect(content).toContain('func (s *
|
|
119
|
+
expect(content).toContain('func (s *OrganizationService) List(');
|
|
120
|
+
expect(content).toContain('func (s *OrganizationService) Get(');
|
|
121
|
+
expect(content).toContain('func (s *OrganizationService) Create(');
|
|
122
|
+
expect(content).toContain('func (s *OrganizationService) Delete(');
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
it('generates path interpolation with fmt.Sprintf', () => {
|
|
@@ -139,7 +139,7 @@ describe('go/resources', () => {
|
|
|
139
139
|
const spec = makeSpec(services);
|
|
140
140
|
const files = generateResources(services, makeCtx(spec));
|
|
141
141
|
const content = files[0].content;
|
|
142
|
-
expect(content).toContain('fmt.Sprintf("/users/%s", id)');
|
|
142
|
+
expect(content).toContain('fmt.Sprintf("/users/%s", url.PathEscape(id))');
|
|
143
143
|
});
|
|
144
144
|
|
|
145
145
|
it('generates paginated methods returning Iterator', () => {
|
|
@@ -165,7 +165,7 @@ describe('go/resources', () => {
|
|
|
165
165
|
const files = generateResources(services, makeCtx(spec));
|
|
166
166
|
const content = files[0].content;
|
|
167
167
|
expect(content).toContain('*Iterator[User]');
|
|
168
|
-
expect(content).toContain('newIterator[User](ctx, s.client, "GET", "/users", nil, "after", "data", opts
|
|
168
|
+
expect(content).toContain('newIterator[User](ctx, s.client, "GET", "/users", nil, "after", "data", opts,');
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
it('generates delete methods returning error', () => {
|
|
@@ -405,4 +405,153 @@ describe('go/resources', () => {
|
|
|
405
405
|
expect(content).toContain('Body interface{} `json:"-"`');
|
|
406
406
|
expect(content).toContain('request(ctx, "POST", "/connect/applications", nil, params.Body, &result, opts)');
|
|
407
407
|
});
|
|
408
|
+
|
|
409
|
+
describe('mutually-exclusive parameter groups', () => {
|
|
410
|
+
const groupedOp = makeOp({
|
|
411
|
+
name: 'listResources',
|
|
412
|
+
httpMethod: 'get',
|
|
413
|
+
path: '/authorization/organization_memberships/{organization_membership_id}/resources',
|
|
414
|
+
pathParams: [{ name: 'organization_membership_id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
415
|
+
queryParams: [
|
|
416
|
+
{ name: 'before', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
417
|
+
{ name: 'after', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
418
|
+
{ name: 'limit', type: { kind: 'primitive', type: 'integer' }, required: false },
|
|
419
|
+
{ name: 'order', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
420
|
+
{ name: 'permission_slug', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
421
|
+
{ name: 'parent_resource_id', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
422
|
+
{ name: 'parent_resource_type_slug', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
423
|
+
{ name: 'parent_resource_external_id', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
424
|
+
],
|
|
425
|
+
parameterGroups: [
|
|
426
|
+
{
|
|
427
|
+
name: 'parent_resource',
|
|
428
|
+
optional: false,
|
|
429
|
+
variants: [
|
|
430
|
+
{
|
|
431
|
+
name: 'by_id',
|
|
432
|
+
parameters: [
|
|
433
|
+
{ name: 'parent_resource_id', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: 'by_external_id',
|
|
438
|
+
parameters: [
|
|
439
|
+
{ name: 'parent_resource_type_slug', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
440
|
+
{ name: 'parent_resource_external_id', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
pagination: {
|
|
447
|
+
strategy: 'cursor' as const,
|
|
448
|
+
param: 'after',
|
|
449
|
+
dataPath: 'data',
|
|
450
|
+
itemType: { kind: 'model' as const, name: 'AuthorizationResource' },
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
function makeGroupedServices(): Service[] {
|
|
455
|
+
return [{ name: 'Authorization', operations: [groupedOp] }];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
it('generates a sealed interface for the parameter group', () => {
|
|
459
|
+
const services = makeGroupedServices();
|
|
460
|
+
const spec = makeSpec(services);
|
|
461
|
+
const files = generateResources(services, makeCtx(spec));
|
|
462
|
+
const content = files[0].content;
|
|
463
|
+
|
|
464
|
+
// Interface declaration with unexported marker + applyToQuery
|
|
465
|
+
expect(content).toContain('type AuthorizationParentResource interface {');
|
|
466
|
+
expect(content).toContain('isAuthorizationParentResource()');
|
|
467
|
+
expect(content).toContain('applyToQuery(url.Values)');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('generates variant structs with shortened field names', () => {
|
|
471
|
+
const services = makeGroupedServices();
|
|
472
|
+
const spec = makeSpec(services);
|
|
473
|
+
const files = generateResources(services, makeCtx(spec));
|
|
474
|
+
const content = files[0].content;
|
|
475
|
+
|
|
476
|
+
// ByID variant
|
|
477
|
+
expect(content).toContain('type AuthorizationParentResourceByID struct {');
|
|
478
|
+
expect(content).toContain('\tID string');
|
|
479
|
+
|
|
480
|
+
// ByExternalID variant
|
|
481
|
+
expect(content).toContain('type AuthorizationParentResourceByExternalID struct {');
|
|
482
|
+
expect(content).toContain('\tTypeSlug string');
|
|
483
|
+
expect(content).toContain('\tExternalID string');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('generates marker methods on each variant', () => {
|
|
487
|
+
const services = makeGroupedServices();
|
|
488
|
+
const spec = makeSpec(services);
|
|
489
|
+
const files = generateResources(services, makeCtx(spec));
|
|
490
|
+
const content = files[0].content;
|
|
491
|
+
|
|
492
|
+
expect(content).toContain('func (p AuthorizationParentResourceByID) isAuthorizationParentResource()');
|
|
493
|
+
expect(content).toContain('func (p AuthorizationParentResourceByExternalID) isAuthorizationParentResource()');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('generates applyToQuery methods using original wire names', () => {
|
|
497
|
+
const services = makeGroupedServices();
|
|
498
|
+
const spec = makeSpec(services);
|
|
499
|
+
const files = generateResources(services, makeCtx(spec));
|
|
500
|
+
const content = files[0].content;
|
|
501
|
+
|
|
502
|
+
// ByID variant sets parent_resource_id
|
|
503
|
+
expect(content).toContain('func (p AuthorizationParentResourceByID) applyToQuery(v url.Values)');
|
|
504
|
+
expect(content).toContain('v.Set("parent_resource_id", p.ID)');
|
|
505
|
+
|
|
506
|
+
// ByExternalID variant sets both wire-name params
|
|
507
|
+
expect(content).toContain('func (p AuthorizationParentResourceByExternalID) applyToQuery(v url.Values)');
|
|
508
|
+
expect(content).toContain('v.Set("parent_resource_type_slug", p.TypeSlug)');
|
|
509
|
+
expect(content).toContain('v.Set("parent_resource_external_id", p.ExternalID)');
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('params struct uses group interface instead of flat pointers', () => {
|
|
513
|
+
const services = makeGroupedServices();
|
|
514
|
+
const spec = makeSpec(services);
|
|
515
|
+
const files = generateResources(services, makeCtx(spec));
|
|
516
|
+
const content = files[0].content;
|
|
517
|
+
|
|
518
|
+
// Should have the group field
|
|
519
|
+
expect(content).toContain('ParentResource AuthorizationParentResource `url:"-" json:"-"`');
|
|
520
|
+
|
|
521
|
+
// Should NOT have the flat pointer fields
|
|
522
|
+
expect(content).not.toMatch(/ParentResourceID\s+\*string/);
|
|
523
|
+
expect(content).not.toMatch(/ParentResourceTypeSlug\s+\*string/);
|
|
524
|
+
expect(content).not.toMatch(/ParentResourceExternalID\s+\*string/);
|
|
525
|
+
|
|
526
|
+
// Should still have non-grouped params
|
|
527
|
+
expect(content).toContain('PermissionSlug string');
|
|
528
|
+
expect(content).toContain('PaginationParams');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('method body builds url.Values and calls applyToQuery', () => {
|
|
532
|
+
const services = makeGroupedServices();
|
|
533
|
+
const spec = makeSpec(services);
|
|
534
|
+
const files = generateResources(services, makeCtx(spec));
|
|
535
|
+
const content = files[0].content;
|
|
536
|
+
|
|
537
|
+
// Should build url.Values manually
|
|
538
|
+
expect(content).toContain('query := url.Values{}');
|
|
539
|
+
// Should encode the non-grouped required param
|
|
540
|
+
expect(content).toContain('query.Set("permission_slug", params.PermissionSlug)');
|
|
541
|
+
// Should call applyToQuery on the group
|
|
542
|
+
expect(content).toContain('params.ParentResource.applyToQuery(query)');
|
|
543
|
+
// Should pass query to the iterator (not params)
|
|
544
|
+
expect(content).toContain('newIterator[AuthorizationResource](ctx, s.client, "GET"');
|
|
545
|
+
expect(content).toContain(', query, "after", "data", opts, nil)');
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('imports net/url when parameter groups are present', () => {
|
|
549
|
+
const services = makeGroupedServices();
|
|
550
|
+
const spec = makeSpec(services);
|
|
551
|
+
const files = generateResources(services, makeCtx(spec));
|
|
552
|
+
const content = files[0].content;
|
|
553
|
+
|
|
554
|
+
expect(content).toContain('"net/url"');
|
|
555
|
+
});
|
|
556
|
+
});
|
|
408
557
|
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateModels } from '../../src/kotlin/models.js';
|
|
3
|
+
import { generateEnums } from '../../src/kotlin/enums.js';
|
|
4
|
+
import type { EmitterContext, ApiSpec, Model } from '@workos/oagen';
|
|
5
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
6
|
+
|
|
7
|
+
const emptySpec: ApiSpec = {
|
|
8
|
+
name: 'Test',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
baseUrl: '',
|
|
11
|
+
services: [],
|
|
12
|
+
models: [],
|
|
13
|
+
enums: [],
|
|
14
|
+
sdk: defaultSdkBehavior(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ctx: EmitterContext = {
|
|
18
|
+
namespace: 'workos',
|
|
19
|
+
namespacePascal: 'WorkOS',
|
|
20
|
+
spec: emptySpec,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('kotlin/models', () => {
|
|
24
|
+
it('returns empty for no models', () => {
|
|
25
|
+
generateEnums([], ctx);
|
|
26
|
+
expect(generateModels([], ctx)).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('generates a Kotlin data class with Jackson annotations', () => {
|
|
30
|
+
const models: Model[] = [
|
|
31
|
+
{
|
|
32
|
+
name: 'Organization',
|
|
33
|
+
fields: [
|
|
34
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
35
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
36
|
+
{
|
|
37
|
+
name: 'created_at',
|
|
38
|
+
type: { kind: 'primitive', type: 'string', format: 'date-time' },
|
|
39
|
+
required: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'external_id',
|
|
43
|
+
type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
|
|
44
|
+
required: false,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
generateEnums([], ctx);
|
|
51
|
+
const files = generateModels(models, { ...ctx, spec: { ...emptySpec, models } });
|
|
52
|
+
|
|
53
|
+
expect(files.length).toBeGreaterThanOrEqual(1);
|
|
54
|
+
const modelFile = files.find((f) => f.path.includes('Organization.kt'))!;
|
|
55
|
+
expect(modelFile).toBeDefined();
|
|
56
|
+
|
|
57
|
+
const content = modelFile.content;
|
|
58
|
+
expect(content).toContain('data class Organization');
|
|
59
|
+
expect(content).toContain('@JsonProperty("id")');
|
|
60
|
+
expect(content).not.toContain('@JvmField');
|
|
61
|
+
expect(content).toContain('OffsetDateTime');
|
|
62
|
+
expect(content).toContain('externalId: String?');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('skips list wrapper and list metadata models', () => {
|
|
66
|
+
const models: Model[] = [
|
|
67
|
+
{
|
|
68
|
+
name: 'Organization',
|
|
69
|
+
fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'OrganizationList',
|
|
73
|
+
fields: [
|
|
74
|
+
{
|
|
75
|
+
name: 'data',
|
|
76
|
+
type: { kind: 'array', items: { kind: 'model', name: 'Organization' } },
|
|
77
|
+
required: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'list_metadata',
|
|
81
|
+
type: { kind: 'model', name: 'ListMetadata' },
|
|
82
|
+
required: true,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'ListMetadata',
|
|
88
|
+
fields: [
|
|
89
|
+
{ name: 'before', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
90
|
+
{ name: 'after', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
generateEnums([], ctx);
|
|
96
|
+
const files = generateModels(models, { ...ctx, spec: { ...emptySpec, models } });
|
|
97
|
+
const filePaths = files.map((f) => f.path);
|
|
98
|
+
|
|
99
|
+
expect(filePaths.some((p) => p.includes('Organization.kt') && !p.includes('List'))).toBe(true);
|
|
100
|
+
expect(filePaths.some((p) => p.includes('OrganizationList.kt'))).toBe(false);
|
|
101
|
+
expect(filePaths.some((p) => p.includes('ListMetadata.kt'))).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('deduplicates structurally identical models preferring shorter names', () => {
|
|
105
|
+
const models: Model[] = [
|
|
106
|
+
{
|
|
107
|
+
name: 'EmailChangeConfirmationUser',
|
|
108
|
+
fields: [
|
|
109
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
110
|
+
{ name: 'email', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'User',
|
|
115
|
+
fields: [
|
|
116
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
117
|
+
{ name: 'email', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
generateEnums([], ctx);
|
|
123
|
+
const files = generateModels(models, { ...ctx, spec: { ...emptySpec, models } });
|
|
124
|
+
|
|
125
|
+
// User should be the canonical (shorter name) — a data class
|
|
126
|
+
const userFile = files.find((f) => f.path.includes('/User.kt'))!;
|
|
127
|
+
expect(userFile).toBeDefined();
|
|
128
|
+
expect(userFile.content).toContain('data class User');
|
|
129
|
+
|
|
130
|
+
// EmailChangeConfirmationUser should be the typealias
|
|
131
|
+
const aliasFile = files.find((f) => f.path.includes('/EmailChangeConfirmationUser.kt'))!;
|
|
132
|
+
expect(aliasFile).toBeDefined();
|
|
133
|
+
expect(aliasFile.content).toContain('typealias EmailChangeConfirmationUser = User');
|
|
134
|
+
});
|
|
135
|
+
});
|