@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,210 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { defaultSdkBehavior, type ApiSpec, type EmitterContext, type Service } from '@workos/oagen';
|
|
3
|
+
import { generateResources } from '../../src/kotlin/resources.js';
|
|
4
|
+
import { generateEnums } from '../../src/kotlin/enums.js';
|
|
5
|
+
|
|
6
|
+
const baseSpec: ApiSpec = {
|
|
7
|
+
name: 'Test',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
baseUrl: 'https://api.workos.com',
|
|
10
|
+
services: [],
|
|
11
|
+
models: [
|
|
12
|
+
{
|
|
13
|
+
name: 'AuthenticateResponse',
|
|
14
|
+
fields: [{ name: 'access_token', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'SSOTokenResponse',
|
|
18
|
+
fields: [{ name: 'access_token', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
enums: [],
|
|
22
|
+
sdk: defaultSdkBehavior(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function ctxFor(services: Service[], enums = baseSpec.enums): EmitterContext {
|
|
26
|
+
return {
|
|
27
|
+
namespace: 'workos',
|
|
28
|
+
namespacePascal: 'WorkOS',
|
|
29
|
+
spec: { ...baseSpec, services, enums },
|
|
30
|
+
resolvedOperations: services.flatMap((service) =>
|
|
31
|
+
service.operations.map((operation) => ({
|
|
32
|
+
service,
|
|
33
|
+
operation,
|
|
34
|
+
methodName: operation.name,
|
|
35
|
+
mountOn: service.name,
|
|
36
|
+
defaults: {},
|
|
37
|
+
inferFromClient: [],
|
|
38
|
+
urlBuilder: false,
|
|
39
|
+
})),
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('kotlin/resources', () => {
|
|
45
|
+
it('collapses duplicated query/body params into a single Kotlin parameter', () => {
|
|
46
|
+
const services: Service[] = [
|
|
47
|
+
{
|
|
48
|
+
name: 'SSO',
|
|
49
|
+
operations: [
|
|
50
|
+
{
|
|
51
|
+
name: 'getProfileAndToken',
|
|
52
|
+
httpMethod: 'post',
|
|
53
|
+
path: '/sso/token',
|
|
54
|
+
pathParams: [],
|
|
55
|
+
queryParams: [{ name: 'code', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
56
|
+
headerParams: [],
|
|
57
|
+
requestBody: { kind: 'model', name: 'GetProfileAndTokenRequest' },
|
|
58
|
+
response: { kind: 'model', name: 'SSOTokenResponse' },
|
|
59
|
+
errors: [],
|
|
60
|
+
injectIdempotencyKey: false,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
const spec = {
|
|
66
|
+
...baseSpec,
|
|
67
|
+
services,
|
|
68
|
+
models: [
|
|
69
|
+
...baseSpec.models,
|
|
70
|
+
{
|
|
71
|
+
name: 'GetProfileAndTokenRequest',
|
|
72
|
+
fields: [{ name: 'code', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
const files = generateResources(services, { ...ctxFor(services), spec: spec as ApiSpec });
|
|
77
|
+
const ssoFile = files.find((file) => file.path.endsWith('/Sso.kt'));
|
|
78
|
+
expect(ssoFile).toBeDefined();
|
|
79
|
+
expect(ssoFile!.content).toContain('fun getProfileAndToken(');
|
|
80
|
+
expect(ssoFile!.content).toContain('code: String');
|
|
81
|
+
expect(ssoFile!.content).not.toContain('bodyCode');
|
|
82
|
+
expect(ssoFile!.content).toContain('params += "code" to code');
|
|
83
|
+
expect(ssoFile!.content).toContain('"code" to code');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('emits a shared authenticate helper for user management authenticate variants', () => {
|
|
87
|
+
const services: Service[] = [
|
|
88
|
+
{
|
|
89
|
+
name: 'UserManagement',
|
|
90
|
+
operations: [
|
|
91
|
+
{
|
|
92
|
+
name: 'authenticateWithPassword',
|
|
93
|
+
httpMethod: 'post',
|
|
94
|
+
path: '/user_management/authenticate',
|
|
95
|
+
pathParams: [],
|
|
96
|
+
queryParams: [],
|
|
97
|
+
headerParams: [],
|
|
98
|
+
requestBody: { kind: 'model', name: 'AuthenticatePasswordRequest' },
|
|
99
|
+
response: { kind: 'model', name: 'AuthenticateResponse' },
|
|
100
|
+
errors: [],
|
|
101
|
+
injectIdempotencyKey: false,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
const spec = {
|
|
107
|
+
...baseSpec,
|
|
108
|
+
services,
|
|
109
|
+
models: [
|
|
110
|
+
...baseSpec.models,
|
|
111
|
+
{
|
|
112
|
+
name: 'AuthenticatePasswordRequest',
|
|
113
|
+
fields: [
|
|
114
|
+
{ name: 'email', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
115
|
+
{ name: 'password', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
const files = generateResources(services, { ...ctxFor(services), spec: spec as ApiSpec });
|
|
121
|
+
const userManagementFile = files.find((file) => file.path.endsWith('/UserManagement.kt'));
|
|
122
|
+
expect(userManagementFile).toBeDefined();
|
|
123
|
+
expect(userManagementFile!.content).toContain('private fun authenticate(');
|
|
124
|
+
expect(userManagementFile!.content).toContain('grantType = "authorization_code"');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('renames package-level parameter group helpers to avoid Role/Password collisions', () => {
|
|
128
|
+
const services: Service[] = [
|
|
129
|
+
{
|
|
130
|
+
name: 'UserManagement',
|
|
131
|
+
operations: [
|
|
132
|
+
{
|
|
133
|
+
name: 'createUser',
|
|
134
|
+
httpMethod: 'post',
|
|
135
|
+
path: '/users',
|
|
136
|
+
pathParams: [],
|
|
137
|
+
queryParams: [],
|
|
138
|
+
headerParams: [],
|
|
139
|
+
requestBody: { kind: 'model', name: 'CreateUserRequest' },
|
|
140
|
+
response: { kind: 'model', name: 'AuthenticateResponse' },
|
|
141
|
+
errors: [],
|
|
142
|
+
injectIdempotencyKey: false,
|
|
143
|
+
parameterGroups: [
|
|
144
|
+
{
|
|
145
|
+
name: 'Password',
|
|
146
|
+
optional: true,
|
|
147
|
+
variants: [
|
|
148
|
+
{
|
|
149
|
+
name: 'Plaintext',
|
|
150
|
+
parameters: [{ name: 'password', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'Role',
|
|
156
|
+
optional: true,
|
|
157
|
+
variants: [
|
|
158
|
+
{
|
|
159
|
+
name: 'Single',
|
|
160
|
+
parameters: [{ name: 'role_slug', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
const spec = {
|
|
170
|
+
...baseSpec,
|
|
171
|
+
services,
|
|
172
|
+
models: [
|
|
173
|
+
...baseSpec.models,
|
|
174
|
+
{
|
|
175
|
+
name: 'CreateUserRequest',
|
|
176
|
+
fields: [
|
|
177
|
+
{ name: 'password', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
178
|
+
{ name: 'role_slug', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
const files = generateResources(services, { ...ctxFor(services), spec: spec as ApiSpec });
|
|
184
|
+
const userManagementFile = files.find((file) => file.path.endsWith('/UserManagement.kt'));
|
|
185
|
+
expect(userManagementFile).toBeDefined();
|
|
186
|
+
expect(userManagementFile!.content).toContain('sealed class CreateUserPassword');
|
|
187
|
+
expect(userManagementFile!.content).toContain('sealed class CreateUserRole');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('collapses asc/desc order enums into SortOrder', () => {
|
|
191
|
+
const enums = [
|
|
192
|
+
{
|
|
193
|
+
name: 'EventsOrder',
|
|
194
|
+
values: [{ value: 'asc' }, { value: 'desc' }],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'OrganizationsOrder',
|
|
198
|
+
values: [{ value: 'asc' }, { value: 'desc' }],
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const files = generateEnums(enums as never, ctxFor([], enums as never));
|
|
203
|
+
const sortOrder = files.find((file) => file.path.endsWith('/SortOrder.kt'));
|
|
204
|
+
const aliases = files.filter((file) => file.path.endsWith('Order.kt') && !file.path.endsWith('/SortOrder.kt'));
|
|
205
|
+
|
|
206
|
+
expect(sortOrder).toBeDefined();
|
|
207
|
+
expect(sortOrder!.content).toContain('enum class SortOrder');
|
|
208
|
+
expect(aliases.length).toBeLessThanOrEqual(1);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateTests } from '../../src/kotlin/tests.js';
|
|
3
|
+
import { generateEnums } from '../../src/kotlin/enums.js';
|
|
4
|
+
import type { EmitterContext, ApiSpec, Service, Model, ResolvedOperation } 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
|
+
function buildResolvedOps(services: Service[]): ResolvedOperation[] {
|
|
58
|
+
return services.flatMap((svc) =>
|
|
59
|
+
svc.operations.map((op) => ({
|
|
60
|
+
service: svc,
|
|
61
|
+
operation: op,
|
|
62
|
+
methodName: op.name,
|
|
63
|
+
mountOn: svc.name,
|
|
64
|
+
})),
|
|
65
|
+
) as ResolvedOperation[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const ctx: EmitterContext = {
|
|
69
|
+
namespace: 'workos',
|
|
70
|
+
namespacePascal: 'WorkOS',
|
|
71
|
+
spec,
|
|
72
|
+
resolvedOperations: buildResolvedOps(services),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
describe('kotlin/tests', () => {
|
|
76
|
+
it('generates per-mount-group test files', () => {
|
|
77
|
+
generateEnums([], ctx);
|
|
78
|
+
const files = generateTests(spec, ctx);
|
|
79
|
+
const testFile = files.find((f) => f.path.includes('OrganizationsTest.kt'));
|
|
80
|
+
expect(testFile).toBeDefined();
|
|
81
|
+
|
|
82
|
+
const content = testFile!.content;
|
|
83
|
+
expect(content).toContain('class OrganizationsTest');
|
|
84
|
+
expect(content).toContain('TestBase');
|
|
85
|
+
expect(content).toContain('@Test');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('generates happy-path test for void/delete methods', () => {
|
|
89
|
+
generateEnums([], ctx);
|
|
90
|
+
const files = generateTests(spec, ctx);
|
|
91
|
+
const testFile = files.find((f) => f.path.includes('OrganizationsTest.kt'))!;
|
|
92
|
+
const content = testFile.content;
|
|
93
|
+
|
|
94
|
+
// Delete method should have an active test, not a @Disabled placeholder.
|
|
95
|
+
// Method name is trimmed from deleteOrganization -> delete by resolveMethodName.
|
|
96
|
+
expect(content).toContain('delete completes without throwing');
|
|
97
|
+
expect(content).not.toContain('@Disabled("generator: could not synthesize required arguments for delete")');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('generates field-value assertions for non-void responses', () => {
|
|
101
|
+
generateEnums([], ctx);
|
|
102
|
+
const files = generateTests(spec, ctx);
|
|
103
|
+
const testFile = files.find((f) => f.path.includes('OrganizationsTest.kt'))!;
|
|
104
|
+
const content = testFile.content;
|
|
105
|
+
|
|
106
|
+
// GET method returning Organization should assert field values
|
|
107
|
+
expect(content).toContain('assertEquals("sample", result.id)');
|
|
108
|
+
expect(content).toContain('assertEquals("sample", result.name)');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('generates error-mapping tests', () => {
|
|
112
|
+
generateEnums([], ctx);
|
|
113
|
+
const files = generateTests(spec, ctx);
|
|
114
|
+
const testFile = files.find((f) => f.path.includes('OrganizationsTest.kt'))!;
|
|
115
|
+
const content = testFile.content;
|
|
116
|
+
|
|
117
|
+
expect(content).toContain('UnauthorizedException');
|
|
118
|
+
expect(content).toContain('NotFoundException');
|
|
119
|
+
expect(content).toContain('RateLimitException');
|
|
120
|
+
expect(content).toContain('GenericServerException');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('generates round-trip test using synthJson for broader coverage', () => {
|
|
124
|
+
generateEnums([], ctx);
|
|
125
|
+
const files = generateTests(spec, ctx);
|
|
126
|
+
const roundTrip = files.find((f) => f.path.includes('GeneratedModelRoundTripTest.kt'));
|
|
127
|
+
expect(roundTrip).toBeDefined();
|
|
128
|
+
|
|
129
|
+
const content = roundTrip!.content;
|
|
130
|
+
expect(content).toContain('Organization round-trips through Jackson');
|
|
131
|
+
expect(content).toContain('assertEquals(tree1, tree2)');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('generates forward-compat test with OffsetDateTime round-trip', () => {
|
|
135
|
+
generateEnums([], ctx);
|
|
136
|
+
const files = generateTests(spec, ctx);
|
|
137
|
+
const fwdCompat = files.find((f) => f.path.includes('GeneratedForwardCompatTest.kt'));
|
|
138
|
+
expect(fwdCompat).toBeDefined();
|
|
139
|
+
|
|
140
|
+
const content = fwdCompat!.content;
|
|
141
|
+
expect(content).toContain('OffsetDateTime round-trips');
|
|
142
|
+
expect(content).toContain('assertEquals(parsed.toInstant(), reparsed.toInstant())');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('emits valid ISO-8601 for date-time fields in round-trip fixtures', () => {
|
|
146
|
+
const dtModels: Model[] = [
|
|
147
|
+
{
|
|
148
|
+
name: 'Event',
|
|
149
|
+
fields: [
|
|
150
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
151
|
+
{
|
|
152
|
+
name: 'created_at',
|
|
153
|
+
type: { kind: 'primitive', type: 'string', format: 'date-time' },
|
|
154
|
+
required: true,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
const dtSpec: ApiSpec = { ...spec, models: dtModels };
|
|
160
|
+
const dtCtx: EmitterContext = {
|
|
161
|
+
...ctx,
|
|
162
|
+
spec: dtSpec,
|
|
163
|
+
resolvedOperations: buildResolvedOps(services),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
generateEnums([], dtCtx);
|
|
167
|
+
const files = generateTests(dtSpec, dtCtx);
|
|
168
|
+
const roundTrip = files.find((f) => f.path.includes('GeneratedModelRoundTripTest.kt'));
|
|
169
|
+
expect(roundTrip).toBeDefined();
|
|
170
|
+
|
|
171
|
+
const content = roundTrip!.content;
|
|
172
|
+
// Should use ISO-8601 timestamp, not "sample"
|
|
173
|
+
expect(content).toContain('2024-01-01T00:00:00Z');
|
|
174
|
+
expect(content).not.toMatch(/created_at.*"sample"/);
|
|
175
|
+
});
|
|
176
|
+
});
|
package/test/node/client.test.ts
CHANGED
|
@@ -194,6 +194,80 @@ describe('generateClient', () => {
|
|
|
194
194
|
expect(serviceBarrel!.content).toContain("export * from './authentication-factor.interface';");
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
+
it('propagates @deprecated from baseline service class to the property declaration', () => {
|
|
198
|
+
// Regression test for PR #1535 reviewer comment r3075509969: when a
|
|
199
|
+
// service class has `@deprecated` in its JSDoc, TS's deprecation-lint
|
|
200
|
+
// only fires at `new X()` call sites — not at `workos.x` access unless
|
|
201
|
+
// the property itself is annotated. The emitter propagates the class
|
|
202
|
+
// deprecation to the property JSDoc so IDEs surface the strikethrough
|
|
203
|
+
// on every access.
|
|
204
|
+
const deprecatedCtx: EmitterContext = {
|
|
205
|
+
...ctx,
|
|
206
|
+
apiSurface: {
|
|
207
|
+
language: 'node',
|
|
208
|
+
extractedFrom: 'test',
|
|
209
|
+
extractedAt: '2026-01-01',
|
|
210
|
+
classes: {
|
|
211
|
+
Organizations: {
|
|
212
|
+
name: 'Organizations',
|
|
213
|
+
methods: {},
|
|
214
|
+
properties: {},
|
|
215
|
+
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
216
|
+
deprecationMessage: 'Use `workos.connect` instead.',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
interfaces: {},
|
|
220
|
+
typeAliases: {},
|
|
221
|
+
enums: {},
|
|
222
|
+
exports: {},
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const files = generateClient(spec, deprecatedCtx);
|
|
227
|
+
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
228
|
+
const content = workosFile.content;
|
|
229
|
+
|
|
230
|
+
// Property JSDoc carries the deprecation and the directive is preserved
|
|
231
|
+
// on the line immediately above the accessor.
|
|
232
|
+
expect(content).toMatch(
|
|
233
|
+
/\/\*\* @deprecated Use `workos\.connect` instead\. \*\/\s+readonly organizations = new Organizations\(this\);/,
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('emits a bare @deprecated when the baseline class deprecation has no message', () => {
|
|
238
|
+
const deprecatedCtx: EmitterContext = {
|
|
239
|
+
...ctx,
|
|
240
|
+
apiSurface: {
|
|
241
|
+
language: 'node',
|
|
242
|
+
extractedFrom: 'test',
|
|
243
|
+
extractedAt: '2026-01-01',
|
|
244
|
+
classes: {
|
|
245
|
+
Organizations: {
|
|
246
|
+
name: 'Organizations',
|
|
247
|
+
methods: {},
|
|
248
|
+
properties: {},
|
|
249
|
+
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
250
|
+
deprecationMessage: '',
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
interfaces: {},
|
|
254
|
+
typeAliases: {},
|
|
255
|
+
enums: {},
|
|
256
|
+
exports: {},
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const files = generateClient(spec, deprecatedCtx);
|
|
261
|
+
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
262
|
+
expect(workosFile.content).toMatch(/\/\*\* @deprecated \*\/\s+readonly organizations = new Organizations\(this\);/);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('does not emit @deprecated when the baseline class has no deprecationMessage', () => {
|
|
266
|
+
const files = generateClient(spec, ctx);
|
|
267
|
+
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
268
|
+
expect(workosFile.content).not.toContain('@deprecated');
|
|
269
|
+
});
|
|
270
|
+
|
|
197
271
|
it('does not generate error handling in WorkOS client (lives in WorkOSBase)', () => {
|
|
198
272
|
const files = generateClient(spec, ctx);
|
|
199
273
|
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
package/test/node/models.test.ts
CHANGED
|
@@ -182,6 +182,23 @@ describe('generateModels', () => {
|
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
it('handles generic type params', () => {
|
|
185
|
+
const service: Service = {
|
|
186
|
+
name: 'DirectorySync',
|
|
187
|
+
operations: [
|
|
188
|
+
{
|
|
189
|
+
name: 'getDirectoryUser',
|
|
190
|
+
httpMethod: 'get',
|
|
191
|
+
path: '/directory_users/{id}',
|
|
192
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
193
|
+
queryParams: [],
|
|
194
|
+
headerParams: [],
|
|
195
|
+
response: { kind: 'model', name: 'DirectoryUser' },
|
|
196
|
+
errors: [],
|
|
197
|
+
injectIdempotencyKey: false,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
|
|
185
202
|
const models: Model[] = [
|
|
186
203
|
{
|
|
187
204
|
name: 'DirectoryUser',
|
|
@@ -204,7 +221,12 @@ describe('generateModels', () => {
|
|
|
204
221
|
},
|
|
205
222
|
];
|
|
206
223
|
|
|
207
|
-
const
|
|
224
|
+
const ctxWithServices: EmitterContext = {
|
|
225
|
+
...ctx,
|
|
226
|
+
spec: { ...emptySpec, services: [service], models },
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const files = generateModels(models, ctxWithServices);
|
|
208
230
|
expect(files[0].content).toContain('export interface DirectoryUser<TCustom = Record<string, any>> {');
|
|
209
231
|
expect(files[0].content).toContain('export interface DirectoryUserResponse<TCustom = Record<string, any>> {');
|
|
210
232
|
});
|
|
@@ -733,6 +755,23 @@ describe('model deduplication', () => {
|
|
|
733
755
|
errors: [],
|
|
734
756
|
injectIdempotencyKey: false,
|
|
735
757
|
},
|
|
758
|
+
{
|
|
759
|
+
name: 'getOrganizationRole',
|
|
760
|
+
httpMethod: 'get',
|
|
761
|
+
path: '/organization_roles/{id}',
|
|
762
|
+
pathParams: [
|
|
763
|
+
{
|
|
764
|
+
name: 'id',
|
|
765
|
+
type: { kind: 'primitive', type: 'string' },
|
|
766
|
+
required: true,
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
queryParams: [],
|
|
770
|
+
headerParams: [],
|
|
771
|
+
response: { kind: 'model', name: 'OrganizationRole' },
|
|
772
|
+
errors: [],
|
|
773
|
+
injectIdempotencyKey: false,
|
|
774
|
+
},
|
|
736
775
|
],
|
|
737
776
|
};
|
|
738
777
|
|
|
@@ -794,4 +833,98 @@ describe('model deduplication', () => {
|
|
|
794
833
|
expect(files[1].content).toContain('export type OrganizationRole = EnvironmentRole');
|
|
795
834
|
expect(files[1].content).toContain('export type OrganizationRoleResponse = EnvironmentRoleResponse');
|
|
796
835
|
});
|
|
836
|
+
|
|
837
|
+
it('generates Date type for date-time fields even when baseline says string', () => {
|
|
838
|
+
const service: Service = {
|
|
839
|
+
name: 'Authorization',
|
|
840
|
+
operations: [
|
|
841
|
+
{
|
|
842
|
+
name: 'getRoleAssignment',
|
|
843
|
+
httpMethod: 'get',
|
|
844
|
+
path: '/role_assignments/{id}',
|
|
845
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
846
|
+
queryParams: [],
|
|
847
|
+
headerParams: [],
|
|
848
|
+
response: { kind: 'model', name: 'RoleAssignment' },
|
|
849
|
+
errors: [],
|
|
850
|
+
injectIdempotencyKey: false,
|
|
851
|
+
},
|
|
852
|
+
],
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const models: Model[] = [
|
|
856
|
+
{
|
|
857
|
+
name: 'RoleAssignment',
|
|
858
|
+
fields: [
|
|
859
|
+
{
|
|
860
|
+
name: 'id',
|
|
861
|
+
type: { kind: 'primitive', type: 'string' },
|
|
862
|
+
required: true,
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
name: 'created_at',
|
|
866
|
+
type: { kind: 'primitive', type: 'string', format: 'date-time' },
|
|
867
|
+
required: true,
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
name: 'updated_at',
|
|
871
|
+
type: { kind: 'primitive', type: 'string', format: 'date-time' },
|
|
872
|
+
required: true,
|
|
873
|
+
},
|
|
874
|
+
// Extra field not in baseline so modelHasNewFields returns true
|
|
875
|
+
// (allowing the dedup test to proceed with generation)
|
|
876
|
+
{
|
|
877
|
+
name: 'role_name',
|
|
878
|
+
type: { kind: 'primitive', type: 'string' },
|
|
879
|
+
required: true,
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
},
|
|
883
|
+
];
|
|
884
|
+
|
|
885
|
+
const ctxWithBaseline: EmitterContext = {
|
|
886
|
+
...ctx,
|
|
887
|
+
spec: { ...emptySpec, services: [service], models },
|
|
888
|
+
apiSurface: {
|
|
889
|
+
language: 'node',
|
|
890
|
+
extractedFrom: 'test',
|
|
891
|
+
extractedAt: '2024-01-01',
|
|
892
|
+
classes: {},
|
|
893
|
+
typeAliases: {},
|
|
894
|
+
enums: {},
|
|
895
|
+
exports: {},
|
|
896
|
+
interfaces: {
|
|
897
|
+
RoleAssignment: {
|
|
898
|
+
name: 'RoleAssignment',
|
|
899
|
+
fields: {
|
|
900
|
+
id: { name: 'id', type: 'string', optional: false },
|
|
901
|
+
createdAt: { name: 'createdAt', type: 'string', optional: false },
|
|
902
|
+
updatedAt: { name: 'updatedAt', type: 'string', optional: false },
|
|
903
|
+
},
|
|
904
|
+
extends: [],
|
|
905
|
+
},
|
|
906
|
+
RoleAssignmentResponse: {
|
|
907
|
+
name: 'RoleAssignmentResponse',
|
|
908
|
+
fields: {
|
|
909
|
+
id: { name: 'id', type: 'string', optional: false },
|
|
910
|
+
created_at: { name: 'created_at', type: 'string', optional: false },
|
|
911
|
+
updated_at: { name: 'updated_at', type: 'string', optional: false },
|
|
912
|
+
},
|
|
913
|
+
extends: [],
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
const files = generateModels(models, ctxWithBaseline);
|
|
920
|
+
const content = files[0].content;
|
|
921
|
+
|
|
922
|
+
// Domain interface should use Date, not string from baseline
|
|
923
|
+
expect(content).toContain(' createdAt: Date;');
|
|
924
|
+
expect(content).toContain(' updatedAt: Date;');
|
|
925
|
+
|
|
926
|
+
// Wire interface should stay as string (JSON native)
|
|
927
|
+
expect(content).toContain(' created_at: string;');
|
|
928
|
+
expect(content).toContain(' updated_at: string;');
|
|
929
|
+
});
|
|
797
930
|
});
|