@workos/oagen-emitters 0.2.1 → 0.4.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +129 -0
- package/dist/index.d.mts +13 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +14549 -3385
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/dotnet.md +336 -0
- 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 +328 -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 +45 -12
- package/smoke/sdk-go.ts +116 -42
- package/smoke/sdk-php.ts +28 -26
- package/smoke/sdk-python.ts +5 -2
- 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 +246 -0
- package/src/dotnet/manifest.ts +36 -0
- package/src/dotnet/models.ts +344 -0
- package/src/dotnet/naming.ts +330 -0
- package/src/dotnet/resources.ts +622 -0
- package/src/dotnet/tests.ts +693 -0
- package/src/dotnet/type-map.ts +201 -0
- package/src/dotnet/wrappers.ts +186 -0
- 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 +84 -0
- package/src/go/manifest.ts +36 -0
- package/src/go/models.ts +254 -0
- package/src/go/naming.ts +179 -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 +4 -0
- package/src/kotlin/client.ts +53 -0
- package/src/kotlin/enums.ts +162 -0
- package/src/kotlin/index.ts +92 -0
- package/src/kotlin/manifest.ts +55 -0
- package/src/kotlin/models.ts +395 -0
- package/src/kotlin/naming.ts +223 -0
- package/src/kotlin/overrides.ts +25 -0
- package/src/kotlin/resources.ts +667 -0
- package/src/kotlin/tests.ts +1019 -0
- package/src/kotlin/type-map.ts +123 -0
- package/src/kotlin/wrappers.ts +168 -0
- package/src/node/client.ts +128 -115
- package/src/node/enums.ts +9 -0
- package/src/node/errors.ts +37 -232
- package/src/node/field-plan.ts +726 -0
- package/src/node/fixtures.ts +9 -1
- package/src/node/index.ts +3 -9
- package/src/node/models.ts +178 -21
- package/src/node/naming.ts +49 -111
- package/src/node/resources.ts +527 -397
- package/src/node/sdk-errors.ts +41 -0
- package/src/node/tests.ts +69 -19
- package/src/node/type-map.ts +4 -2
- package/src/node/utils.ts +13 -71
- package/src/node/wrappers.ts +151 -0
- package/src/php/client.ts +179 -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 +279 -0
- package/src/php/resources.ts +636 -0
- package/src/php/tests.ts +609 -0
- package/src/php/type-map.ts +90 -0
- package/src/php/utils.ts +18 -0
- package/src/php/wrappers.ts +152 -0
- package/src/python/client.ts +345 -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 +189 -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 +472 -0
- package/src/shared/naming-utils.ts +154 -0
- package/src/shared/non-spec-services.ts +54 -0
- package/src/shared/resolved-ops.ts +109 -0
- package/src/shared/wrapper-utils.ts +70 -0
- 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 +260 -0
- package/test/dotnet/resources.test.ts +255 -0
- package/test/dotnet/tests.test.ts +202 -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/kotlin/models.test.ts +135 -0
- package/test/kotlin/tests.test.ts +176 -0
- package/test/node/client.test.ts +92 -12
- package/test/node/enums.test.ts +2 -0
- package/test/node/errors.test.ts +2 -41
- package/test/node/models.test.ts +2 -0
- package/test/node/naming.test.ts +23 -0
- package/test/node/resources.test.ts +315 -84
- package/test/node/serializers.test.ts +3 -1
- package/test/node/type-map.test.ts +11 -0
- package/test/php/client.test.ts +95 -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 +682 -0
- package/test/php/tests.test.ts +185 -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 -746
|
@@ -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('AuthenticationError');
|
|
109
|
+
expect(content).toContain('TestError404');
|
|
110
|
+
expect(content).toContain('NotFoundError');
|
|
111
|
+
expect(content).toContain('TestError422');
|
|
112
|
+
expect(content).toContain('UnprocessableEntityError');
|
|
113
|
+
expect(content).toContain('TestError429');
|
|
114
|
+
expect(content).toContain('RateLimitExceededError');
|
|
115
|
+
expect(content).toContain('TestError500');
|
|
116
|
+
expect(content).toContain('ServerError');
|
|
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,92 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { EmitterContext, ApiSpec, Service } from '@workos/oagen';
|
|
3
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
4
|
+
import { generateClient } from '../../src/go/client.js';
|
|
5
|
+
|
|
6
|
+
function makeSpec(services: Service[]): ApiSpec {
|
|
7
|
+
return {
|
|
8
|
+
name: 'Test',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
baseUrl: 'https://api.workos.com',
|
|
11
|
+
services,
|
|
12
|
+
models: [],
|
|
13
|
+
enums: [],
|
|
14
|
+
sdk: defaultSdkBehavior(),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeCtx(spec: ApiSpec): EmitterContext {
|
|
19
|
+
return {
|
|
20
|
+
namespace: 'workos',
|
|
21
|
+
namespacePascal: 'WorkOS',
|
|
22
|
+
spec,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('go/client', () => {
|
|
27
|
+
it('generates only workos.go', () => {
|
|
28
|
+
const spec = makeSpec([]);
|
|
29
|
+
const files = generateClient(spec, makeCtx(spec));
|
|
30
|
+
expect(files.length).toBe(1);
|
|
31
|
+
expect(files[0].path).toBe('workos.go');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('generates Client struct with service fields', () => {
|
|
35
|
+
const spec = makeSpec([
|
|
36
|
+
{
|
|
37
|
+
name: 'Organizations',
|
|
38
|
+
operations: [
|
|
39
|
+
{
|
|
40
|
+
name: 'listOrganizations',
|
|
41
|
+
httpMethod: 'get',
|
|
42
|
+
path: '/organizations',
|
|
43
|
+
pathParams: [],
|
|
44
|
+
queryParams: [],
|
|
45
|
+
headerParams: [],
|
|
46
|
+
response: { kind: 'model', name: 'Organization' },
|
|
47
|
+
errors: [],
|
|
48
|
+
injectIdempotencyKey: false,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
const files = generateClient(spec, makeCtx(spec));
|
|
54
|
+
const workosFile = files.find((f) => f.path === 'workos.go')!;
|
|
55
|
+
const content = workosFile.content;
|
|
56
|
+
|
|
57
|
+
expect(content).toContain('package workos');
|
|
58
|
+
expect(content).toContain('organizations *organizationService');
|
|
59
|
+
expect(content).toContain('func NewClient(apiKey string, opts ...ClientOption) *Client {');
|
|
60
|
+
expect(content).toContain('func (c *Client) Organizations() *organizationService {');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('does not emit static options or HTTP infrastructure', () => {
|
|
64
|
+
const spec = makeSpec([]);
|
|
65
|
+
const files = generateClient(spec, makeCtx(spec));
|
|
66
|
+
const workosFile = files.find((f) => f.path === 'workos.go')!;
|
|
67
|
+
const content = workosFile.content;
|
|
68
|
+
|
|
69
|
+
// These definitions are now in hand-maintained options.go
|
|
70
|
+
expect(content).not.toContain('type ClientOption func(*Client)');
|
|
71
|
+
expect(content).not.toContain('func WithBaseURL');
|
|
72
|
+
expect(content).not.toContain('type RequestOption');
|
|
73
|
+
expect(content).not.toContain('type requestConfig struct');
|
|
74
|
+
// Constants are defined in options.go, but referenced in NewClient
|
|
75
|
+
expect(content).not.toContain('defaultBaseURL =');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('uses acronym-aware service accessors and fields', () => {
|
|
79
|
+
const spec = makeSpec([
|
|
80
|
+
{ name: 'ApiKeys', operations: [] },
|
|
81
|
+
{ name: 'SSO', operations: [] },
|
|
82
|
+
]);
|
|
83
|
+
const files = generateClient(spec, makeCtx(spec));
|
|
84
|
+
const workosFile = files.find((f) => f.path === 'workos.go')!;
|
|
85
|
+
const content = workosFile.content;
|
|
86
|
+
|
|
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
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { EmitterContext, ApiSpec, Enum } from '@workos/oagen';
|
|
3
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
4
|
+
import { generateEnums } from '../../src/go/enums.js';
|
|
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('go/enums', () => {
|
|
23
|
+
it('returns empty for no enums', () => {
|
|
24
|
+
expect(generateEnums([], ctx)).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('generates typed string constants', () => {
|
|
28
|
+
const enums: Enum[] = [
|
|
29
|
+
{
|
|
30
|
+
name: 'ConnectionStatus',
|
|
31
|
+
values: [
|
|
32
|
+
{ name: 'ACTIVE', value: 'active' },
|
|
33
|
+
{ name: 'INACTIVE', value: 'inactive' },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
const files = generateEnums(enums, ctx);
|
|
38
|
+
expect(files).toHaveLength(1);
|
|
39
|
+
expect(files[0].path).toBe('enums.go');
|
|
40
|
+
const content = files[0].content;
|
|
41
|
+
expect(content).toContain('package workos');
|
|
42
|
+
expect(content).toContain('type ConnectionStatus string');
|
|
43
|
+
expect(content).toContain('ConnectionStatusActive ConnectionStatus = "active"');
|
|
44
|
+
expect(content).toContain('ConnectionStatusInactive ConnectionStatus = "inactive"');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('deduplicates identical enums as type aliases', () => {
|
|
48
|
+
const enums: Enum[] = [
|
|
49
|
+
{
|
|
50
|
+
name: 'Alpha',
|
|
51
|
+
values: [{ name: 'A', value: 'a' }],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'Beta',
|
|
55
|
+
values: [{ name: 'A', value: 'a' }],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
const files = generateEnums(enums, ctx);
|
|
59
|
+
const content = files[0].content;
|
|
60
|
+
expect(content).toContain('type Alpha string');
|
|
61
|
+
expect(content).toContain('type Beta = Alpha');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('handles empty enums as type aliases to string', () => {
|
|
65
|
+
const enums: Enum[] = [{ name: 'UnknownType', values: [] }];
|
|
66
|
+
const files = generateEnums(enums, ctx);
|
|
67
|
+
const content = files[0].content;
|
|
68
|
+
expect(content).toContain('type UnknownType = string');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('snapshot: ConnectionStatus enum', () => {
|
|
72
|
+
const enums: Enum[] = [
|
|
73
|
+
{
|
|
74
|
+
name: 'ConnectionStatus',
|
|
75
|
+
values: [
|
|
76
|
+
{ name: 'ACTIVE', value: 'active' },
|
|
77
|
+
{ name: 'INACTIVE', value: 'inactive' },
|
|
78
|
+
{ name: 'PENDING', value: 'pending' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
const files = generateEnums(enums, ctx);
|
|
83
|
+
expect(files[0].content).toMatchInlineSnapshot(`
|
|
84
|
+
"package workos
|
|
85
|
+
|
|
86
|
+
// ConnectionStatus represents connection status values.
|
|
87
|
+
type ConnectionStatus string
|
|
88
|
+
|
|
89
|
+
const (
|
|
90
|
+
ConnectionStatusActive ConnectionStatus = "active"
|
|
91
|
+
ConnectionStatusInactive ConnectionStatus = "inactive"
|
|
92
|
+
ConnectionStatusPending ConnectionStatus = "pending"
|
|
93
|
+
)
|
|
94
|
+
"
|
|
95
|
+
`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('emits Deprecated comments for deprecated enum values', () => {
|
|
99
|
+
const enums: Enum[] = [
|
|
100
|
+
{
|
|
101
|
+
name: 'WidgetStatus',
|
|
102
|
+
values: [
|
|
103
|
+
{ name: 'ACTIVE', value: 'active', description: 'Currently active', deprecated: true },
|
|
104
|
+
{ name: 'LEGACY', value: 'legacy', deprecated: true },
|
|
105
|
+
{ name: 'CURRENT', value: 'current' },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
const files = generateEnums(enums, ctx);
|
|
110
|
+
const content = files[0].content;
|
|
111
|
+
// deprecated value WITH description gets separator + Deprecated
|
|
112
|
+
expect(content).toContain(
|
|
113
|
+
'\t// WidgetStatusActive is Currently active.\n\t//\n\t// Deprecated: this value is deprecated.',
|
|
114
|
+
);
|
|
115
|
+
// deprecated value WITHOUT description gets Deprecated only
|
|
116
|
+
expect(content).toContain('\t// Deprecated: this value is deprecated.\n\tWidgetStatusLegacy');
|
|
117
|
+
// non-deprecated value does NOT get Deprecated
|
|
118
|
+
expect(content).not.toMatch(/Deprecated.*\n\tWidgetStatusCurrent/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('uses Go acronym conventions for enum type names', () => {
|
|
122
|
+
const enums: Enum[] = [
|
|
123
|
+
{
|
|
124
|
+
name: 'SsoConnectionType',
|
|
125
|
+
values: [{ name: 'SAML', value: 'saml' }],
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
const files = generateEnums(enums, ctx);
|
|
129
|
+
const content = files[0].content;
|
|
130
|
+
expect(content).toContain('type SSOConnectionType string');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { goEmitter } from '../../src/go/index.js';
|
|
3
|
+
|
|
4
|
+
describe('go/errors', () => {
|
|
5
|
+
it('returns empty array (errors are hand-maintained in the target SDK)', () => {
|
|
6
|
+
const files = goEmitter.generateErrors({} as any);
|
|
7
|
+
expect(files).toHaveLength(0);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { EmitterContext, ApiSpec, Model } from '@workos/oagen';
|
|
3
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
4
|
+
import { generateModels } from '../../src/go/models.js';
|
|
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('go/models', () => {
|
|
23
|
+
it('returns empty for no models', () => {
|
|
24
|
+
expect(generateModels([], ctx)).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('generates a struct with required and optional fields', () => {
|
|
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
|
+
name: 'metadata',
|
|
36
|
+
type: { kind: 'map', valueType: { kind: 'primitive', type: 'string' } },
|
|
37
|
+
required: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
const files = generateModels(models, ctx);
|
|
43
|
+
expect(files).toHaveLength(1);
|
|
44
|
+
expect(files[0].path).toBe('models.go');
|
|
45
|
+
const content = files[0].content;
|
|
46
|
+
expect(content).toContain('package workos');
|
|
47
|
+
expect(content).toContain('type Organization struct {');
|
|
48
|
+
expect(content).toContain('ID string `json:"id"`');
|
|
49
|
+
expect(content).toContain('Name string `json:"name"`');
|
|
50
|
+
expect(content).toContain('Metadata map[string]string `json:"metadata,omitempty"`');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('handles model refs as pointer types', () => {
|
|
54
|
+
const models: Model[] = [
|
|
55
|
+
{
|
|
56
|
+
name: 'User',
|
|
57
|
+
fields: [
|
|
58
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
59
|
+
{ name: 'profile', type: { kind: 'model', name: 'Profile' }, required: true },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'Profile',
|
|
64
|
+
fields: [{ name: 'bio', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
const files = generateModels(models, ctx);
|
|
68
|
+
const content = files[0].content;
|
|
69
|
+
expect(content).toContain('Profile *Profile `json:"profile"`');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('handles nullable fields as pointers', () => {
|
|
73
|
+
const models: Model[] = [
|
|
74
|
+
{
|
|
75
|
+
name: 'Item',
|
|
76
|
+
fields: [
|
|
77
|
+
{
|
|
78
|
+
name: 'description',
|
|
79
|
+
type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
|
|
80
|
+
required: false,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
const files = generateModels(models, ctx);
|
|
86
|
+
const content = files[0].content;
|
|
87
|
+
expect(content).toContain('Description *string');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('skips list wrapper models', () => {
|
|
91
|
+
const models: Model[] = [
|
|
92
|
+
{
|
|
93
|
+
name: 'OrganizationList',
|
|
94
|
+
fields: [
|
|
95
|
+
{
|
|
96
|
+
name: 'data',
|
|
97
|
+
type: {
|
|
98
|
+
kind: 'array',
|
|
99
|
+
items: { kind: 'model', name: 'Organization' },
|
|
100
|
+
},
|
|
101
|
+
required: true,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'list_metadata',
|
|
105
|
+
type: { kind: 'model', name: 'ListMetadata' },
|
|
106
|
+
required: true,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
const files = generateModels(models, ctx);
|
|
112
|
+
const content = files[0].content;
|
|
113
|
+
expect(content).not.toContain('OrganizationList');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('deduplicates structurally identical models', () => {
|
|
117
|
+
const models: Model[] = [
|
|
118
|
+
{
|
|
119
|
+
name: 'Alpha',
|
|
120
|
+
fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'Beta',
|
|
124
|
+
fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
const files = generateModels(models, ctx);
|
|
128
|
+
const content = files[0].content;
|
|
129
|
+
expect(content).toContain('type Alpha struct {');
|
|
130
|
+
expect(content).toContain('type Beta = Alpha');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('uses Go acronym conventions for field names', () => {
|
|
134
|
+
const models: Model[] = [
|
|
135
|
+
{
|
|
136
|
+
name: 'Connection',
|
|
137
|
+
fields: [
|
|
138
|
+
{ name: 'connection_id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
139
|
+
{ name: 'sso_url', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
const files = generateModels(models, ctx);
|
|
144
|
+
const content = files[0].content;
|
|
145
|
+
expect(content).toContain('ConnectionID string');
|
|
146
|
+
expect(content).toContain('SSOURL *string');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('generates array fields', () => {
|
|
150
|
+
const models: Model[] = [
|
|
151
|
+
{
|
|
152
|
+
name: 'Org',
|
|
153
|
+
fields: [
|
|
154
|
+
{
|
|
155
|
+
name: 'domains',
|
|
156
|
+
type: { kind: 'array', items: { kind: 'primitive', type: 'string' } },
|
|
157
|
+
required: true,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
const files = generateModels(models, ctx);
|
|
163
|
+
const content = files[0].content;
|
|
164
|
+
expect(content).toContain('Domains []string `json:"domains"`');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('generates enum field references', () => {
|
|
168
|
+
const models: Model[] = [
|
|
169
|
+
{
|
|
170
|
+
name: 'Connection',
|
|
171
|
+
fields: [{ name: 'status', type: { kind: 'enum', name: 'ConnectionStatus' }, required: true }],
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
const files = generateModels(models, ctx);
|
|
175
|
+
const content = files[0].content;
|
|
176
|
+
expect(content).toContain('Status ConnectionStatus `json:"status"`');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('preserves DTO model names when emitting distinct types', () => {
|
|
180
|
+
const models: Model[] = [
|
|
181
|
+
{
|
|
182
|
+
name: 'RedirectUriDto',
|
|
183
|
+
fields: [{ name: 'uri', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
const files = generateModels(models, ctx);
|
|
187
|
+
const content = files[0].content;
|
|
188
|
+
expect(content).toContain('type RedirectURIDto struct {');
|
|
189
|
+
expect(content).toContain('URI string `json:"uri"`');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('emits Deprecated comments for deprecated fields', () => {
|
|
193
|
+
const models: Model[] = [
|
|
194
|
+
{
|
|
195
|
+
name: 'Widget',
|
|
196
|
+
fields: [
|
|
197
|
+
{
|
|
198
|
+
name: 'old_name',
|
|
199
|
+
type: { kind: 'primitive', type: 'string' },
|
|
200
|
+
required: false,
|
|
201
|
+
description: 'The original name.',
|
|
202
|
+
deprecated: true,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'legacy_id',
|
|
206
|
+
type: { kind: 'primitive', type: 'string' },
|
|
207
|
+
required: false,
|
|
208
|
+
deprecated: true,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: 'current_name',
|
|
212
|
+
type: { kind: 'primitive', type: 'string' },
|
|
213
|
+
required: true,
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
const files = generateModels(models, ctx);
|
|
219
|
+
const content = files[0].content;
|
|
220
|
+
// deprecated field WITH description gets separator + Deprecated
|
|
221
|
+
expect(content).toContain('\t// OldName is the original name.\n\t//\n\t// Deprecated: this field is deprecated.');
|
|
222
|
+
// deprecated field WITHOUT description gets Deprecated only (no separator)
|
|
223
|
+
expect(content).toContain('\t// Deprecated: this field is deprecated.\n\tLegacyID');
|
|
224
|
+
// non-deprecated field does NOT get Deprecated
|
|
225
|
+
expect(content).not.toMatch(/Deprecated.*\n\tCurrentName/);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('snapshot: Organization struct', () => {
|
|
229
|
+
const models: Model[] = [
|
|
230
|
+
{
|
|
231
|
+
name: 'Organization',
|
|
232
|
+
description: 'Represents an organization.',
|
|
233
|
+
fields: [
|
|
234
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
235
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
236
|
+
{ name: 'created_at', type: { kind: 'primitive', type: 'string', format: 'date-time' }, required: true },
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
const files = generateModels(models, ctx);
|
|
241
|
+
expect(files[0].content).toMatchInlineSnapshot(`
|
|
242
|
+
"package workos
|
|
243
|
+
|
|
244
|
+
// Organization represents an organization.
|
|
245
|
+
type Organization struct {
|
|
246
|
+
ID string \`json:"id"\`
|
|
247
|
+
Name string \`json:"name"\`
|
|
248
|
+
CreatedAt string \`json:"created_at"\`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// PaginationParams contains common pagination parameters for list operations.
|
|
252
|
+
type PaginationParams struct {
|
|
253
|
+
// Before is a cursor for reverse pagination.
|
|
254
|
+
Before *string \`url:"before,omitempty" json:"-"\`
|
|
255
|
+
// After is a cursor for forward pagination.
|
|
256
|
+
After *string \`url:"after,omitempty" json:"-"\`
|
|
257
|
+
// Limit is the maximum number of items to return per page.
|
|
258
|
+
Limit *int \`url:"limit,omitempty" json:"-"\`
|
|
259
|
+
// Order is the sort order for results (asc or desc).
|
|
260
|
+
Order *string \`url:"order,omitempty" json:"-"\`
|
|
261
|
+
}
|
|
262
|
+
"
|
|
263
|
+
`);
|
|
264
|
+
});
|
|
265
|
+
});
|