@workos/oagen-emitters 0.3.0 → 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.
Files changed (65) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +7 -0
  3. package/dist/index.d.mts +4 -1
  4. package/dist/index.d.mts.map +1 -1
  5. package/dist/index.mjs +3288 -791
  6. package/dist/index.mjs.map +1 -1
  7. package/docs/sdk-architecture/dotnet.md +336 -0
  8. package/oagen.config.ts +42 -12
  9. package/package.json +2 -2
  10. package/smoke/sdk-dotnet.ts +45 -12
  11. package/src/dotnet/client.ts +89 -0
  12. package/src/dotnet/enums.ts +323 -0
  13. package/src/dotnet/fixtures.ts +236 -0
  14. package/src/dotnet/index.ts +246 -0
  15. package/src/dotnet/manifest.ts +36 -0
  16. package/src/dotnet/models.ts +344 -0
  17. package/src/dotnet/naming.ts +330 -0
  18. package/src/dotnet/resources.ts +622 -0
  19. package/src/dotnet/tests.ts +693 -0
  20. package/src/dotnet/type-map.ts +201 -0
  21. package/src/dotnet/wrappers.ts +186 -0
  22. package/src/go/index.ts +5 -2
  23. package/src/go/naming.ts +5 -17
  24. package/src/index.ts +1 -0
  25. package/src/kotlin/client.ts +53 -0
  26. package/src/kotlin/enums.ts +162 -0
  27. package/src/kotlin/index.ts +92 -0
  28. package/src/kotlin/manifest.ts +55 -0
  29. package/src/kotlin/models.ts +395 -0
  30. package/src/kotlin/naming.ts +223 -0
  31. package/src/kotlin/overrides.ts +25 -0
  32. package/src/kotlin/resources.ts +667 -0
  33. package/src/kotlin/tests.ts +1019 -0
  34. package/src/kotlin/type-map.ts +123 -0
  35. package/src/kotlin/wrappers.ts +168 -0
  36. package/src/node/client.ts +50 -0
  37. package/src/node/index.ts +1 -0
  38. package/src/node/resources.ts +164 -44
  39. package/src/node/tests.ts +37 -7
  40. package/src/php/client.ts +11 -3
  41. package/src/php/naming.ts +2 -21
  42. package/src/php/resources.ts +81 -6
  43. package/src/php/tests.ts +93 -17
  44. package/src/php/wrappers.ts +1 -0
  45. package/src/python/client.ts +37 -29
  46. package/src/python/enums.ts +7 -7
  47. package/src/python/models.ts +1 -1
  48. package/src/python/naming.ts +2 -22
  49. package/src/shared/model-utils.ts +232 -15
  50. package/src/shared/naming-utils.ts +47 -0
  51. package/src/shared/wrapper-utils.ts +12 -1
  52. package/test/dotnet/client.test.ts +121 -0
  53. package/test/dotnet/enums.test.ts +193 -0
  54. package/test/dotnet/errors.test.ts +9 -0
  55. package/test/dotnet/manifest.test.ts +82 -0
  56. package/test/dotnet/models.test.ts +260 -0
  57. package/test/dotnet/resources.test.ts +255 -0
  58. package/test/dotnet/tests.test.ts +202 -0
  59. package/test/kotlin/models.test.ts +135 -0
  60. package/test/kotlin/tests.test.ts +176 -0
  61. package/test/node/client.test.ts +74 -0
  62. package/test/node/resources.test.ts +216 -15
  63. package/test/php/client.test.ts +2 -1
  64. package/test/php/resources.test.ts +38 -0
  65. package/test/php/tests.test.ts +67 -0
@@ -0,0 +1,255 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateResources } from '../../src/dotnet/resources.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 emptySpec: ApiSpec = {
8
+ name: 'Test',
9
+ version: '1.0.0',
10
+ baseUrl: '',
11
+ services: [],
12
+ models: [],
13
+ enums: [],
14
+ sdk: defaultSdkBehavior(),
15
+ };
16
+
17
+ const ctx: EmitterContext = {
18
+ namespace: 'workos',
19
+ namespacePascal: 'WorkOS',
20
+ spec: emptySpec,
21
+ };
22
+
23
+ describe('dotnet/resources', () => {
24
+ it('returns empty for no services', () => {
25
+ primeEnumAliases([]);
26
+ expect(generateResources([], ctx)).toEqual([]);
27
+ });
28
+
29
+ it('generates a service class with methods', () => {
30
+ const models: Model[] = [
31
+ {
32
+ name: 'Organization',
33
+ fields: [
34
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
35
+ { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
36
+ ],
37
+ },
38
+ ];
39
+
40
+ const services: Service[] = [
41
+ {
42
+ name: 'Organizations',
43
+ operations: [
44
+ {
45
+ name: 'getOrganization',
46
+ httpMethod: 'get',
47
+ path: '/organizations/{id}',
48
+ pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
49
+ queryParams: [],
50
+ headerParams: [],
51
+ response: { kind: 'model', name: 'Organization' },
52
+ errors: [],
53
+ injectIdempotencyKey: false,
54
+ },
55
+ {
56
+ name: 'deleteOrganization',
57
+ httpMethod: 'delete',
58
+ path: '/organizations/{id}',
59
+ pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
60
+ queryParams: [],
61
+ headerParams: [],
62
+ response: { kind: 'primitive', type: 'unknown' },
63
+ errors: [],
64
+ injectIdempotencyKey: false,
65
+ },
66
+ ],
67
+ },
68
+ ];
69
+
70
+ primeEnumAliases([]);
71
+ const ctxWithServices: EmitterContext = {
72
+ ...ctx,
73
+ spec: { ...emptySpec, services, models },
74
+ };
75
+
76
+ const files = generateResources(services, ctxWithServices);
77
+ expect(files.length).toBeGreaterThanOrEqual(1);
78
+
79
+ const serviceFile = files.find((f) => f.path.includes('OrganizationsService.cs'))!;
80
+ expect(serviceFile).toBeDefined();
81
+
82
+ const content = serviceFile.content;
83
+ // Namespace and class
84
+ expect(content).toContain('namespace WorkOS');
85
+ expect(content).toContain('public class OrganizationsService : Service');
86
+
87
+ // GET method
88
+ expect(content).toContain('GetAsync');
89
+ expect(content).toContain('async Task');
90
+
91
+ // DELETE method
92
+ expect(content).toContain('DeleteAsync');
93
+ });
94
+
95
+ it('generates options classes for operations with params', () => {
96
+ const models: Model[] = [
97
+ {
98
+ name: 'Organization',
99
+ fields: [
100
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
101
+ { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
102
+ ],
103
+ },
104
+ {
105
+ name: 'CreateOrganizationRequest',
106
+ fields: [{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true }],
107
+ },
108
+ ];
109
+
110
+ const services: Service[] = [
111
+ {
112
+ name: 'Organizations',
113
+ operations: [
114
+ {
115
+ name: 'createOrganization',
116
+ httpMethod: 'post',
117
+ path: '/organizations',
118
+ pathParams: [],
119
+ queryParams: [],
120
+ headerParams: [],
121
+ requestBody: { kind: 'model', name: 'CreateOrganizationRequest' },
122
+ response: { kind: 'model', name: 'Organization' },
123
+ errors: [],
124
+ injectIdempotencyKey: false,
125
+ },
126
+ ],
127
+ },
128
+ ];
129
+
130
+ primeEnumAliases([]);
131
+ const ctxWithServices: EmitterContext = {
132
+ ...ctx,
133
+ spec: { ...emptySpec, services, models },
134
+ };
135
+
136
+ const files = generateResources(services, ctxWithServices);
137
+ const optionsFile = files.find((f) => f.path.includes('Options.cs'))!;
138
+ expect(optionsFile).toBeDefined();
139
+
140
+ const content = optionsFile.content;
141
+ expect(content).toContain('Options');
142
+ expect(content).toContain('[JsonProperty("name")]');
143
+ expect(content).toContain('public string Name');
144
+ });
145
+
146
+ it('generates paginated list method with auto-pagination', () => {
147
+ const models: Model[] = [
148
+ {
149
+ name: 'Organization',
150
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
151
+ },
152
+ {
153
+ name: 'OrganizationList',
154
+ fields: [
155
+ {
156
+ name: 'data',
157
+ type: { kind: 'array', items: { kind: 'model', name: 'Organization' } },
158
+ required: true,
159
+ },
160
+ {
161
+ name: 'list_metadata',
162
+ type: { kind: 'model', name: 'ListMetadata' },
163
+ required: true,
164
+ },
165
+ ],
166
+ },
167
+ ];
168
+
169
+ const services: Service[] = [
170
+ {
171
+ name: 'Organizations',
172
+ operations: [
173
+ {
174
+ name: 'listOrganizations',
175
+ httpMethod: 'get',
176
+ path: '/organizations',
177
+ pathParams: [],
178
+ queryParams: [
179
+ { name: 'limit', type: { kind: 'primitive', type: 'integer' }, required: false },
180
+ { name: 'after', type: { kind: 'primitive', type: 'string' }, required: false },
181
+ ],
182
+ headerParams: [],
183
+ response: { kind: 'model', name: 'OrganizationList' },
184
+ errors: [],
185
+ injectIdempotencyKey: false,
186
+ pagination: {
187
+ strategy: 'cursor',
188
+ param: 'after',
189
+ dataPath: 'data',
190
+ itemType: { kind: 'model', name: 'Organization' },
191
+ },
192
+ },
193
+ ],
194
+ },
195
+ ];
196
+
197
+ primeEnumAliases([]);
198
+ const ctxWithServices: EmitterContext = {
199
+ ...ctx,
200
+ spec: { ...emptySpec, services, models },
201
+ };
202
+
203
+ const files = generateResources(services, ctxWithServices);
204
+ const serviceFile = files.find((f) => f.path.includes('OrganizationsService.cs'))!;
205
+ const content = serviceFile.content;
206
+
207
+ // List method (public method name omits Async suffix; return type is async Task)
208
+ expect(content).toContain('async Task<WorkOSList<Organization>>');
209
+ expect(content).toContain('List(');
210
+
211
+ // Auto-pagination method
212
+ expect(content).toContain('ListAutoPagingAsync');
213
+ expect(content).toContain('IAsyncEnumerable<Organization>');
214
+ });
215
+
216
+ it('generates deprecated operations with Obsolete attribute', () => {
217
+ const models: Model[] = [
218
+ {
219
+ name: 'Organization',
220
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
221
+ },
222
+ ];
223
+
224
+ const services: Service[] = [
225
+ {
226
+ name: 'Organizations',
227
+ operations: [
228
+ {
229
+ name: 'getOrganization',
230
+ httpMethod: 'get',
231
+ path: '/organizations/{id}',
232
+ pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
233
+ queryParams: [],
234
+ headerParams: [],
235
+ response: { kind: 'model', name: 'Organization' },
236
+ errors: [],
237
+ injectIdempotencyKey: false,
238
+ deprecated: true,
239
+ },
240
+ ],
241
+ },
242
+ ];
243
+
244
+ primeEnumAliases([]);
245
+ const ctxWithServices: EmitterContext = {
246
+ ...ctx,
247
+ spec: { ...emptySpec, services, models },
248
+ };
249
+
250
+ const files = generateResources(services, ctxWithServices);
251
+ const serviceFile = files.find((f) => f.path.includes('OrganizationsService.cs'))!;
252
+
253
+ expect(serviceFile.content).toContain('[System.Obsolete');
254
+ });
255
+ });
@@ -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,135 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateModels } from '../../src/kotlin/models.js';
3
+ import { generateEnums } from '../../src/kotlin/enums.js';
4
+ import type { EmitterContext, ApiSpec, Model } from '@workos/oagen';
5
+ import { defaultSdkBehavior } from '@workos/oagen';
6
+
7
+ const emptySpec: ApiSpec = {
8
+ name: 'Test',
9
+ version: '1.0.0',
10
+ baseUrl: '',
11
+ services: [],
12
+ models: [],
13
+ enums: [],
14
+ sdk: defaultSdkBehavior(),
15
+ };
16
+
17
+ const ctx: EmitterContext = {
18
+ namespace: 'workos',
19
+ namespacePascal: 'WorkOS',
20
+ spec: emptySpec,
21
+ };
22
+
23
+ describe('kotlin/models', () => {
24
+ it('returns empty for no models', () => {
25
+ generateEnums([], ctx);
26
+ expect(generateModels([], ctx)).toEqual([]);
27
+ });
28
+
29
+ it('generates a Kotlin data class with Jackson annotations', () => {
30
+ const models: Model[] = [
31
+ {
32
+ name: 'Organization',
33
+ fields: [
34
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
35
+ { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
36
+ {
37
+ name: 'created_at',
38
+ type: { kind: 'primitive', type: 'string', format: 'date-time' },
39
+ required: true,
40
+ },
41
+ {
42
+ name: 'external_id',
43
+ type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
44
+ required: false,
45
+ },
46
+ ],
47
+ },
48
+ ];
49
+
50
+ generateEnums([], ctx);
51
+ const files = generateModels(models, { ...ctx, spec: { ...emptySpec, models } });
52
+
53
+ expect(files.length).toBeGreaterThanOrEqual(1);
54
+ const modelFile = files.find((f) => f.path.includes('Organization.kt'))!;
55
+ expect(modelFile).toBeDefined();
56
+
57
+ const content = modelFile.content;
58
+ expect(content).toContain('data class Organization');
59
+ expect(content).toContain('@JsonProperty("id")');
60
+ expect(content).toContain('@JvmField');
61
+ expect(content).toContain('OffsetDateTime');
62
+ expect(content).toContain('externalId: String?');
63
+ });
64
+
65
+ it('skips list wrapper and list metadata models', () => {
66
+ const models: Model[] = [
67
+ {
68
+ name: 'Organization',
69
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
70
+ },
71
+ {
72
+ name: 'OrganizationList',
73
+ fields: [
74
+ {
75
+ name: 'data',
76
+ type: { kind: 'array', items: { kind: 'model', name: 'Organization' } },
77
+ required: true,
78
+ },
79
+ {
80
+ name: 'list_metadata',
81
+ type: { kind: 'model', name: 'ListMetadata' },
82
+ required: true,
83
+ },
84
+ ],
85
+ },
86
+ {
87
+ name: 'ListMetadata',
88
+ fields: [
89
+ { name: 'before', type: { kind: 'primitive', type: 'string' }, required: false },
90
+ { name: 'after', type: { kind: 'primitive', type: 'string' }, required: false },
91
+ ],
92
+ },
93
+ ];
94
+
95
+ generateEnums([], ctx);
96
+ const files = generateModels(models, { ...ctx, spec: { ...emptySpec, models } });
97
+ const filePaths = files.map((f) => f.path);
98
+
99
+ expect(filePaths.some((p) => p.includes('Organization.kt') && !p.includes('List'))).toBe(true);
100
+ expect(filePaths.some((p) => p.includes('OrganizationList.kt'))).toBe(false);
101
+ expect(filePaths.some((p) => p.includes('ListMetadata.kt'))).toBe(false);
102
+ });
103
+
104
+ it('deduplicates structurally identical models preferring shorter names', () => {
105
+ const models: Model[] = [
106
+ {
107
+ name: 'EmailChangeConfirmationUser',
108
+ fields: [
109
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
110
+ { name: 'email', type: { kind: 'primitive', type: 'string' }, required: true },
111
+ ],
112
+ },
113
+ {
114
+ name: 'User',
115
+ fields: [
116
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
117
+ { name: 'email', type: { kind: 'primitive', type: 'string' }, required: true },
118
+ ],
119
+ },
120
+ ];
121
+
122
+ generateEnums([], ctx);
123
+ const files = generateModels(models, { ...ctx, spec: { ...emptySpec, models } });
124
+
125
+ // User should be the canonical (shorter name) — a data class
126
+ const userFile = files.find((f) => f.path.includes('/User.kt'))!;
127
+ expect(userFile).toBeDefined();
128
+ expect(userFile.content).toContain('data class User');
129
+
130
+ // EmailChangeConfirmationUser should be the typealias
131
+ const aliasFile = files.find((f) => f.path.includes('/EmailChangeConfirmationUser.kt'))!;
132
+ expect(aliasFile).toBeDefined();
133
+ expect(aliasFile.content).toContain('typealias EmailChangeConfirmationUser = User');
134
+ });
135
+ });