@contractkit/prettier-plugin 0.8.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 +35 -0
- package/.turbo/turbo-build.log +15 -0
- package/.turbo/turbo-format.log +29 -0
- package/.turbo/turbo-test$colon$ci.log +105 -0
- package/.turbo/turbo-test.log +14 -0
- package/CHANGELOG.md +111 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +305 -0
- package/coverage/coverage-final.json +6 -0
- package/coverage/favicon.png +0 -0
- package/coverage/indent.ts.html +88 -0
- package/coverage/index.html +176 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/print-ck.ts.html +370 -0
- package/coverage/print-contract.ts.html +292 -0
- package/coverage/print-operation.ts.html +811 -0
- package/coverage/print-type.ts.html +511 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/__tests__/print-ck.test.d.ts +2 -0
- package/dist/__tests__/print-ck.test.d.ts.map +1 -0
- package/dist/__tests__/print-op.test.d.ts +2 -0
- package/dist/__tests__/print-op.test.d.ts.map +1 -0
- package/dist/__tests__/print-op.test.js +359 -0
- package/dist/__tests__/print-op.test.js.map +1 -0
- package/dist/indent.d.ts +2 -0
- package/dist/indent.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +520 -0
- package/dist/index.js.map +1 -0
- package/dist/print-ck.d.ts +4 -0
- package/dist/print-ck.d.ts.map +1 -0
- package/dist/print-contract.d.ts +3 -0
- package/dist/print-contract.d.ts.map +1 -0
- package/dist/print-dto.d.ts +3 -0
- package/dist/print-dto.d.ts.map +1 -0
- package/dist/print-dto.js +100 -0
- package/dist/print-dto.js.map +1 -0
- package/dist/print-op.d.ts +24 -0
- package/dist/print-op.d.ts.map +1 -0
- package/dist/print-op.js +224 -0
- package/dist/print-op.js.map +1 -0
- package/dist/print-operation.d.ts +24 -0
- package/dist/print-operation.d.ts.map +1 -0
- package/dist/print-type.d.ts +21 -0
- package/dist/print-type.d.ts.map +1 -0
- package/dist/print-type.js +110 -0
- package/dist/print-type.js.map +1 -0
- package/eslint.config.js +6 -0
- package/package.json +47 -0
- package/src/indent.ts +1 -0
- package/src/index.ts +46 -0
- package/src/print-ck.ts +95 -0
- package/src/print-contract.ts +69 -0
- package/src/print-operation.ts +242 -0
- package/src/print-type.ts +142 -0
- package/tests/print-ck.test.ts +606 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { printCk } from '../src/print-ck.js';
|
|
3
|
+
import { parseCk, DiagnosticCollector } from '@contractkit/core';
|
|
4
|
+
import type { OpRouteNode, OpOperationNode, CkRootNode, SecurityFields, ParamSource, ContractTypeNode, OpParamNode } from '@contractkit/core';
|
|
5
|
+
|
|
6
|
+
// ─── Minimal AST builders ────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function makeLoc(line = 1) {
|
|
9
|
+
return { file: 'test.ck', line };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
function normalizeParamSource(value: any): ParamSource {
|
|
14
|
+
if (!value) return value;
|
|
15
|
+
if (typeof value === 'string') return { kind: 'ref', name: value };
|
|
16
|
+
if (Array.isArray(value)) return { kind: 'params', nodes: value as OpParamNode[] };
|
|
17
|
+
if (value.kind === 'params' || value.kind === 'ref' || value.kind === 'type') return value as ParamSource;
|
|
18
|
+
return { kind: 'type', node: value as ContractTypeNode };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeOp(method: OpOperationNode['method'], overrides?: Partial<OpOperationNode> & { query?: unknown; headers?: unknown }): OpOperationNode {
|
|
22
|
+
const normalized = { ...overrides } as Partial<OpOperationNode>;
|
|
23
|
+
if (overrides?.query !== undefined) normalized.query = normalizeParamSource(overrides.query);
|
|
24
|
+
if (overrides?.headers !== undefined) normalized.headers = normalizeParamSource(overrides.headers);
|
|
25
|
+
return { method, responses: [], loc: makeLoc(), ...normalized };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeRoute(path: string, operations: OpOperationNode[], overrides?: Partial<OpRouteNode> & { params?: unknown }): OpRouteNode {
|
|
29
|
+
const normalized = { ...overrides } as Partial<OpRouteNode>;
|
|
30
|
+
if (overrides?.params !== undefined) normalized.params = normalizeParamSource(overrides.params);
|
|
31
|
+
return { path, operations, loc: makeLoc(), ...normalized };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeRoot(routes: OpRouteNode[]): CkRootNode {
|
|
35
|
+
return { kind: 'ckRoot', meta: {}, services: {}, models: [], routes, file: 'test.ck' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Route modifier printing ─────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
describe('printCk — route modifiers', () => {
|
|
41
|
+
it('prints route with no modifiers', () => {
|
|
42
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('get')])]);
|
|
43
|
+
expect(printCk(ast)).toContain('operation /users: {');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('prints route with internal modifier', () => {
|
|
47
|
+
const ast = makeRoot([makeRoute('/admin/users', [makeOp('get')], { modifiers: ['internal'] })]);
|
|
48
|
+
expect(printCk(ast)).toContain('operation(internal) /admin/users: {');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('prints route with deprecated modifier', () => {
|
|
52
|
+
const ast = makeRoot([makeRoute('/v1/users', [makeOp('get')], { modifiers: ['deprecated'] })]);
|
|
53
|
+
expect(printCk(ast)).toContain('operation(deprecated) /v1/users: {');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('treats empty modifiers array same as no modifier', () => {
|
|
57
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('get')], { modifiers: [] })]);
|
|
58
|
+
expect(printCk(ast)).toContain('operation /users: {');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('preserves description alongside modifier', () => {
|
|
62
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('get')], { modifiers: ['deprecated'], description: 'Old user list' })]);
|
|
63
|
+
expect(printCk(ast)).toContain('operation(deprecated) /users: { # Old user list');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ─── Operation modifier printing ─────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
describe('printCk — operation modifiers', () => {
|
|
70
|
+
it('prints operation with no modifiers', () => {
|
|
71
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('get')])]);
|
|
72
|
+
expect(printCk(ast)).toContain(' get: {');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('prints operation with internal modifier', () => {
|
|
76
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('get', { modifiers: ['internal'] })])]);
|
|
77
|
+
expect(printCk(ast)).toContain(' get(internal): {');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('prints operation with deprecated modifier', () => {
|
|
81
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('get', { modifiers: ['deprecated'] })])]);
|
|
82
|
+
expect(printCk(ast)).toContain(' get(deprecated): {');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('treats empty operation modifiers array as no modifiers', () => {
|
|
86
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('delete', { modifiers: [] })])]);
|
|
87
|
+
expect(printCk(ast)).toContain(' delete: {');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('prints multiple operations with mixed modifiers', () => {
|
|
91
|
+
const ast = makeRoot([
|
|
92
|
+
makeRoute('/users', [makeOp('get', { modifiers: ['deprecated'] }), makeOp('post', { modifiers: ['internal'] }), makeOp('delete')]),
|
|
93
|
+
]);
|
|
94
|
+
const output = printCk(ast);
|
|
95
|
+
expect(output).toContain(' get(deprecated): {');
|
|
96
|
+
expect(output).toContain(' post(internal): {');
|
|
97
|
+
expect(output).toContain(' delete: {');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('preserves description alongside modifier on operation', () => {
|
|
101
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('get', { modifiers: ['internal'], description: 'Admin only' })])]);
|
|
102
|
+
expect(printCk(ast)).toContain(' get(internal): { # Admin only');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ─── Route + operation modifier combinations ──────────────────────────────────
|
|
107
|
+
|
|
108
|
+
describe('printCk — route and operation modifiers combined', () => {
|
|
109
|
+
it('prints both route and operation modifiers independently', () => {
|
|
110
|
+
const ast = makeRoot([
|
|
111
|
+
makeRoute('/admin/users', [makeOp('get', { modifiers: ['deprecated'] }), makeOp('post', { modifiers: ['internal'] })], {
|
|
112
|
+
modifiers: ['internal'],
|
|
113
|
+
}),
|
|
114
|
+
]);
|
|
115
|
+
const output = printCk(ast);
|
|
116
|
+
expect(output).toContain('operation(internal) /admin/users: {');
|
|
117
|
+
expect(output).toContain(' get(deprecated): {');
|
|
118
|
+
expect(output).toContain(' post(internal): {');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('modifier placement: operation(modifier) before path, verb(modifier) before colon', () => {
|
|
122
|
+
const ast = makeRoot([makeRoute('/admin', [makeOp('get', { modifiers: ['deprecated'] })], { modifiers: ['internal'] })]);
|
|
123
|
+
const output = printCk(ast);
|
|
124
|
+
expect(output).toMatch(/^operation\(internal\) \/admin: \{/m);
|
|
125
|
+
expect(output).toMatch(/^\s+get\(deprecated\): \{/m);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ─── Security printing ────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function makeSecFields(fields: Partial<Pick<SecurityFields, 'roles'>>): SecurityFields {
|
|
132
|
+
return { ...fields, loc: makeLoc() };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
describe('printCk — security', () => {
|
|
136
|
+
it('prints operation-level security: none', () => {
|
|
137
|
+
const ast = makeRoot([makeRoute('/health', [makeOp('get', { security: 'none' })])]);
|
|
138
|
+
expect(printCk(ast)).toContain(' security: none');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('prints security block with roles only', () => {
|
|
142
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('get', { security: makeSecFields({ roles: ['admin'] }) })])]);
|
|
143
|
+
const out = printCk(ast);
|
|
144
|
+
expect(out).toContain(' security: {');
|
|
145
|
+
expect(out).toContain(' roles: admin');
|
|
146
|
+
expect(out).toContain(' }');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('prints security block with multiple roles space-separated', () => {
|
|
150
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('get', { security: makeSecFields({ roles: ['admin', 'moderator'] }) })])]);
|
|
151
|
+
expect(printCk(ast)).toContain(' roles: admin moderator');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('prints operation-level signature as its own keyword', () => {
|
|
155
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('post', { signature: 'hmac-sha256' })])]);
|
|
156
|
+
const out = printCk(ast);
|
|
157
|
+
expect(out).toContain(' signature: "hmac-sha256"');
|
|
158
|
+
expect(out).not.toContain('security: {');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('prints unquoted identifier signature without quotes', () => {
|
|
162
|
+
const ast = makeRoot([makeRoute('/webhooks', [makeOp('post', { signature: 'MODERN_TREASURY_WEBHOOK' })])]);
|
|
163
|
+
expect(printCk(ast)).toContain(' signature: MODERN_TREASURY_WEBHOOK');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('prints route-level security with shallower indentation', () => {
|
|
167
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('get')], { security: makeSecFields({ roles: ['admin'] }) })]);
|
|
168
|
+
const out = printCk(ast);
|
|
169
|
+
expect(out).toContain(' security: {');
|
|
170
|
+
expect(out).toContain(' roles: admin');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('prints route-level security: none', () => {
|
|
174
|
+
const ast = makeRoot([makeRoute('/public', [makeOp('get')], { security: 'none' })]);
|
|
175
|
+
expect(printCk(ast)).toContain(' security: none');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('emits no security line when security is undefined', () => {
|
|
179
|
+
const ast = makeRoot([makeRoute('/users', [makeOp('get')])]);
|
|
180
|
+
expect(printCk(ast)).not.toContain('security');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ─── Query / headers with descriptions ───────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
describe('printCk — query and headers descriptions', () => {
|
|
187
|
+
it('emits inline comment on query params that have descriptions', () => {
|
|
188
|
+
const loc = makeLoc();
|
|
189
|
+
const ast = makeRoot([
|
|
190
|
+
makeRoute('/users', [
|
|
191
|
+
makeOp('get', {
|
|
192
|
+
query: [
|
|
193
|
+
{ name: 'page', optional: false, nullable: false, type: { kind: 'scalar', name: 'int' }, description: 'Page number', loc },
|
|
194
|
+
{ name: 'limit', optional: false, nullable: false, type: { kind: 'scalar', name: 'int' }, loc },
|
|
195
|
+
],
|
|
196
|
+
}),
|
|
197
|
+
]),
|
|
198
|
+
]);
|
|
199
|
+
const out = printCk(ast);
|
|
200
|
+
expect(out).toContain(' page: int # Page number');
|
|
201
|
+
expect(out).toContain(' limit: int');
|
|
202
|
+
expect(out).not.toMatch(/limit: int #/);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('emits inline comment on headers params that have descriptions', () => {
|
|
206
|
+
const loc = makeLoc();
|
|
207
|
+
const ast = makeRoot([
|
|
208
|
+
makeRoute('/users', [
|
|
209
|
+
makeOp('get', {
|
|
210
|
+
headers: [
|
|
211
|
+
{
|
|
212
|
+
name: 'X-Request-Id',
|
|
213
|
+
optional: false,
|
|
214
|
+
nullable: false,
|
|
215
|
+
type: { kind: 'scalar', name: 'uuid' },
|
|
216
|
+
description: 'Idempotency key',
|
|
217
|
+
loc,
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
}),
|
|
221
|
+
]),
|
|
222
|
+
]);
|
|
223
|
+
expect(printCk(ast)).toContain(' X-Request-Id: uuid # Idempotency key');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ─── Query / headers — optional, nullable, defaults ──────────────────────────
|
|
228
|
+
|
|
229
|
+
describe('printCk — query and headers optional, nullable, default', () => {
|
|
230
|
+
const loc = makeLoc();
|
|
231
|
+
|
|
232
|
+
it('emits ? for optional query params', () => {
|
|
233
|
+
const ast = makeRoot([
|
|
234
|
+
makeRoute('/search', [
|
|
235
|
+
makeOp('get', {
|
|
236
|
+
query: [
|
|
237
|
+
{ name: 'q', optional: true, nullable: false, type: { kind: 'scalar', name: 'string' }, loc },
|
|
238
|
+
{ name: 'page', optional: false, nullable: false, type: { kind: 'scalar', name: 'int' }, loc },
|
|
239
|
+
],
|
|
240
|
+
}),
|
|
241
|
+
]),
|
|
242
|
+
]);
|
|
243
|
+
const out = printCk(ast);
|
|
244
|
+
expect(out).toContain(' q?: string');
|
|
245
|
+
expect(out).toContain(' page: int');
|
|
246
|
+
expect(out).not.toContain('page?:');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('emits | null for nullable query params', () => {
|
|
250
|
+
const ast = makeRoot([
|
|
251
|
+
makeRoute('/items', [
|
|
252
|
+
makeOp('get', {
|
|
253
|
+
query: [{ name: 'filter', optional: false, nullable: true, type: { kind: 'scalar', name: 'string' }, loc }],
|
|
254
|
+
}),
|
|
255
|
+
]),
|
|
256
|
+
]);
|
|
257
|
+
expect(printCk(ast)).toContain(' filter: string | null');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('emits = value for numeric defaults', () => {
|
|
261
|
+
const ast = makeRoot([
|
|
262
|
+
makeRoute('/items', [
|
|
263
|
+
makeOp('get', {
|
|
264
|
+
query: [{ name: 'limit', optional: false, nullable: false, type: { kind: 'scalar', name: 'int' }, default: 20, loc }],
|
|
265
|
+
}),
|
|
266
|
+
]),
|
|
267
|
+
]);
|
|
268
|
+
expect(printCk(ast)).toContain(' limit: int = 20');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('emits = value for boolean defaults', () => {
|
|
272
|
+
const ast = makeRoot([
|
|
273
|
+
makeRoute('/items', [
|
|
274
|
+
makeOp('get', {
|
|
275
|
+
query: [{ name: 'active', optional: false, nullable: false, type: { kind: 'scalar', name: 'boolean' }, default: true, loc }],
|
|
276
|
+
}),
|
|
277
|
+
]),
|
|
278
|
+
]);
|
|
279
|
+
expect(printCk(ast)).toContain(' active: boolean = true');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('emits = "value" for string defaults that need quoting', () => {
|
|
283
|
+
const ast = makeRoot([
|
|
284
|
+
makeRoute('/items', [
|
|
285
|
+
makeOp('get', {
|
|
286
|
+
query: [
|
|
287
|
+
{ name: 'label', optional: false, nullable: false, type: { kind: 'scalar', name: 'string' }, default: 'hello world', loc },
|
|
288
|
+
],
|
|
289
|
+
}),
|
|
290
|
+
]),
|
|
291
|
+
]);
|
|
292
|
+
expect(printCk(ast)).toContain(' label: string = "hello world"');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('combines optional, default, and description on the same param', () => {
|
|
296
|
+
const ast = makeRoot([
|
|
297
|
+
makeRoute('/items', [
|
|
298
|
+
makeOp('get', {
|
|
299
|
+
query: [
|
|
300
|
+
{
|
|
301
|
+
name: 'page',
|
|
302
|
+
optional: true,
|
|
303
|
+
nullable: false,
|
|
304
|
+
type: { kind: 'scalar', name: 'int' },
|
|
305
|
+
default: 1,
|
|
306
|
+
description: 'Page number',
|
|
307
|
+
loc,
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
}),
|
|
311
|
+
]),
|
|
312
|
+
]);
|
|
313
|
+
expect(printCk(ast)).toContain(' page?: int = 1 # Page number');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('applies the same rules to headers params', () => {
|
|
317
|
+
const ast = makeRoot([
|
|
318
|
+
makeRoute('/items', [
|
|
319
|
+
makeOp('get', {
|
|
320
|
+
headers: [{ name: 'X-Version', optional: true, nullable: false, type: { kind: 'scalar', name: 'string' }, default: 'v1', loc }],
|
|
321
|
+
}),
|
|
322
|
+
]),
|
|
323
|
+
]);
|
|
324
|
+
expect(printCk(ast)).toContain(' X-Version?: string = v1');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ─── Request blocks ──────────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
describe('printCk — request blocks', () => {
|
|
331
|
+
it('prints a single content-type request', () => {
|
|
332
|
+
const ast = makeRoot([
|
|
333
|
+
makeRoute('/users', [
|
|
334
|
+
makeOp('post', {
|
|
335
|
+
request: { bodies: [{ contentType: 'application/json', bodyType: { kind: 'ref', name: 'CreateUser' } }] },
|
|
336
|
+
}),
|
|
337
|
+
]),
|
|
338
|
+
]);
|
|
339
|
+
const out = printCk(ast);
|
|
340
|
+
expect(out).toContain(' request: {');
|
|
341
|
+
expect(out).toContain(' application/json: CreateUser');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('round-trips vendor JSON mime types like application/vnd.api+json', () => {
|
|
345
|
+
const ast = makeRoot([
|
|
346
|
+
makeRoute('/users', [
|
|
347
|
+
makeOp('post', {
|
|
348
|
+
request: { bodies: [{ contentType: 'application/vnd.api+json', bodyType: { kind: 'ref', name: 'CreateUser' } }] },
|
|
349
|
+
responses: [{ statusCode: 201, contentType: 'application/vnd.api+json', bodyType: { kind: 'ref', name: 'User' } }],
|
|
350
|
+
}),
|
|
351
|
+
]),
|
|
352
|
+
]);
|
|
353
|
+
const out = printCk(ast);
|
|
354
|
+
expect(out).toContain(' application/vnd.api+json: CreateUser');
|
|
355
|
+
expect(out).toContain(' application/vnd.api+json: User');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('prints multiple content-types preserving source order', () => {
|
|
359
|
+
const ast = makeRoot([
|
|
360
|
+
makeRoute('/auth/token', [
|
|
361
|
+
makeOp('post', {
|
|
362
|
+
request: {
|
|
363
|
+
bodies: [
|
|
364
|
+
{ contentType: 'application/json', bodyType: { kind: 'ref', name: 'AuthRequest' } },
|
|
365
|
+
{ contentType: 'application/x-www-form-urlencoded', bodyType: { kind: 'ref', name: 'AuthRequest' } },
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
}),
|
|
369
|
+
]),
|
|
370
|
+
]);
|
|
371
|
+
const out = printCk(ast);
|
|
372
|
+
expect(out).toContain(' application/json: AuthRequest');
|
|
373
|
+
expect(out).toContain(' application/x-www-form-urlencoded: AuthRequest');
|
|
374
|
+
// Order check
|
|
375
|
+
const i1 = out.indexOf('application/json:');
|
|
376
|
+
const i2 = out.indexOf('application/x-www-form-urlencoded:');
|
|
377
|
+
expect(i1).toBeLessThan(i2);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('printCk — response headers', () => {
|
|
382
|
+
it('prints response headers alongside body', () => {
|
|
383
|
+
const ast = makeRoot([
|
|
384
|
+
makeRoute('/transfers/{id}', [
|
|
385
|
+
makeOp('get', {
|
|
386
|
+
responses: [
|
|
387
|
+
{
|
|
388
|
+
statusCode: 200,
|
|
389
|
+
contentType: 'application/json',
|
|
390
|
+
bodyType: { kind: 'ref', name: 'Transfer' },
|
|
391
|
+
headers: [
|
|
392
|
+
{ name: 'preference-applied', optional: true, type: { kind: 'scalar', name: 'string' } },
|
|
393
|
+
{ name: 'etag', optional: false, type: { kind: 'scalar', name: 'string' }, description: 'cache validator' },
|
|
394
|
+
],
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
}),
|
|
398
|
+
]),
|
|
399
|
+
]);
|
|
400
|
+
const out = printCk(ast);
|
|
401
|
+
expect(out).toContain(' 200: {');
|
|
402
|
+
expect(out).toContain(' application/json: Transfer');
|
|
403
|
+
expect(out).toContain(' headers: {');
|
|
404
|
+
expect(out).toContain(' preference-applied?: string');
|
|
405
|
+
expect(out).toContain(' etag: string # cache validator');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('prints response headers without a body', () => {
|
|
409
|
+
const ast = makeRoot([
|
|
410
|
+
makeRoute('/resources/{id}', [
|
|
411
|
+
makeOp('delete', {
|
|
412
|
+
responses: [
|
|
413
|
+
{
|
|
414
|
+
statusCode: 204,
|
|
415
|
+
headers: [{ name: 'x-deleted-at', optional: false, type: { kind: 'scalar', name: 'string' } }],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
}),
|
|
419
|
+
]),
|
|
420
|
+
]);
|
|
421
|
+
const out = printCk(ast);
|
|
422
|
+
expect(out).toContain(' 204: {');
|
|
423
|
+
expect(out).toContain(' headers: {');
|
|
424
|
+
expect(out).toContain(' x-deleted-at: string');
|
|
425
|
+
expect(out).not.toContain('application/json');
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('printCk — options-level header globals (round-trip)', () => {
|
|
430
|
+
function roundTrip(source: string): string {
|
|
431
|
+
const diag = new DiagnosticCollector();
|
|
432
|
+
const ast = parseCk(source, 'test.ck', diag);
|
|
433
|
+
expect(diag.hasErrors()).toBe(false);
|
|
434
|
+
return printCk(ast);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
it('round-trips options.request.headers', () => {
|
|
438
|
+
const source = `\
|
|
439
|
+
options {
|
|
440
|
+
request: {
|
|
441
|
+
headers: {
|
|
442
|
+
x-request-id: uuid
|
|
443
|
+
authorization?: string
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
operation /widgets: {
|
|
449
|
+
get: {
|
|
450
|
+
response: {
|
|
451
|
+
200: {
|
|
452
|
+
application/json: Widget
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
`;
|
|
458
|
+
const out = roundTrip(source);
|
|
459
|
+
expect(out).toBe(source);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('round-trips options.response.headers', () => {
|
|
463
|
+
const source = `\
|
|
464
|
+
options {
|
|
465
|
+
response: {
|
|
466
|
+
headers: {
|
|
467
|
+
x-request-id: uuid
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
operation /widgets: {
|
|
473
|
+
get: {
|
|
474
|
+
response: {
|
|
475
|
+
200: {
|
|
476
|
+
application/json: Widget
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
`;
|
|
482
|
+
expect(roundTrip(source)).toBe(source);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('round-trips operation-level headers: none', () => {
|
|
486
|
+
const source = `\
|
|
487
|
+
operation /widgets: {
|
|
488
|
+
get: {
|
|
489
|
+
headers: none
|
|
490
|
+
response: {
|
|
491
|
+
200: {
|
|
492
|
+
application/json: Widget
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
`;
|
|
498
|
+
expect(roundTrip(source)).toBe(source);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('round-trips per-status headers: none', () => {
|
|
502
|
+
const source = `\
|
|
503
|
+
operation /widgets: {
|
|
504
|
+
get: {
|
|
505
|
+
response: {
|
|
506
|
+
200: {
|
|
507
|
+
application/json: Widget
|
|
508
|
+
}
|
|
509
|
+
404: {
|
|
510
|
+
headers: none
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
`;
|
|
516
|
+
expect(roundTrip(source)).toBe(source);
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe('printCk — multi-base inheritance and override (round-trip)', () => {
|
|
521
|
+
function roundTrip(source: string): string {
|
|
522
|
+
const diag = new DiagnosticCollector();
|
|
523
|
+
const ast = parseCk(source, 'test.ck', diag);
|
|
524
|
+
expect(diag.hasErrors()).toBe(false);
|
|
525
|
+
return printCk(ast);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
it('round-trips multi-base inheritance', () => {
|
|
529
|
+
const source = `\
|
|
530
|
+
contract Test5: A & B & C & D & {
|
|
531
|
+
e: string
|
|
532
|
+
}
|
|
533
|
+
`;
|
|
534
|
+
expect(roundTrip(source)).toBe(source);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('round-trips override modifier on a field', () => {
|
|
538
|
+
const source = `\
|
|
539
|
+
contract Test5: A & {
|
|
540
|
+
a: override int
|
|
541
|
+
e: string
|
|
542
|
+
}
|
|
543
|
+
`;
|
|
544
|
+
expect(roundTrip(source)).toBe(source);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('emits canonical modifier order: override → deprecated → readonly', () => {
|
|
548
|
+
const source = `\
|
|
549
|
+
contract Test: A & {
|
|
550
|
+
a: deprecated override readonly string
|
|
551
|
+
}
|
|
552
|
+
`;
|
|
553
|
+
// Canonical order is override → deprecated → readonly|writeonly.
|
|
554
|
+
// Re-printing reorders the modifiers.
|
|
555
|
+
const printed = roundTrip(source);
|
|
556
|
+
expect(printed).toContain('a: override deprecated readonly string');
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
describe('printCk — typed path params (round-trip)', () => {
|
|
561
|
+
function roundTrip(source: string): string {
|
|
562
|
+
const diag = new DiagnosticCollector();
|
|
563
|
+
const ast = parseCk(source, 'test.ck', diag);
|
|
564
|
+
expect(diag.hasErrors()).toBe(false);
|
|
565
|
+
return printCk(ast);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
it('round-trips path params with constraint arguments', () => {
|
|
569
|
+
const source = `\
|
|
570
|
+
operation /orders/{orderId}: {
|
|
571
|
+
params: {
|
|
572
|
+
orderId: int(min=1, max=5)
|
|
573
|
+
}
|
|
574
|
+
get: {
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
`;
|
|
578
|
+
expect(roundTrip(source)).toBe(source);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('round-trips path params with enum types', () => {
|
|
582
|
+
const source = `\
|
|
583
|
+
operation /pets/{status}: {
|
|
584
|
+
params: {
|
|
585
|
+
status: enum(available, pending, sold)
|
|
586
|
+
}
|
|
587
|
+
get: {
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
`;
|
|
591
|
+
expect(roundTrip(source)).toBe(source);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('round-trips path params with regex constraints and a comment', () => {
|
|
595
|
+
const source = `\
|
|
596
|
+
operation /users/{slug}: {
|
|
597
|
+
params: {
|
|
598
|
+
slug: string(regex=/^[a-z0-9-]+$/) # url slug
|
|
599
|
+
}
|
|
600
|
+
get: {
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
`;
|
|
604
|
+
expect(roundTrip(source)).toBe(source);
|
|
605
|
+
});
|
|
606
|
+
});
|
package/tsconfig.json
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineProject } from 'vitest/config';
|
|
2
|
+
import swc from 'unplugin-swc';
|
|
3
|
+
|
|
4
|
+
export default defineProject({
|
|
5
|
+
test: {
|
|
6
|
+
globals: true,
|
|
7
|
+
include: ['./tests/**/*.test.ts'],
|
|
8
|
+
environment: 'node',
|
|
9
|
+
},
|
|
10
|
+
plugins: [swc.vite({ tsconfigFile: false })],
|
|
11
|
+
});
|