@contractkit/openapi-to-ck 0.7.1
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/.turbo/turbo-build$colon$ci.log +40 -0
- package/.turbo/turbo-build.log +20 -0
- package/.turbo/turbo-format.log +36 -0
- package/.turbo/turbo-test$colon$ci.log +77 -0
- package/.turbo/turbo-test.log +19 -0
- package/CHANGELOG.md +108 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +878 -0
- package/coverage/coverage-final.json +10 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/ast-to-ck.ts.html +1336 -0
- package/coverage/src/circular-refs.ts.html +298 -0
- package/coverage/src/convert.ts.html +535 -0
- package/coverage/src/index.html +221 -0
- package/coverage/src/normalize.ts.html +1162 -0
- package/coverage/src/paths-to-ast.ts.html +1165 -0
- package/coverage/src/schema-to-ast.ts.html +1378 -0
- package/coverage/src/tag-splitter.ts.html +685 -0
- package/coverage/src/warnings.ts.html +154 -0
- package/coverage/tests/helpers.ts.html +559 -0
- package/coverage/tests/index.html +116 -0
- package/dist/ast-to-ck.d.ts +8 -0
- package/dist/ast-to-ck.d.ts.map +1 -0
- package/dist/chunk-CQGFQKBJ.js +1681 -0
- package/dist/chunk-CQGFQKBJ.js.map +1 -0
- package/dist/chunk-FNUU2DWY.js +1779 -0
- package/dist/chunk-FNUU2DWY.js.map +1 -0
- package/dist/chunk-LD2HAFZG.js +1681 -0
- package/dist/chunk-LD2HAFZG.js.map +1 -0
- package/dist/chunk-LQ2B3EJG.js +1777 -0
- package/dist/chunk-LQ2B3EJG.js.map +1 -0
- package/dist/chunk-M6JA6WY2.js +1714 -0
- package/dist/chunk-M6JA6WY2.js.map +1 -0
- package/dist/chunk-MQTKLXTN.js +1681 -0
- package/dist/chunk-MQTKLXTN.js.map +1 -0
- package/dist/chunk-NV7RGUUS.js +1699 -0
- package/dist/chunk-NV7RGUUS.js.map +1 -0
- package/dist/chunk-REM25FDE.js +1779 -0
- package/dist/chunk-REM25FDE.js.map +1 -0
- package/dist/chunk-SNW7GJOM.js +1681 -0
- package/dist/chunk-SNW7GJOM.js.map +1 -0
- package/dist/chunk-UWOSCBRG.js +1681 -0
- package/dist/chunk-UWOSCBRG.js.map +1 -0
- package/dist/chunk-YP7FWA4H.js +1779 -0
- package/dist/chunk-YP7FWA4H.js.map +1 -0
- package/dist/circular-refs.d.ts +12 -0
- package/dist/circular-refs.d.ts.map +1 -0
- package/dist/convert.d.ts +6 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/normalize.d.ts +12 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/paths-to-ast.d.ts +22 -0
- package/dist/paths-to-ast.d.ts.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +86 -0
- package/dist/plugin.js.map +1 -0
- package/dist/schema-to-ast.d.ts +39 -0
- package/dist/schema-to-ast.d.ts.map +1 -0
- package/dist/tag-splitter.d.ts +16 -0
- package/dist/tag-splitter.d.ts.map +1 -0
- package/dist/types.d.ts +137 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/warnings.d.ts +10 -0
- package/dist/warnings.d.ts.map +1 -0
- package/eslint.config.js +6 -0
- package/package.json +46 -0
- package/src/ast-to-ck.ts +417 -0
- package/src/circular-refs.ts +71 -0
- package/src/convert.ts +150 -0
- package/src/index.ts +8 -0
- package/src/normalize.ts +359 -0
- package/src/paths-to-ast.ts +360 -0
- package/src/plugin.ts +89 -0
- package/src/schema-to-ast.ts +431 -0
- package/src/tag-splitter.ts +200 -0
- package/src/types.ts +133 -0
- package/src/warnings.ts +23 -0
- package/tests/ast-to-ck.test.ts +425 -0
- package/tests/circular-refs.test.ts +71 -0
- package/tests/convert.test.ts +330 -0
- package/tests/fixtures/petstore-3.1.json +187 -0
- package/tests/helpers.ts +158 -0
- package/tests/normalize.test.ts +114 -0
- package/tests/schema-to-ast.test.ts +341 -0
- package/tests/tag-splitter.test.ts +75 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { normalize } from '../src/normalize.js';
|
|
3
|
+
import { WarningCollector } from '../src/warnings.js';
|
|
4
|
+
|
|
5
|
+
describe('normalize', () => {
|
|
6
|
+
describe('Swagger 2.0', () => {
|
|
7
|
+
it('converts definitions to components/schemas', () => {
|
|
8
|
+
const doc = {
|
|
9
|
+
swagger: '2.0',
|
|
10
|
+
info: { title: 'Test', version: '1.0' },
|
|
11
|
+
definitions: {
|
|
12
|
+
User: { type: 'object', properties: { id: { type: 'string' } } },
|
|
13
|
+
},
|
|
14
|
+
paths: {},
|
|
15
|
+
};
|
|
16
|
+
const result = normalize(doc, new WarningCollector());
|
|
17
|
+
expect(result.openapi).toBe('3.1.0');
|
|
18
|
+
expect(result.components?.schemas?.User).toBeDefined();
|
|
19
|
+
expect((result.components!.schemas!.User as Record<string, unknown>).type).toBe('object');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('converts body parameters to requestBody', () => {
|
|
23
|
+
const doc = {
|
|
24
|
+
swagger: '2.0',
|
|
25
|
+
info: { title: 'Test', version: '1.0' },
|
|
26
|
+
definitions: {},
|
|
27
|
+
paths: {
|
|
28
|
+
'/users': {
|
|
29
|
+
post: {
|
|
30
|
+
parameters: [{ in: 'body', name: 'body', schema: { $ref: '#/definitions/User' } }],
|
|
31
|
+
responses: { '200': { description: 'OK' } },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const result = normalize(doc, new WarningCollector());
|
|
37
|
+
const post = (result.paths!['/users'] as Record<string, unknown>).post as Record<string, unknown>;
|
|
38
|
+
expect(post.requestBody).toBeDefined();
|
|
39
|
+
const reqBody = post.requestBody as Record<string, unknown>;
|
|
40
|
+
expect(reqBody.content).toHaveProperty('application/json');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('converts securityDefinitions to components/securitySchemes', () => {
|
|
44
|
+
const doc = {
|
|
45
|
+
swagger: '2.0',
|
|
46
|
+
info: { title: 'Test', version: '1.0' },
|
|
47
|
+
definitions: {},
|
|
48
|
+
securityDefinitions: {
|
|
49
|
+
BasicAuth: { type: 'basic' },
|
|
50
|
+
ApiKey: { type: 'apiKey', name: 'X-API-Key', in: 'header' },
|
|
51
|
+
},
|
|
52
|
+
paths: {},
|
|
53
|
+
};
|
|
54
|
+
const result = normalize(doc, new WarningCollector());
|
|
55
|
+
const schemes = result.components!.securitySchemes!;
|
|
56
|
+
expect((schemes.BasicAuth as Record<string, unknown>).type).toBe('http');
|
|
57
|
+
expect((schemes.ApiKey as Record<string, unknown>).type).toBe('apiKey');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('builds server from host, basePath, schemes', () => {
|
|
61
|
+
const doc = {
|
|
62
|
+
swagger: '2.0',
|
|
63
|
+
info: { title: 'Test', version: '1.0' },
|
|
64
|
+
host: 'api.example.com',
|
|
65
|
+
basePath: '/v1',
|
|
66
|
+
schemes: ['https'],
|
|
67
|
+
paths: {},
|
|
68
|
+
};
|
|
69
|
+
const result = normalize(doc, new WarningCollector());
|
|
70
|
+
expect(result.servers![0]!.url).toBe('https://api.example.com/v1');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('OpenAPI 3.0', () => {
|
|
75
|
+
it('converts nullable: true to type array', () => {
|
|
76
|
+
const doc = {
|
|
77
|
+
openapi: '3.0.3',
|
|
78
|
+
info: { title: 'Test', version: '1.0' },
|
|
79
|
+
components: {
|
|
80
|
+
schemas: {
|
|
81
|
+
User: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
name: { type: 'string', nullable: true },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
const result = normalize(doc as Record<string, unknown>, new WarningCollector());
|
|
91
|
+
expect(result.openapi).toBe('3.1.0');
|
|
92
|
+
const nameSchema = (result.components!.schemas!.User as Record<string, Record<string, unknown>>).properties!.name;
|
|
93
|
+
expect(nameSchema.type).toEqual(['string', 'null']);
|
|
94
|
+
expect(nameSchema.nullable).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('OpenAPI 3.1', () => {
|
|
99
|
+
it('passes through 3.1 documents unchanged', () => {
|
|
100
|
+
const doc = {
|
|
101
|
+
openapi: '3.1.0',
|
|
102
|
+
info: { title: 'Test', version: '1.0' },
|
|
103
|
+
components: {
|
|
104
|
+
schemas: {
|
|
105
|
+
User: { type: 'object', properties: { id: { type: 'string' } } },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const result = normalize(doc as Record<string, unknown>, new WarningCollector());
|
|
110
|
+
expect(result.openapi).toBe('3.1.0');
|
|
111
|
+
expect(result.components?.schemas?.User).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { schemaToTypeNode, schemasToModels, sanitizeName } from '../src/schema-to-ast.js';
|
|
3
|
+
import type { SchemaContext } from '../src/schema-to-ast.js';
|
|
4
|
+
import { WarningCollector } from '../src/warnings.js';
|
|
5
|
+
import type { NormalizedSchema } from '../src/types.js';
|
|
6
|
+
|
|
7
|
+
function makeCtx(overrides?: Partial<SchemaContext>): SchemaContext {
|
|
8
|
+
return {
|
|
9
|
+
circularRefs: new Set(),
|
|
10
|
+
warnings: new WarningCollector(),
|
|
11
|
+
path: '#/test',
|
|
12
|
+
includeComments: true,
|
|
13
|
+
namedSchemas: {},
|
|
14
|
+
extractedModels: [],
|
|
15
|
+
inlineCounter: 0,
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('schemaToTypeNode', () => {
|
|
21
|
+
describe('scalar types', () => {
|
|
22
|
+
it('converts string → string', () => {
|
|
23
|
+
const result = schemaToTypeNode({ type: 'string' }, makeCtx());
|
|
24
|
+
expect(result).toEqual({ kind: 'scalar', name: 'string' });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('converts string with format email → email', () => {
|
|
28
|
+
const result = schemaToTypeNode({ type: 'string', format: 'email' }, makeCtx());
|
|
29
|
+
expect(result).toEqual({ kind: 'scalar', name: 'email' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('converts string with format uri → url', () => {
|
|
33
|
+
const result = schemaToTypeNode({ type: 'string', format: 'uri' }, makeCtx());
|
|
34
|
+
expect(result).toEqual({ kind: 'scalar', name: 'url' });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('converts string with format uuid → uuid', () => {
|
|
38
|
+
const result = schemaToTypeNode({ type: 'string', format: 'uuid' }, makeCtx());
|
|
39
|
+
expect(result).toEqual({ kind: 'scalar', name: 'uuid' });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('converts string with format date → date', () => {
|
|
43
|
+
const result = schemaToTypeNode({ type: 'string', format: 'date' }, makeCtx());
|
|
44
|
+
expect(result).toEqual({ kind: 'scalar', name: 'date' });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('converts string with format date-time → datetime', () => {
|
|
48
|
+
const result = schemaToTypeNode({ type: 'string', format: 'date-time' }, makeCtx());
|
|
49
|
+
expect(result).toEqual({ kind: 'scalar', name: 'datetime' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('converts string with format binary → binary', () => {
|
|
53
|
+
const result = schemaToTypeNode({ type: 'string', format: 'binary' }, makeCtx());
|
|
54
|
+
expect(result).toEqual({ kind: 'scalar', name: 'binary' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('converts string with minLength/maxLength', () => {
|
|
58
|
+
const result = schemaToTypeNode({ type: 'string', minLength: 1, maxLength: 100 }, makeCtx());
|
|
59
|
+
expect(result).toEqual({ kind: 'scalar', name: 'string', min: 1, max: 100 });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('converts string with equal min/max to len', () => {
|
|
63
|
+
const result = schemaToTypeNode({ type: 'string', minLength: 3, maxLength: 3 }, makeCtx());
|
|
64
|
+
expect(result).toEqual({ kind: 'scalar', name: 'string', len: 3 });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('converts string with pattern', () => {
|
|
68
|
+
const result = schemaToTypeNode({ type: 'string', pattern: '^[a-z]+$' }, makeCtx());
|
|
69
|
+
expect(result).toEqual({ kind: 'scalar', name: 'string', regex: '/^[a-z]+$/' });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('converts integer → int', () => {
|
|
73
|
+
const result = schemaToTypeNode({ type: 'integer' }, makeCtx());
|
|
74
|
+
expect(result).toEqual({ kind: 'scalar', name: 'int' });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('converts integer with format int64 → bigint', () => {
|
|
78
|
+
const result = schemaToTypeNode({ type: 'integer', format: 'int64' }, makeCtx());
|
|
79
|
+
expect(result).toEqual({ kind: 'scalar', name: 'bigint' });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('converts integer with min/max', () => {
|
|
83
|
+
const result = schemaToTypeNode({ type: 'integer', minimum: 0, maximum: 100 }, makeCtx());
|
|
84
|
+
expect(result).toEqual({ kind: 'scalar', name: 'int', min: 0, max: 100 });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('converts number → number', () => {
|
|
88
|
+
const result = schemaToTypeNode({ type: 'number' }, makeCtx());
|
|
89
|
+
expect(result).toEqual({ kind: 'scalar', name: 'number' });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('converts boolean → boolean', () => {
|
|
93
|
+
const result = schemaToTypeNode({ type: 'boolean' }, makeCtx());
|
|
94
|
+
expect(result).toEqual({ kind: 'scalar', name: 'boolean' });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('compound types', () => {
|
|
99
|
+
it('converts array with items', () => {
|
|
100
|
+
const result = schemaToTypeNode(
|
|
101
|
+
{
|
|
102
|
+
type: 'array',
|
|
103
|
+
items: { type: 'string' },
|
|
104
|
+
},
|
|
105
|
+
makeCtx(),
|
|
106
|
+
);
|
|
107
|
+
expect(result).toEqual({
|
|
108
|
+
kind: 'array',
|
|
109
|
+
item: { kind: 'scalar', name: 'string' },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('converts array with min/maxItems', () => {
|
|
114
|
+
const result = schemaToTypeNode(
|
|
115
|
+
{
|
|
116
|
+
type: 'array',
|
|
117
|
+
items: { type: 'string' },
|
|
118
|
+
minItems: 1,
|
|
119
|
+
maxItems: 10,
|
|
120
|
+
},
|
|
121
|
+
makeCtx(),
|
|
122
|
+
);
|
|
123
|
+
expect(result).toEqual({
|
|
124
|
+
kind: 'array',
|
|
125
|
+
item: { kind: 'scalar', name: 'string' },
|
|
126
|
+
min: 1,
|
|
127
|
+
max: 10,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('converts prefixItems to tuple', () => {
|
|
132
|
+
const result = schemaToTypeNode(
|
|
133
|
+
{
|
|
134
|
+
type: 'array',
|
|
135
|
+
prefixItems: [{ type: 'string' }, { type: 'integer' }],
|
|
136
|
+
},
|
|
137
|
+
makeCtx(),
|
|
138
|
+
);
|
|
139
|
+
expect(result).toEqual({
|
|
140
|
+
kind: 'tuple',
|
|
141
|
+
items: [
|
|
142
|
+
{ kind: 'scalar', name: 'string' },
|
|
143
|
+
{ kind: 'scalar', name: 'int' },
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('converts object with additionalProperties to record', () => {
|
|
149
|
+
const result = schemaToTypeNode(
|
|
150
|
+
{
|
|
151
|
+
type: 'object',
|
|
152
|
+
additionalProperties: { type: 'integer' },
|
|
153
|
+
},
|
|
154
|
+
makeCtx(),
|
|
155
|
+
);
|
|
156
|
+
expect(result).toEqual({
|
|
157
|
+
kind: 'record',
|
|
158
|
+
key: { kind: 'scalar', name: 'string' },
|
|
159
|
+
value: { kind: 'scalar', name: 'int' },
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('converts enum', () => {
|
|
164
|
+
const result = schemaToTypeNode({ enum: ['asc', 'desc'] }, makeCtx());
|
|
165
|
+
expect(result).toEqual({ kind: 'enum', values: ['asc', 'desc'] });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('converts const', () => {
|
|
169
|
+
const result = schemaToTypeNode({ const: 'hello' }, makeCtx());
|
|
170
|
+
expect(result).toEqual({ kind: 'literal', value: 'hello' });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('combiners', () => {
|
|
175
|
+
it('converts oneOf to union', () => {
|
|
176
|
+
const result = schemaToTypeNode(
|
|
177
|
+
{
|
|
178
|
+
oneOf: [{ type: 'string' }, { type: 'integer' }],
|
|
179
|
+
},
|
|
180
|
+
makeCtx(),
|
|
181
|
+
);
|
|
182
|
+
expect(result).toEqual({
|
|
183
|
+
kind: 'union',
|
|
184
|
+
members: [
|
|
185
|
+
{ kind: 'scalar', name: 'string' },
|
|
186
|
+
{ kind: 'scalar', name: 'int' },
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('converts anyOf to union', () => {
|
|
192
|
+
const result = schemaToTypeNode(
|
|
193
|
+
{
|
|
194
|
+
anyOf: [{ type: 'string' }, { type: 'number' }],
|
|
195
|
+
},
|
|
196
|
+
makeCtx(),
|
|
197
|
+
);
|
|
198
|
+
expect(result).toEqual({
|
|
199
|
+
kind: 'union',
|
|
200
|
+
members: [
|
|
201
|
+
{ kind: 'scalar', name: 'string' },
|
|
202
|
+
{ kind: 'scalar', name: 'number' },
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('converts allOf to intersection', () => {
|
|
208
|
+
const result = schemaToTypeNode(
|
|
209
|
+
{
|
|
210
|
+
allOf: [
|
|
211
|
+
{ type: 'object', properties: { a: { type: 'string' } } },
|
|
212
|
+
{ type: 'object', properties: { b: { type: 'integer' } } },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
makeCtx(),
|
|
216
|
+
);
|
|
217
|
+
expect(result.kind).toBe('intersection');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('references', () => {
|
|
222
|
+
it('converts $ref to model ref', () => {
|
|
223
|
+
const result = schemaToTypeNode({ $ref: '#/components/schemas/User' }, makeCtx());
|
|
224
|
+
expect(result).toEqual({ kind: 'ref', name: 'User' });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('wraps circular $ref in lazy', () => {
|
|
228
|
+
const ctx = makeCtx({ circularRefs: new Set(['TreeNode']) });
|
|
229
|
+
const result = schemaToTypeNode({ $ref: '#/components/schemas/TreeNode' }, ctx);
|
|
230
|
+
expect(result).toEqual({ kind: 'lazy', inner: { kind: 'ref', name: 'TreeNode' } });
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('nullable', () => {
|
|
235
|
+
it('converts type array with null to union', () => {
|
|
236
|
+
const result = schemaToTypeNode({ type: ['string', 'null'] } as NormalizedSchema, makeCtx());
|
|
237
|
+
expect(result).toEqual({
|
|
238
|
+
kind: 'union',
|
|
239
|
+
members: [
|
|
240
|
+
{ kind: 'scalar', name: 'string' },
|
|
241
|
+
{ kind: 'scalar', name: 'null' },
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('edge cases', () => {
|
|
248
|
+
it('handles schema with no type', () => {
|
|
249
|
+
const result = schemaToTypeNode({}, makeCtx());
|
|
250
|
+
expect(result).toEqual({ kind: 'scalar', name: 'unknown' });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('handles empty object type', () => {
|
|
254
|
+
const result = schemaToTypeNode({ type: 'object' }, makeCtx());
|
|
255
|
+
expect(result).toEqual({ kind: 'scalar', name: 'object' });
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('schemasToModels', () => {
|
|
261
|
+
it('converts object schemas to models with fields', () => {
|
|
262
|
+
const schemas = {
|
|
263
|
+
User: {
|
|
264
|
+
type: 'object',
|
|
265
|
+
properties: {
|
|
266
|
+
id: { type: 'string', format: 'uuid', readOnly: true },
|
|
267
|
+
name: { type: 'string', minLength: 1, maxLength: 100 },
|
|
268
|
+
email: { type: 'string', format: 'email' },
|
|
269
|
+
bio: { type: 'string', description: 'User bio' },
|
|
270
|
+
},
|
|
271
|
+
required: ['id', 'name', 'email'],
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
const ctx = makeCtx({ namedSchemas: schemas as Record<string, NormalizedSchema> });
|
|
275
|
+
const models = schemasToModels(schemas as Record<string, NormalizedSchema>, ctx);
|
|
276
|
+
|
|
277
|
+
expect(models.length).toBe(1);
|
|
278
|
+
const user = models[0]!;
|
|
279
|
+
expect(user.name).toBe('User');
|
|
280
|
+
expect(user.fields.length).toBe(4);
|
|
281
|
+
|
|
282
|
+
const idField = user.fields.find(f => f.name === 'id')!;
|
|
283
|
+
expect(idField.visibility).toBe('readonly');
|
|
284
|
+
expect(idField.optional).toBe(false);
|
|
285
|
+
|
|
286
|
+
const bioField = user.fields.find(f => f.name === 'bio')!;
|
|
287
|
+
expect(bioField.optional).toBe(true);
|
|
288
|
+
expect(bioField.description).toBe('User bio');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('converts type aliases', () => {
|
|
292
|
+
const schemas = {
|
|
293
|
+
UserId: { type: 'string', format: 'uuid' },
|
|
294
|
+
};
|
|
295
|
+
const ctx = makeCtx({ namedSchemas: schemas as Record<string, NormalizedSchema> });
|
|
296
|
+
const models = schemasToModels(schemas as Record<string, NormalizedSchema>, ctx);
|
|
297
|
+
|
|
298
|
+
expect(models.length).toBe(1);
|
|
299
|
+
expect(models[0]!.name).toBe('UserId');
|
|
300
|
+
expect(models[0]!.type).toEqual({ kind: 'scalar', name: 'uuid' });
|
|
301
|
+
expect(models[0]!.fields.length).toBe(0);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('converts allOf with $ref to inheritance', () => {
|
|
305
|
+
const schemas = {
|
|
306
|
+
User: {
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: { id: { type: 'string' } },
|
|
309
|
+
required: ['id'],
|
|
310
|
+
},
|
|
311
|
+
Admin: {
|
|
312
|
+
allOf: [{ $ref: '#/components/schemas/User' }, { type: 'object', properties: { role: { type: 'string' } } }],
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
const ctx = makeCtx({ namedSchemas: schemas as Record<string, NormalizedSchema> });
|
|
316
|
+
const models = schemasToModels(schemas as Record<string, NormalizedSchema>, ctx);
|
|
317
|
+
|
|
318
|
+
const admin = models.find(m => m.name === 'Admin')!;
|
|
319
|
+
expect(admin.bases).toEqual(['User']);
|
|
320
|
+
expect(admin.fields.length).toBe(1);
|
|
321
|
+
expect(admin.fields[0]!.name).toBe('role');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('sanitizeName', () => {
|
|
326
|
+
it('passes through valid names', () => {
|
|
327
|
+
expect(sanitizeName('User', new WarningCollector())).toBe('User');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('sanitizes names with dots', () => {
|
|
331
|
+
expect(sanitizeName('user.response', new WarningCollector())).toBe('UserResponse');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('sanitizes names with hyphens', () => {
|
|
335
|
+
expect(sanitizeName('my-model', new WarningCollector())).toBe('MyModel');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('sanitizes names with spaces', () => {
|
|
339
|
+
expect(sanitizeName('some model', new WarningCollector())).toBe('SomeModel');
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { splitByTag, mergeIntoSingle } from '../src/tag-splitter.js';
|
|
3
|
+
import { model, field, scalarType, refType, arrayType, opRoute, opOperation, opResponse } from './helpers.js';
|
|
4
|
+
import type { OpRouteNode } from '@contractkit/core';
|
|
5
|
+
|
|
6
|
+
describe('splitByTag', () => {
|
|
7
|
+
it('splits routes into separate files by tag', () => {
|
|
8
|
+
const userModel = model('User', [field('id', scalarType('uuid'))]);
|
|
9
|
+
const postModel = model('Post', [field('id', scalarType('uuid')), field('author', refType('User'))]);
|
|
10
|
+
|
|
11
|
+
const userRoute = opRoute('/users', [opOperation('get', { responses: [opResponse(200, arrayType(refType('User')), 'application/json')] })]);
|
|
12
|
+
const postRoute = opRoute('/posts', [opOperation('get', { responses: [opResponse(200, arrayType(refType('Post')), 'application/json')] })]);
|
|
13
|
+
|
|
14
|
+
const routeTags = new Map<OpRouteNode, string>([
|
|
15
|
+
[userRoute, 'users'],
|
|
16
|
+
[postRoute, 'posts'],
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const result = splitByTag([userModel, postModel], [userRoute, postRoute], routeTags);
|
|
20
|
+
|
|
21
|
+
expect(result.has('users.ck')).toBe(true);
|
|
22
|
+
expect(result.has('posts.ck')).toBe(true);
|
|
23
|
+
|
|
24
|
+
const usersFile = result.get('users.ck')!;
|
|
25
|
+
expect(usersFile.routes).toContain(userRoute);
|
|
26
|
+
expect(usersFile.models.some(m => m.name === 'User')).toBe(true);
|
|
27
|
+
|
|
28
|
+
const postsFile = result.get('posts.ck')!;
|
|
29
|
+
expect(postsFile.routes).toContain(postRoute);
|
|
30
|
+
expect(postsFile.models.some(m => m.name === 'Post')).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('puts models referenced by multiple tags in shared.ck', () => {
|
|
34
|
+
const sharedModel = model('Pagination', [field('page', scalarType('int'))]);
|
|
35
|
+
|
|
36
|
+
const route1 = opRoute('/users', [opOperation('get', { query: 'Pagination', responses: [] })]);
|
|
37
|
+
const route2 = opRoute('/posts', [opOperation('get', { query: 'Pagination', responses: [] })]);
|
|
38
|
+
|
|
39
|
+
const routeTags = new Map<OpRouteNode, string>([
|
|
40
|
+
[route1, 'users'],
|
|
41
|
+
[route2, 'posts'],
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const result = splitByTag([sharedModel], [route1, route2], routeTags);
|
|
45
|
+
expect(result.has('shared.ck')).toBe(true);
|
|
46
|
+
expect(result.get('shared.ck')!.models.some(m => m.name === 'Pagination')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('puts orphan models in shared.ck', () => {
|
|
50
|
+
const orphan = model('Config', [field('key', scalarType('string'))]);
|
|
51
|
+
const result = splitByTag([orphan], [], new Map());
|
|
52
|
+
expect(result.has('shared.ck')).toBe(true);
|
|
53
|
+
expect(result.get('shared.ck')!.models.some(m => m.name === 'Config')).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('assigns untagged routes to default.ck', () => {
|
|
57
|
+
const route = opRoute('/health', [opOperation('get', { responses: [opResponse(200)] })]);
|
|
58
|
+
const routeTags = new Map<OpRouteNode, string>([[route, 'default']]);
|
|
59
|
+
|
|
60
|
+
const result = splitByTag([], [route], routeTags);
|
|
61
|
+
expect(result.has('default.ck')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('mergeIntoSingle', () => {
|
|
66
|
+
it('merges all models and routes into one CkRootNode', () => {
|
|
67
|
+
const models = [model('User', [field('id', scalarType('uuid'))])];
|
|
68
|
+
const routes = [opRoute('/users', [opOperation('get', { responses: [] })])];
|
|
69
|
+
|
|
70
|
+
const result = mergeIntoSingle(models, routes);
|
|
71
|
+
expect(result.models).toBe(models);
|
|
72
|
+
expect(result.routes).toBe(routes);
|
|
73
|
+
expect(result.file).toBe('api.ck');
|
|
74
|
+
});
|
|
75
|
+
});
|