@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,185 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { EmitterContext, ApiSpec, Service, Model } from '@workos/oagen';
|
|
3
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
4
|
+
import { generateTests } from '../../src/php/tests.js';
|
|
5
|
+
|
|
6
|
+
const models: Model[] = [
|
|
7
|
+
{
|
|
8
|
+
name: 'Organization',
|
|
9
|
+
fields: [
|
|
10
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
11
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const services: Service[] = [
|
|
17
|
+
{
|
|
18
|
+
name: 'Organizations',
|
|
19
|
+
operations: [
|
|
20
|
+
{
|
|
21
|
+
name: 'getOrganization',
|
|
22
|
+
httpMethod: 'get',
|
|
23
|
+
path: '/organizations/{id}',
|
|
24
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
25
|
+
queryParams: [],
|
|
26
|
+
headerParams: [],
|
|
27
|
+
response: { kind: 'model', name: 'Organization' },
|
|
28
|
+
errors: [],
|
|
29
|
+
injectIdempotencyKey: false,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'listOrganizations',
|
|
33
|
+
httpMethod: 'get',
|
|
34
|
+
path: '/organizations',
|
|
35
|
+
pathParams: [],
|
|
36
|
+
queryParams: [{ name: 'after', type: { kind: 'primitive', type: 'string' }, required: false }],
|
|
37
|
+
headerParams: [],
|
|
38
|
+
response: { kind: 'model', name: 'Organization' },
|
|
39
|
+
errors: [],
|
|
40
|
+
pagination: {
|
|
41
|
+
strategy: 'cursor',
|
|
42
|
+
param: 'after',
|
|
43
|
+
dataPath: 'data',
|
|
44
|
+
itemType: { kind: 'model', name: 'Organization' },
|
|
45
|
+
},
|
|
46
|
+
injectIdempotencyKey: false,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const spec: ApiSpec = {
|
|
53
|
+
name: 'Test',
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
baseUrl: '',
|
|
56
|
+
services,
|
|
57
|
+
models,
|
|
58
|
+
enums: [],
|
|
59
|
+
sdk: defaultSdkBehavior(),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const ctx: EmitterContext = {
|
|
63
|
+
namespace: 'workos',
|
|
64
|
+
namespacePascal: 'WorkOS',
|
|
65
|
+
spec,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
describe('generateTests', () => {
|
|
69
|
+
it('does not generate TestHelper (now hand-maintained)', () => {
|
|
70
|
+
const result = generateTests(spec, ctx);
|
|
71
|
+
|
|
72
|
+
const helper = result.find((f) => f.path === 'tests/TestHelper.php');
|
|
73
|
+
expect(helper).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('generates resource test files', () => {
|
|
77
|
+
const result = generateTests(spec, ctx);
|
|
78
|
+
|
|
79
|
+
const resourceTest = result.find((f) => f.path === 'tests/Service/OrganizationsTest.php');
|
|
80
|
+
expect(resourceTest).toBeDefined();
|
|
81
|
+
expect(resourceTest!.content).toContain('class OrganizationsTest extends TestCase');
|
|
82
|
+
expect(resourceTest!.content).toContain('use TestHelper;');
|
|
83
|
+
expect(resourceTest!.content).toContain('testGet');
|
|
84
|
+
// Round-trip assertion: fromArray -> toArray must not throw
|
|
85
|
+
expect(resourceTest!.content).toContain('$result->toArray()');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('generates client test', () => {
|
|
89
|
+
const result = generateTests(spec, ctx);
|
|
90
|
+
|
|
91
|
+
const clientTest = result.find((f) => f.path === 'tests/ClientTest.php');
|
|
92
|
+
expect(clientTest).toBeDefined();
|
|
93
|
+
expect(clientTest!.content).toContain('testConstructor');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('generates pagination boundary test with cursor null assertions', () => {
|
|
97
|
+
const result = generateTests(spec, ctx);
|
|
98
|
+
|
|
99
|
+
const resourceTest = result.find((f) => f.path === 'tests/Service/OrganizationsTest.php');
|
|
100
|
+
expect(resourceTest).toBeDefined();
|
|
101
|
+
expect(resourceTest!.content).toContain('testPaginationBoundary');
|
|
102
|
+
// Cursor null assertions
|
|
103
|
+
expect(resourceTest!.content).toContain("$this->assertNull($result->listMetadata['before'])");
|
|
104
|
+
expect(resourceTest!.content).toContain("$this->assertNull($result->listMetadata['after'])");
|
|
105
|
+
// Iteration still tested
|
|
106
|
+
expect(resourceTest!.content).toContain('foreach ($result as $item)');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('generates redirect endpoint test with query param assertions', () => {
|
|
110
|
+
const ssoServices: Service[] = [
|
|
111
|
+
{
|
|
112
|
+
name: 'SSO',
|
|
113
|
+
operations: [
|
|
114
|
+
{
|
|
115
|
+
name: 'getAuthorizationUrl',
|
|
116
|
+
httpMethod: 'get',
|
|
117
|
+
path: '/sso/authorize',
|
|
118
|
+
pathParams: [],
|
|
119
|
+
queryParams: [
|
|
120
|
+
{ name: 'client_id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
121
|
+
{ name: 'response_type', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
122
|
+
{ name: 'redirect_uri', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
123
|
+
{ name: 'state', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
124
|
+
],
|
|
125
|
+
headerParams: [],
|
|
126
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
127
|
+
errors: [],
|
|
128
|
+
injectIdempotencyKey: false,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const ssoSpec = { ...spec, services: ssoServices };
|
|
135
|
+
const ssoCtx: EmitterContext = {
|
|
136
|
+
...ctx,
|
|
137
|
+
spec: ssoSpec,
|
|
138
|
+
resolvedOperations: [
|
|
139
|
+
{
|
|
140
|
+
operation: ssoServices[0].operations[0],
|
|
141
|
+
service: ssoServices[0],
|
|
142
|
+
methodName: 'get_authorization_url',
|
|
143
|
+
mountOn: 'SSO',
|
|
144
|
+
defaults: { response_type: 'code' },
|
|
145
|
+
inferFromClient: ['client_id'],
|
|
146
|
+
} as any,
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = generateTests(ssoSpec, ssoCtx);
|
|
151
|
+
const testFile = result.find((f) => f.path === 'tests/Service/SSOTest.php');
|
|
152
|
+
expect(testFile).toBeDefined();
|
|
153
|
+
const content = testFile!.content;
|
|
154
|
+
|
|
155
|
+
// Should be a redirect endpoint test (no mock responses, returns string)
|
|
156
|
+
expect(content).toContain('$this->assertIsString($result)');
|
|
157
|
+
expect(content).toContain("assertStringContainsString('sso/authorize'");
|
|
158
|
+
|
|
159
|
+
// Should parse query params from URL
|
|
160
|
+
expect(content).toContain('parse_str(parse_url($result, PHP_URL_QUERY)');
|
|
161
|
+
|
|
162
|
+
// Should assert visible query params (required and optional)
|
|
163
|
+
expect(content).toContain("assertSame('test_value', $query['redirect_uri'])");
|
|
164
|
+
expect(content).toContain("assertSame('test_value', $query['state'])");
|
|
165
|
+
|
|
166
|
+
// Should pass optional params in the method call
|
|
167
|
+
expect(content).toContain("state: 'test_value'");
|
|
168
|
+
|
|
169
|
+
// Should assert hidden defaults
|
|
170
|
+
expect(content).toContain("assertSame('code', $query['response_type'])");
|
|
171
|
+
|
|
172
|
+
// Should assert inferred client fields
|
|
173
|
+
expect(content).toContain("assertArrayHasKey('client_id', $query)");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('generates fixture JSON files', () => {
|
|
177
|
+
const result = generateTests(spec, ctx);
|
|
178
|
+
|
|
179
|
+
const fixture = result.find((f) => f.path.includes('Fixtures/organization.json'));
|
|
180
|
+
expect(fixture).toBeDefined();
|
|
181
|
+
const parsed = JSON.parse(fixture!.content);
|
|
182
|
+
expect(parsed).toHaveProperty('id');
|
|
183
|
+
expect(parsed).toHaveProperty('name');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateClient } from '../../src/python/client.js';
|
|
3
|
+
import type { EmitterContext, ApiSpec, Service, Model } from '@workos/oagen';
|
|
4
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
5
|
+
|
|
6
|
+
const models: Model[] = [
|
|
7
|
+
{
|
|
8
|
+
name: 'Organization',
|
|
9
|
+
fields: [
|
|
10
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
11
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const services: Service[] = [
|
|
17
|
+
{
|
|
18
|
+
name: 'Organizations',
|
|
19
|
+
operations: [
|
|
20
|
+
{
|
|
21
|
+
name: 'listOrganizations',
|
|
22
|
+
httpMethod: 'get',
|
|
23
|
+
path: '/organizations',
|
|
24
|
+
pathParams: [],
|
|
25
|
+
queryParams: [],
|
|
26
|
+
headerParams: [],
|
|
27
|
+
response: { kind: 'model', name: 'Organization' },
|
|
28
|
+
errors: [],
|
|
29
|
+
injectIdempotencyKey: false,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const spec: ApiSpec = {
|
|
36
|
+
name: 'TestAPI',
|
|
37
|
+
version: '1.0.0',
|
|
38
|
+
baseUrl: 'https://api.workos.com',
|
|
39
|
+
services,
|
|
40
|
+
models,
|
|
41
|
+
enums: [],
|
|
42
|
+
sdk: defaultSdkBehavior(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const ctx: EmitterContext = {
|
|
46
|
+
namespace: 'workos',
|
|
47
|
+
namespacePascal: 'WorkOS',
|
|
48
|
+
spec,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe('generateClient', () => {
|
|
52
|
+
it('generates slim client that subclasses _base_client', () => {
|
|
53
|
+
const files = generateClient(spec, ctx);
|
|
54
|
+
|
|
55
|
+
const clientFile = files.find((f) => f.path === 'src/workos/_client.py');
|
|
56
|
+
expect(clientFile).toBeDefined();
|
|
57
|
+
|
|
58
|
+
const content = clientFile!.content;
|
|
59
|
+
// Imports from _base_client
|
|
60
|
+
expect(content).toContain('from ._base_client import');
|
|
61
|
+
expect(content).toContain('WorkOSClient as _SyncBase');
|
|
62
|
+
expect(content).toContain('AsyncWorkOSClient as _AsyncBase');
|
|
63
|
+
// Subclass definitions
|
|
64
|
+
expect(content).toContain('class WorkOSClient(_SyncBase):');
|
|
65
|
+
expect(content).toContain('class AsyncWorkOSClient(_AsyncBase):');
|
|
66
|
+
// Lazy resource accessors via cached_property
|
|
67
|
+
expect(content).toContain('@functools.cached_property');
|
|
68
|
+
expect(content).toContain('def organizations(self) -> Organizations:');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('does not contain static HTTP infrastructure', () => {
|
|
72
|
+
const files = generateClient(spec, ctx);
|
|
73
|
+
const clientFile = files.find((f) => f.path === 'src/workos/_client.py');
|
|
74
|
+
const content = clientFile!.content;
|
|
75
|
+
|
|
76
|
+
// Static code should NOT be in the generated file
|
|
77
|
+
expect(content).not.toContain('class _BaseWorkOSClient:');
|
|
78
|
+
expect(content).not.toContain('def request(');
|
|
79
|
+
expect(content).not.toContain('def request_page(');
|
|
80
|
+
expect(content).not.toContain('RETRY_STATUS_CODES');
|
|
81
|
+
expect(content).not.toContain('Idempotency-Key');
|
|
82
|
+
expect(content).not.toContain('def _parse_retry_after(');
|
|
83
|
+
expect(content).not.toContain('def _raise_error(');
|
|
84
|
+
expect(content).not.toContain('def close(self)');
|
|
85
|
+
expect(content).not.toContain('def __enter__');
|
|
86
|
+
expect(content).not.toContain('import httpx');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does not generate barrel __init__.py (now @oagen-ignore-file)', () => {
|
|
90
|
+
const files = generateClient(spec, ctx);
|
|
91
|
+
const barrel = files.find((f) => f.path === 'src/workos/__init__.py');
|
|
92
|
+
expect(barrel).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('does not generate pyproject.toml or py.typed (now in target)', () => {
|
|
96
|
+
const files = generateClient(spec, ctx);
|
|
97
|
+
expect(files.find((f) => f.path === 'pyproject.toml')).toBeUndefined();
|
|
98
|
+
expect(files.find((f) => f.path === 'src/workos/py.typed')).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('generates service __init__.py', () => {
|
|
102
|
+
const files = generateClient(spec, ctx);
|
|
103
|
+
|
|
104
|
+
const serviceInit = files.find((f) => f.path === 'src/workos/organizations/__init__.py');
|
|
105
|
+
expect(serviceInit).toBeDefined();
|
|
106
|
+
expect(serviceInit!.content).toContain('from ._resource import Organizations, AsyncOrganizations');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('generates flat directory structure for services (no nested namespaces)', () => {
|
|
110
|
+
const nestedSpec: ApiSpec = {
|
|
111
|
+
...spec,
|
|
112
|
+
services: [
|
|
113
|
+
...services,
|
|
114
|
+
{
|
|
115
|
+
name: 'OrganizationsApiKeys',
|
|
116
|
+
operations: [
|
|
117
|
+
{
|
|
118
|
+
name: 'listOrganizationApiKeys',
|
|
119
|
+
httpMethod: 'get',
|
|
120
|
+
path: '/organizations/api_keys',
|
|
121
|
+
pathParams: [],
|
|
122
|
+
queryParams: [],
|
|
123
|
+
headerParams: [],
|
|
124
|
+
response: { kind: 'model', name: 'Organization' },
|
|
125
|
+
errors: [],
|
|
126
|
+
injectIdempotencyKey: false,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const files = generateClient(nestedSpec, { ...ctx, spec: nestedSpec });
|
|
134
|
+
|
|
135
|
+
// Service gets its own flat directory (no nesting)
|
|
136
|
+
const serviceInit = files.find((f) => f.path === 'src/workos/organizations_api_keys/__init__.py');
|
|
137
|
+
expect(serviceInit).toBeDefined();
|
|
138
|
+
|
|
139
|
+
// Client should import from the flat path
|
|
140
|
+
const clientFile = files.find((f) => f.path === 'src/workos/_client.py');
|
|
141
|
+
expect(clientFile!.content).toContain('OrganizationsApiKeys');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('does not generate compat shim modules', () => {
|
|
145
|
+
const files = generateClient(spec, ctx);
|
|
146
|
+
|
|
147
|
+
expect(files.find((f) => f.path === 'src/workos/client.py')).toBeUndefined();
|
|
148
|
+
expect(files.find((f) => f.path === 'src/workos/async_client.py')).toBeUndefined();
|
|
149
|
+
expect(files.find((f) => f.path === 'src/workos/exceptions.py')).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('does not generate user_management helper methods but generates types barrels', () => {
|
|
153
|
+
const compatSpec: ApiSpec = {
|
|
154
|
+
...spec,
|
|
155
|
+
services: [
|
|
156
|
+
{
|
|
157
|
+
name: 'UserManagementUsers',
|
|
158
|
+
operations: [
|
|
159
|
+
{
|
|
160
|
+
name: 'listUsers',
|
|
161
|
+
httpMethod: 'get',
|
|
162
|
+
path: '/user_management/users',
|
|
163
|
+
pathParams: [],
|
|
164
|
+
queryParams: [],
|
|
165
|
+
headerParams: [],
|
|
166
|
+
response: { kind: 'model', name: 'Organization' },
|
|
167
|
+
errors: [],
|
|
168
|
+
injectIdempotencyKey: false,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'UserManagementAuthentication',
|
|
174
|
+
operations: [
|
|
175
|
+
{
|
|
176
|
+
name: 'authorize',
|
|
177
|
+
httpMethod: 'get',
|
|
178
|
+
path: '/user_management/authorize',
|
|
179
|
+
pathParams: [],
|
|
180
|
+
queryParams: [],
|
|
181
|
+
headerParams: [],
|
|
182
|
+
response: { kind: 'model', name: 'Organization' },
|
|
183
|
+
errors: [],
|
|
184
|
+
injectIdempotencyKey: false,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const files = generateClient(compatSpec, { ...ctx, spec: compatSpec });
|
|
192
|
+
const clientFile = files.find((f) => f.path === 'src/workos/_client.py');
|
|
193
|
+
// load_sealed_session is now added via @oagen-ignore in the target SDK, not emitter-generated
|
|
194
|
+
expect(clientFile!.content).not.toContain('def load_sealed_session');
|
|
195
|
+
// Client has flat accessors, no methods from child services
|
|
196
|
+
expect(clientFile!.content).not.toContain('def get_authorization_url');
|
|
197
|
+
expect(clientFile!.content).not.toContain('def get_user(');
|
|
198
|
+
expect(clientFile!.content).not.toContain('def create_user(');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateEnums } from '../../src/python/enums.js';
|
|
3
|
+
import type { EmitterContext, ApiSpec, Enum, Service } 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('generateEnums', () => {
|
|
23
|
+
it('returns empty for no enums', () => {
|
|
24
|
+
expect(generateEnums([], ctx)).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('generates str, Enum class', () => {
|
|
28
|
+
const enums: Enum[] = [
|
|
29
|
+
{
|
|
30
|
+
name: 'Status',
|
|
31
|
+
values: [
|
|
32
|
+
{ name: 'ACTIVE', value: 'active' },
|
|
33
|
+
{ name: 'INACTIVE', value: 'inactive' },
|
|
34
|
+
{ name: 'PENDING', value: 'pending' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const service: Service = {
|
|
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
|
+
};
|
|
55
|
+
|
|
56
|
+
const files = generateEnums(enums, {
|
|
57
|
+
...ctx,
|
|
58
|
+
spec: { ...emptySpec, services: [service] },
|
|
59
|
+
});
|
|
60
|
+
expect(files.length).toBe(1);
|
|
61
|
+
expect(files[0].content).toContain('from enum import Enum');
|
|
62
|
+
expect(files[0].content).toContain('class Status(str, Enum):');
|
|
63
|
+
expect(files[0].content).toContain(' ACTIVE = "active"');
|
|
64
|
+
expect(files[0].content).toContain(' INACTIVE = "inactive"');
|
|
65
|
+
expect(files[0].content).toContain(' PENDING = "pending"');
|
|
66
|
+
expect(files[0].content).toContain('def _missing_(cls, value: object) -> Optional["Status"]:');
|
|
67
|
+
expect(files[0].content).toContain('unknown = str.__new__(cls, value)');
|
|
68
|
+
expect(files[0].content).toContain('StatusLiteral: TypeAlias = Literal["active", "inactive", "pending"]');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('places enum in service directory when referenced', () => {
|
|
72
|
+
const service: Service = {
|
|
73
|
+
name: 'Organizations',
|
|
74
|
+
operations: [
|
|
75
|
+
{
|
|
76
|
+
name: 'listOrganizations',
|
|
77
|
+
httpMethod: 'get',
|
|
78
|
+
path: '/organizations',
|
|
79
|
+
pathParams: [],
|
|
80
|
+
queryParams: [
|
|
81
|
+
{
|
|
82
|
+
name: 'status',
|
|
83
|
+
type: { kind: 'enum', name: 'OrgStatus' },
|
|
84
|
+
required: false,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
headerParams: [],
|
|
88
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
89
|
+
errors: [],
|
|
90
|
+
injectIdempotencyKey: false,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const enums: Enum[] = [
|
|
96
|
+
{
|
|
97
|
+
name: 'OrgStatus',
|
|
98
|
+
values: [
|
|
99
|
+
{ name: 'ACTIVE', value: 'active' },
|
|
100
|
+
{ name: 'INACTIVE', value: 'inactive' },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const files = generateEnums(enums, {
|
|
106
|
+
...ctx,
|
|
107
|
+
spec: { ...emptySpec, services: [service] },
|
|
108
|
+
});
|
|
109
|
+
expect(files.length).toBe(1);
|
|
110
|
+
expect(files[0].path).toBe('src/workos/organizations/models/org_status.py');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('deduplicates values that produce the same string', () => {
|
|
114
|
+
const enums: Enum[] = [
|
|
115
|
+
{
|
|
116
|
+
name: 'Action',
|
|
117
|
+
values: [
|
|
118
|
+
{ name: 'SIGN_UP', value: 'sign-up' },
|
|
119
|
+
{ name: 'SIGN_UP_2', value: 'sign_up' },
|
|
120
|
+
{ name: 'SIGN_UP_3', value: 'sign up' },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const files = generateEnums(enums, ctx);
|
|
126
|
+
expect(files.length).toBe(1);
|
|
127
|
+
expect(files[0].content).toContain('class Action(str, Enum):');
|
|
128
|
+
expect(files[0].content).toContain('SIGN_UP = "sign-up"');
|
|
129
|
+
expect(files[0].content).toContain('SIGN_UP_2 = "sign_up"');
|
|
130
|
+
expect(files[0].content).toContain('SIGN_UP_3 = "sign up"');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('generates type alias for structurally identical enums', () => {
|
|
134
|
+
const enums: Enum[] = [
|
|
135
|
+
{
|
|
136
|
+
name: 'ConnectionType',
|
|
137
|
+
values: [
|
|
138
|
+
{ name: 'SAML', value: 'saml' },
|
|
139
|
+
{ name: 'OIDC', value: 'oidc' },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'ProfileConnectionType',
|
|
144
|
+
values: [
|
|
145
|
+
{ name: 'SAML', value: 'saml' },
|
|
146
|
+
{ name: 'OIDC', value: 'oidc' },
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const files = generateEnums(enums, ctx);
|
|
152
|
+
expect(files.length).toBe(2);
|
|
153
|
+
|
|
154
|
+
// Canonical (alphabetically first) should be a full enum
|
|
155
|
+
const canonical = files.find((f) => f.path.includes('connection_type.py') && !f.path.includes('profile'))!;
|
|
156
|
+
expect(canonical).toBeDefined();
|
|
157
|
+
expect(canonical.content).toContain('class ConnectionType(str, Enum):');
|
|
158
|
+
|
|
159
|
+
// Alias should import canonical and create assignment alias
|
|
160
|
+
const alias = files.find((f) => f.path.includes('profile_connection_type.py'))!;
|
|
161
|
+
expect(alias).toBeDefined();
|
|
162
|
+
expect(alias.content).toContain('import ConnectionType');
|
|
163
|
+
expect(alias.content).toContain('ProfileConnectionType: TypeAlias = ConnectionType');
|
|
164
|
+
expect(alias.content).not.toContain('Literal');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('handles enum with descriptions', () => {
|
|
168
|
+
const enums: Enum[] = [
|
|
169
|
+
{
|
|
170
|
+
name: 'Role',
|
|
171
|
+
values: [
|
|
172
|
+
{ name: 'ADMIN', value: 'admin', description: 'Administrator role' },
|
|
173
|
+
{ name: 'MEMBER', value: 'member', description: 'Regular member' },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const files = generateEnums(enums, ctx);
|
|
179
|
+
expect(files.length).toBe(1);
|
|
180
|
+
expect(files[0].content).toContain('class Role(str, Enum):');
|
|
181
|
+
expect(files[0].content).toContain('ADMIN = "admin"');
|
|
182
|
+
expect(files[0].content).toContain('Administrator role');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('preserves canonical enum values from the spec', () => {
|
|
186
|
+
const enums: Enum[] = [
|
|
187
|
+
{
|
|
188
|
+
name: 'Provider',
|
|
189
|
+
values: [{ name: 'GITHUB_OAUTH', value: 'GithubOAuth' }],
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const files = generateEnums(enums, ctx);
|
|
194
|
+
expect(files[0].content).toContain('"GithubOAuth"');
|
|
195
|
+
expect(files[0].content).not.toContain('"GitHubOAuth"');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('adds .. deprecated:: docstring for deprecated enum values', () => {
|
|
199
|
+
const enums: Enum[] = [
|
|
200
|
+
{
|
|
201
|
+
name: 'Status',
|
|
202
|
+
values: [
|
|
203
|
+
{ name: 'ACTIVE', value: 'active' },
|
|
204
|
+
{ name: 'OLD_STATUS', value: 'old_status', deprecated: true, description: 'Use ACTIVE instead' },
|
|
205
|
+
{ name: 'LEGACY', value: 'legacy', deprecated: true },
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
const files = generateEnums(enums, ctx);
|
|
211
|
+
expect(files.length).toBe(1);
|
|
212
|
+
const content = files[0].content;
|
|
213
|
+
|
|
214
|
+
// Deprecated value with description gets both
|
|
215
|
+
expect(content).toContain('OLD_STATUS = "old_status"');
|
|
216
|
+
expect(content).toContain('"""Use ACTIVE instead\n\n .. deprecated::"""');
|
|
217
|
+
|
|
218
|
+
// Deprecated value without description gets just .. deprecated::
|
|
219
|
+
expect(content).toContain('LEGACY = "legacy"');
|
|
220
|
+
expect(content).toContain('""".. deprecated::"""');
|
|
221
|
+
|
|
222
|
+
// Non-deprecated value should not get a docstring
|
|
223
|
+
expect(content).toContain('ACTIVE = "active"');
|
|
224
|
+
const activeIdx = content.indexOf('ACTIVE = "active"');
|
|
225
|
+
const nextLine = content.slice(activeIdx).split('\n')[1];
|
|
226
|
+
expect(nextLine).not.toContain('"""');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { pythonEmitter } from '../../src/python/index.js';
|
|
3
|
+
import type { EmitterContext } from '@workos/oagen';
|
|
4
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
5
|
+
|
|
6
|
+
const ctx: EmitterContext = {
|
|
7
|
+
namespace: 'workos',
|
|
8
|
+
namespacePascal: 'WorkOS',
|
|
9
|
+
spec: { name: 'Test', version: '1.0.0', baseUrl: '', services: [], models: [], enums: [], sdk: defaultSdkBehavior() },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('generateErrors', () => {
|
|
13
|
+
it('returns empty array (errors now hand-maintained in target SDK)', () => {
|
|
14
|
+
expect(pythonEmitter.generateErrors!(ctx)).toEqual([]);
|
|
15
|
+
});
|
|
16
|
+
});
|