@contractkit/prettier-plugin 0.14.2 → 0.14.4
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 +5 -5
- package/.turbo/turbo-test$colon$ci.log +11 -20
- package/CHANGELOG.md +40 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -713
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +4 -5
- package/dist/indent.d.ts +0 -2
- package/dist/indent.d.ts.map +0 -1
- package/dist/print-ck.d.ts +0 -20
- package/dist/print-ck.d.ts.map +0 -1
- package/dist/print-contract.d.ts +0 -13
- package/dist/print-contract.d.ts.map +0 -1
- package/dist/print-operation.d.ts +0 -48
- package/dist/print-operation.d.ts.map +0 -1
- package/dist/print-type.d.ts +0 -26
- package/dist/print-type.d.ts.map +0 -1
- package/src/indent.ts +0 -1
- package/src/print-ck.ts +0 -179
- package/src/print-contract.ts +0 -86
- package/src/print-operation.ts +0 -454
- package/src/print-type.ts +0 -155
- package/tests/print-ck.test.ts +0 -1121
- package/tests/round-trip.test.ts +0 -694
package/tests/print-ck.test.ts
DELETED
|
@@ -1,1121 +0,0 @@
|
|
|
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
|
-
// A route description is emitted above the declaration, not inline: a `#` after the `{`
|
|
64
|
-
// would re-parse as the first operation's description instead.
|
|
65
|
-
expect(printCk(ast)).toContain('# Old user list\noperation(deprecated) /users: {');
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// ─── Operation modifier printing ─────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
describe('printCk — operation modifiers', () => {
|
|
72
|
-
it('prints operation with no modifiers', () => {
|
|
73
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get')])]);
|
|
74
|
-
expect(printCk(ast)).toContain(' get: {');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('prints operation with internal modifier', () => {
|
|
78
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get', { modifiers: ['internal'] })])]);
|
|
79
|
-
expect(printCk(ast)).toContain(' get(internal): {');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('prints operation with deprecated modifier', () => {
|
|
83
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get', { modifiers: ['deprecated'] })])]);
|
|
84
|
-
expect(printCk(ast)).toContain(' get(deprecated): {');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('treats empty operation modifiers array as no modifiers', () => {
|
|
88
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('delete', { modifiers: [] })])]);
|
|
89
|
-
expect(printCk(ast)).toContain(' delete: {');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('prints multiple operations with mixed modifiers', () => {
|
|
93
|
-
const ast = makeRoot([
|
|
94
|
-
makeRoute('/users', [makeOp('get', { modifiers: ['deprecated'] }), makeOp('post', { modifiers: ['internal'] }), makeOp('delete')]),
|
|
95
|
-
]);
|
|
96
|
-
const output = printCk(ast);
|
|
97
|
-
expect(output).toContain(' get(deprecated): {');
|
|
98
|
-
expect(output).toContain(' post(internal): {');
|
|
99
|
-
expect(output).toContain(' delete: {');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('preserves description alongside modifier on operation', () => {
|
|
103
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get', { modifiers: ['internal'], description: 'Admin only' })])]);
|
|
104
|
-
expect(printCk(ast)).toContain(' get(internal): { # Admin only');
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// ─── Route + operation modifier combinations ──────────────────────────────────
|
|
109
|
-
|
|
110
|
-
describe('printCk — route and operation modifiers combined', () => {
|
|
111
|
-
it('prints both route and operation modifiers independently', () => {
|
|
112
|
-
const ast = makeRoot([
|
|
113
|
-
makeRoute('/admin/users', [makeOp('get', { modifiers: ['deprecated'] }), makeOp('post', { modifiers: ['internal'] })], {
|
|
114
|
-
modifiers: ['internal'],
|
|
115
|
-
}),
|
|
116
|
-
]);
|
|
117
|
-
const output = printCk(ast);
|
|
118
|
-
expect(output).toContain('operation(internal) /admin/users: {');
|
|
119
|
-
expect(output).toContain(' get(deprecated): {');
|
|
120
|
-
expect(output).toContain(' post(internal): {');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('modifier placement: operation(modifier) before path, verb(modifier) before colon', () => {
|
|
124
|
-
const ast = makeRoot([makeRoute('/admin', [makeOp('get', { modifiers: ['deprecated'] })], { modifiers: ['internal'] })]);
|
|
125
|
-
const output = printCk(ast);
|
|
126
|
-
expect(output).toMatch(/^operation\(internal\) \/admin: \{/m);
|
|
127
|
-
expect(output).toMatch(/^\s+get\(deprecated\): \{/m);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// ─── Security printing ────────────────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
function makeSecFields(fields: Partial<Pick<SecurityFields, 'policy'>>): SecurityFields {
|
|
134
|
-
return { ...fields, loc: makeLoc() };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
describe('printCk — security', () => {
|
|
138
|
-
it('prints operation-level security: none', () => {
|
|
139
|
-
const ast = makeRoot([makeRoute('/health', [makeOp('get', { security: 'none' })])]);
|
|
140
|
-
expect(printCk(ast)).toContain(' security: none');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('prints security block with policy: <name>', () => {
|
|
144
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get', { security: makeSecFields({ policy: 'paymentsWrite' }) })])]);
|
|
145
|
-
const out = printCk(ast);
|
|
146
|
-
expect(out).toContain(' security: {');
|
|
147
|
-
expect(out).toContain(' policy: paymentsWrite');
|
|
148
|
-
expect(out).toContain(' }');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('prints security block with policy: none for explicit bypass', () => {
|
|
152
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get', { security: makeSecFields({ policy: false }) })])]);
|
|
153
|
-
expect(printCk(ast)).toContain(' policy: none');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('prints operation-level signature as its own keyword', () => {
|
|
157
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('post', { signature: 'hmac-sha256' })])]);
|
|
158
|
-
const out = printCk(ast);
|
|
159
|
-
expect(out).toContain(' signature: "hmac-sha256"');
|
|
160
|
-
expect(out).not.toContain('security: {');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('prints unquoted identifier signature without quotes', () => {
|
|
164
|
-
const ast = makeRoot([makeRoute('/webhooks', [makeOp('post', { signature: 'MODERN_TREASURY_WEBHOOK' })])]);
|
|
165
|
-
expect(printCk(ast)).toContain(' signature: MODERN_TREASURY_WEBHOOK');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('prints block-form signature when a signature policy is set', () => {
|
|
169
|
-
const ast = makeRoot([
|
|
170
|
-
makeRoute('/webhooks', [makeOp('post', { signature: 'SLACK_WEBHOOK', signaturePolicy: 'slackSignatureValid' })]),
|
|
171
|
-
]);
|
|
172
|
-
const out = printCk(ast);
|
|
173
|
-
expect(out).toContain(' signature: {');
|
|
174
|
-
expect(out).toContain(' options: SLACK_WEBHOOK');
|
|
175
|
-
expect(out).toContain(' policy: slackSignatureValid');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('prints route-level security with shallower indentation', () => {
|
|
179
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get')], { security: makeSecFields({ policy: 'paymentsWrite' }) })]);
|
|
180
|
-
const out = printCk(ast);
|
|
181
|
-
expect(out).toContain(' security: {');
|
|
182
|
-
expect(out).toContain(' policy: paymentsWrite');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('prints route-level security: none', () => {
|
|
186
|
-
const ast = makeRoot([makeRoute('/public', [makeOp('get')], { security: 'none' })]);
|
|
187
|
-
expect(printCk(ast)).toContain(' security: none');
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('emits no security line when security is undefined', () => {
|
|
191
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get')])]);
|
|
192
|
-
expect(printCk(ast)).not.toContain('security');
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// ─── plugins block printing ───────────────────────────────────────────────────
|
|
197
|
-
|
|
198
|
-
describe('printCk — plugins block', () => {
|
|
199
|
-
it('prints plugins block with single entry', () => {
|
|
200
|
-
const ast = makeRoot([makeRoute('/auth/token', [makeOp('post', { plugins: { bruno: 'request-token.yml' } })])]);
|
|
201
|
-
const out = printCk(ast);
|
|
202
|
-
expect(out).toContain(' plugins: {');
|
|
203
|
-
expect(out).toContain(' bruno: "request-token.yml"');
|
|
204
|
-
expect(out).toContain(' }');
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('prints plugins block with multiple entries', () => {
|
|
208
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('post', { plugins: { bruno: 'create-user.yml', typescript: 'stub.ts' } })])]);
|
|
209
|
-
const out = printCk(ast);
|
|
210
|
-
expect(out).toContain(' bruno: "create-user.yml"');
|
|
211
|
-
expect(out).toContain(' typescript: "stub.ts"');
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('omits plugins block when plugins is undefined', () => {
|
|
215
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get')])]);
|
|
216
|
-
expect(printCk(ast)).not.toContain('plugins');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('omits plugins block when plugins is empty object', () => {
|
|
220
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get', { plugins: {} })])]);
|
|
221
|
-
expect(printCk(ast)).not.toContain('plugins');
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('plugins block round-trips via parse', () => {
|
|
225
|
-
const src = `operation /auth/token: {
|
|
226
|
-
post: {
|
|
227
|
-
plugins: {
|
|
228
|
-
bruno: "request-token.yml"
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
`;
|
|
233
|
-
const diag = new DiagnosticCollector();
|
|
234
|
-
const ast = parseCk(src, 'test.ck', diag);
|
|
235
|
-
expect(diag.hasErrors()).toBe(false);
|
|
236
|
-
const out = printCk(ast);
|
|
237
|
-
expect(out).toContain(' bruno: "request-token.yml"');
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('prints object plugin value with file:// URL', () => {
|
|
241
|
-
const ast = makeRoot([
|
|
242
|
-
makeRoute('/auth/token', [
|
|
243
|
-
makeOp('post', { plugins: { bruno: { template: 'file://request-token.yml' } } }),
|
|
244
|
-
]),
|
|
245
|
-
]);
|
|
246
|
-
const out = printCk(ast);
|
|
247
|
-
expect(out).toContain(' bruno: {');
|
|
248
|
-
expect(out).toContain(' template: "file://request-token.yml"');
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('round-trips object plugin value through parse', () => {
|
|
252
|
-
const src = `operation /auth/token: {
|
|
253
|
-
post: {
|
|
254
|
-
plugins: {
|
|
255
|
-
bruno: {
|
|
256
|
-
template: "file://request-token.yml"
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
`;
|
|
262
|
-
const diag = new DiagnosticCollector();
|
|
263
|
-
const ast = parseCk(src, 'test.ck', diag);
|
|
264
|
-
expect(diag.hasErrors()).toBe(false);
|
|
265
|
-
const out = printCk(ast);
|
|
266
|
-
expect(out).toContain(' bruno: {');
|
|
267
|
-
expect(out).toContain(' template: "file://request-token.yml"');
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('prints scalar, array, and nested-object plugin values', () => {
|
|
271
|
-
const ast = makeRoot([
|
|
272
|
-
makeRoute('/x', [
|
|
273
|
-
makeOp('get', {
|
|
274
|
-
plugins: {
|
|
275
|
-
flag: true,
|
|
276
|
-
count: 3,
|
|
277
|
-
tags: ['a', 'b'],
|
|
278
|
-
meta: { nested: { x: 1 } },
|
|
279
|
-
},
|
|
280
|
-
}),
|
|
281
|
-
]),
|
|
282
|
-
]);
|
|
283
|
-
const out = printCk(ast);
|
|
284
|
-
expect(out).toContain('flag: true');
|
|
285
|
-
expect(out).toContain('count: 3');
|
|
286
|
-
expect(out).toContain('tags: [');
|
|
287
|
-
expect(out).toContain('meta: {');
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// ─── Query / headers with descriptions ───────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
describe('printCk — query and headers descriptions', () => {
|
|
294
|
-
it('emits inline comment on query params that have descriptions', () => {
|
|
295
|
-
const loc = makeLoc();
|
|
296
|
-
const ast = makeRoot([
|
|
297
|
-
makeRoute('/users', [
|
|
298
|
-
makeOp('get', {
|
|
299
|
-
query: [
|
|
300
|
-
{ name: 'page', optional: false, nullable: false, type: { kind: 'scalar', name: 'int' }, description: 'Page number', loc },
|
|
301
|
-
{ name: 'limit', optional: false, nullable: false, type: { kind: 'scalar', name: 'int' }, loc },
|
|
302
|
-
],
|
|
303
|
-
}),
|
|
304
|
-
]),
|
|
305
|
-
]);
|
|
306
|
-
const out = printCk(ast);
|
|
307
|
-
expect(out).toContain(' page: int # Page number');
|
|
308
|
-
expect(out).toContain(' limit: int');
|
|
309
|
-
expect(out).not.toMatch(/limit: int #/);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('emits inline comment on headers params that have descriptions', () => {
|
|
313
|
-
const loc = makeLoc();
|
|
314
|
-
const ast = makeRoot([
|
|
315
|
-
makeRoute('/users', [
|
|
316
|
-
makeOp('get', {
|
|
317
|
-
headers: [
|
|
318
|
-
{
|
|
319
|
-
name: 'X-Request-Id',
|
|
320
|
-
optional: false,
|
|
321
|
-
nullable: false,
|
|
322
|
-
type: { kind: 'scalar', name: 'uuid' },
|
|
323
|
-
description: 'Idempotency key',
|
|
324
|
-
loc,
|
|
325
|
-
},
|
|
326
|
-
],
|
|
327
|
-
}),
|
|
328
|
-
]),
|
|
329
|
-
]);
|
|
330
|
-
expect(printCk(ast)).toContain(' X-Request-Id: uuid # Idempotency key');
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
// ─── Query / headers — optional, nullable, defaults ──────────────────────────
|
|
335
|
-
|
|
336
|
-
describe('printCk — query and headers optional, nullable, default', () => {
|
|
337
|
-
const loc = makeLoc();
|
|
338
|
-
|
|
339
|
-
it('emits ? for optional query params', () => {
|
|
340
|
-
const ast = makeRoot([
|
|
341
|
-
makeRoute('/search', [
|
|
342
|
-
makeOp('get', {
|
|
343
|
-
query: [
|
|
344
|
-
{ name: 'q', optional: true, nullable: false, type: { kind: 'scalar', name: 'string' }, loc },
|
|
345
|
-
{ name: 'page', optional: false, nullable: false, type: { kind: 'scalar', name: 'int' }, loc },
|
|
346
|
-
],
|
|
347
|
-
}),
|
|
348
|
-
]),
|
|
349
|
-
]);
|
|
350
|
-
const out = printCk(ast);
|
|
351
|
-
expect(out).toContain(' q?: string');
|
|
352
|
-
expect(out).toContain(' page: int');
|
|
353
|
-
expect(out).not.toContain('page?:');
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it('emits | null for nullable query params', () => {
|
|
357
|
-
const ast = makeRoot([
|
|
358
|
-
makeRoute('/items', [
|
|
359
|
-
makeOp('get', {
|
|
360
|
-
query: [{ name: 'filter', optional: false, nullable: true, type: { kind: 'scalar', name: 'string' }, loc }],
|
|
361
|
-
}),
|
|
362
|
-
]),
|
|
363
|
-
]);
|
|
364
|
-
expect(printCk(ast)).toContain(' filter: string | null');
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it('emits = value for numeric defaults', () => {
|
|
368
|
-
const ast = makeRoot([
|
|
369
|
-
makeRoute('/items', [
|
|
370
|
-
makeOp('get', {
|
|
371
|
-
query: [{ name: 'limit', optional: false, nullable: false, type: { kind: 'scalar', name: 'int' }, default: 20, loc }],
|
|
372
|
-
}),
|
|
373
|
-
]),
|
|
374
|
-
]);
|
|
375
|
-
expect(printCk(ast)).toContain(' limit: int = 20');
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it('emits = value for boolean defaults', () => {
|
|
379
|
-
const ast = makeRoot([
|
|
380
|
-
makeRoute('/items', [
|
|
381
|
-
makeOp('get', {
|
|
382
|
-
query: [{ name: 'active', optional: false, nullable: false, type: { kind: 'scalar', name: 'boolean' }, default: true, loc }],
|
|
383
|
-
}),
|
|
384
|
-
]),
|
|
385
|
-
]);
|
|
386
|
-
expect(printCk(ast)).toContain(' active: boolean = true');
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it('emits = "value" for string defaults that need quoting', () => {
|
|
390
|
-
const ast = makeRoot([
|
|
391
|
-
makeRoute('/items', [
|
|
392
|
-
makeOp('get', {
|
|
393
|
-
query: [
|
|
394
|
-
{ name: 'label', optional: false, nullable: false, type: { kind: 'scalar', name: 'string' }, default: 'hello world', loc },
|
|
395
|
-
],
|
|
396
|
-
}),
|
|
397
|
-
]),
|
|
398
|
-
]);
|
|
399
|
-
expect(printCk(ast)).toContain(' label: string = "hello world"');
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it('combines optional, default, and description on the same param', () => {
|
|
403
|
-
const ast = makeRoot([
|
|
404
|
-
makeRoute('/items', [
|
|
405
|
-
makeOp('get', {
|
|
406
|
-
query: [
|
|
407
|
-
{
|
|
408
|
-
name: 'page',
|
|
409
|
-
optional: true,
|
|
410
|
-
nullable: false,
|
|
411
|
-
type: { kind: 'scalar', name: 'int' },
|
|
412
|
-
default: 1,
|
|
413
|
-
description: 'Page number',
|
|
414
|
-
loc,
|
|
415
|
-
},
|
|
416
|
-
],
|
|
417
|
-
}),
|
|
418
|
-
]),
|
|
419
|
-
]);
|
|
420
|
-
expect(printCk(ast)).toContain(' page?: int = 1 # Page number');
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it('applies the same rules to headers params', () => {
|
|
424
|
-
const ast = makeRoot([
|
|
425
|
-
makeRoute('/items', [
|
|
426
|
-
makeOp('get', {
|
|
427
|
-
headers: [{ name: 'X-Version', optional: true, nullable: false, type: { kind: 'scalar', name: 'string' }, default: 'v1', loc }],
|
|
428
|
-
}),
|
|
429
|
-
]),
|
|
430
|
-
]);
|
|
431
|
-
expect(printCk(ast)).toContain(' X-Version?: string = v1');
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
// ─── Request blocks ──────────────────────────────────────────────────────────
|
|
436
|
-
|
|
437
|
-
describe('printCk — request blocks', () => {
|
|
438
|
-
it('prints a single content-type request', () => {
|
|
439
|
-
const ast = makeRoot([
|
|
440
|
-
makeRoute('/users', [
|
|
441
|
-
makeOp('post', {
|
|
442
|
-
request: { bodies: [{ contentType: 'application/json', bodyType: { kind: 'ref', name: 'CreateUser' } }] },
|
|
443
|
-
}),
|
|
444
|
-
]),
|
|
445
|
-
]);
|
|
446
|
-
const out = printCk(ast);
|
|
447
|
-
expect(out).toContain(' request: {');
|
|
448
|
-
expect(out).toContain(' application/json: CreateUser');
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it('round-trips vendor JSON mime types like application/vnd.api+json', () => {
|
|
452
|
-
const ast = makeRoot([
|
|
453
|
-
makeRoute('/users', [
|
|
454
|
-
makeOp('post', {
|
|
455
|
-
request: { bodies: [{ contentType: 'application/vnd.api+json', bodyType: { kind: 'ref', name: 'CreateUser' } }] },
|
|
456
|
-
responses: [{ statusCode: 201, hasBlock: true, bodies: [{ contentType: 'application/vnd.api+json', bodyType: { kind: 'ref', name: 'User' } }] }],
|
|
457
|
-
}),
|
|
458
|
-
]),
|
|
459
|
-
]);
|
|
460
|
-
const out = printCk(ast);
|
|
461
|
-
expect(out).toContain(' application/vnd.api+json: CreateUser');
|
|
462
|
-
expect(out).toContain(' application/vnd.api+json: User');
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
it('prints multiple content-types preserving source order', () => {
|
|
466
|
-
const ast = makeRoot([
|
|
467
|
-
makeRoute('/auth/token', [
|
|
468
|
-
makeOp('post', {
|
|
469
|
-
request: {
|
|
470
|
-
bodies: [
|
|
471
|
-
{ contentType: 'application/json', bodyType: { kind: 'ref', name: 'AuthRequest' } },
|
|
472
|
-
{ contentType: 'application/x-www-form-urlencoded', bodyType: { kind: 'ref', name: 'AuthRequest' } },
|
|
473
|
-
],
|
|
474
|
-
},
|
|
475
|
-
}),
|
|
476
|
-
]),
|
|
477
|
-
]);
|
|
478
|
-
const out = printCk(ast);
|
|
479
|
-
expect(out).toContain(' application/json: AuthRequest');
|
|
480
|
-
expect(out).toContain(' application/x-www-form-urlencoded: AuthRequest');
|
|
481
|
-
// Order check
|
|
482
|
-
const i1 = out.indexOf('application/json:');
|
|
483
|
-
const i2 = out.indexOf('application/x-www-form-urlencoded:');
|
|
484
|
-
expect(i1).toBeLessThan(i2);
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
describe('printCk — response headers', () => {
|
|
489
|
-
it('prints response headers alongside body', () => {
|
|
490
|
-
const ast = makeRoot([
|
|
491
|
-
makeRoute('/transfers/{id}', [
|
|
492
|
-
makeOp('get', {
|
|
493
|
-
responses: [
|
|
494
|
-
{
|
|
495
|
-
statusCode: 200,
|
|
496
|
-
hasBlock: true,
|
|
497
|
-
bodies: [{ contentType: 'application/json', bodyType: { kind: 'ref', name: 'Transfer' } }],
|
|
498
|
-
headers: [
|
|
499
|
-
{ name: 'preference-applied', optional: true, type: { kind: 'scalar', name: 'string' } },
|
|
500
|
-
{ name: 'etag', optional: false, type: { kind: 'scalar', name: 'string' }, description: 'cache validator' },
|
|
501
|
-
],
|
|
502
|
-
},
|
|
503
|
-
],
|
|
504
|
-
}),
|
|
505
|
-
]),
|
|
506
|
-
]);
|
|
507
|
-
const out = printCk(ast);
|
|
508
|
-
expect(out).toContain(' 200: {');
|
|
509
|
-
expect(out).toContain(' application/json: Transfer');
|
|
510
|
-
expect(out).toContain(' headers: {');
|
|
511
|
-
expect(out).toContain(' preference-applied?: string');
|
|
512
|
-
expect(out).toContain(' etag: string # cache validator');
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
it('prints response headers without a body', () => {
|
|
516
|
-
const ast = makeRoot([
|
|
517
|
-
makeRoute('/resources/{id}', [
|
|
518
|
-
makeOp('delete', {
|
|
519
|
-
responses: [
|
|
520
|
-
{
|
|
521
|
-
statusCode: 204,
|
|
522
|
-
hasBlock: true,
|
|
523
|
-
bodies: [],
|
|
524
|
-
headers: [{ name: 'x-deleted-at', optional: false, type: { kind: 'scalar', name: 'string' } }],
|
|
525
|
-
},
|
|
526
|
-
],
|
|
527
|
-
}),
|
|
528
|
-
]),
|
|
529
|
-
]);
|
|
530
|
-
const out = printCk(ast);
|
|
531
|
-
expect(out).toContain(' 204: {');
|
|
532
|
-
expect(out).toContain(' headers: {');
|
|
533
|
-
expect(out).toContain(' x-deleted-at: string');
|
|
534
|
-
expect(out).not.toContain('application/json');
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
describe('printCk — mcp field', () => {
|
|
539
|
-
function roundTrip(source: string): string {
|
|
540
|
-
const diag = new DiagnosticCollector();
|
|
541
|
-
const ast = parseCk(source, 'test.ck', diag);
|
|
542
|
-
expect(diag.hasErrors()).toBe(false);
|
|
543
|
-
return printCk(ast);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
it('prints mcp: true', () => {
|
|
547
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get', { mcp: true })])]);
|
|
548
|
-
expect(printCk(ast)).toContain(' mcp: true');
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
it('prints explicit mcp: false', () => {
|
|
552
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get', { mcp: false })])]);
|
|
553
|
-
expect(printCk(ast)).toContain(' mcp: false');
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it('omits mcp when undefined', () => {
|
|
557
|
-
const ast = makeRoot([makeRoute('/users', [makeOp('get')])]);
|
|
558
|
-
expect(printCk(ast)).not.toContain('mcp:');
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
it('prints mcp block with text fields and reconstructed hint list', () => {
|
|
562
|
-
const ast = makeRoot([
|
|
563
|
-
makeRoute('/routes', [
|
|
564
|
-
makeOp('post', {
|
|
565
|
-
mcp: {
|
|
566
|
-
name: 'searchRoutes',
|
|
567
|
-
title: 'Search routes',
|
|
568
|
-
description: 'Full-text search.',
|
|
569
|
-
readOnlyHint: true,
|
|
570
|
-
idempotentHint: true,
|
|
571
|
-
destructiveHint: false,
|
|
572
|
-
loc: makeLoc(),
|
|
573
|
-
},
|
|
574
|
-
}),
|
|
575
|
-
]),
|
|
576
|
-
]);
|
|
577
|
-
const out = printCk(ast);
|
|
578
|
-
expect(out).toContain(' mcp: {');
|
|
579
|
-
expect(out).toContain(' name: "searchRoutes"');
|
|
580
|
-
expect(out).toContain(' title: "Search routes"');
|
|
581
|
-
expect(out).toContain(' description: "Full-text search."');
|
|
582
|
-
expect(out).toContain(' hint: readOnly, idempotent, nonDestructive');
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
it('round-trips mcp: true / false through parse', () => {
|
|
586
|
-
expect(roundTrip('operation /a: {\n get: {\n mcp: true\n }\n}\n')).toContain(' mcp: true');
|
|
587
|
-
expect(roundTrip('operation /b: {\n get: {\n mcp: false\n }\n}\n')).toContain(' mcp: false');
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
it('round-trips an mcp settings block through parse', () => {
|
|
591
|
-
const source = `\
|
|
592
|
-
operation /routes: {
|
|
593
|
-
post: {
|
|
594
|
-
mcp: {
|
|
595
|
-
name: "searchRoutes"
|
|
596
|
-
title: "Search routes"
|
|
597
|
-
description: "Full-text search across routes."
|
|
598
|
-
hint: readOnly, idempotent, nonDestructive, closedWorld
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
`;
|
|
603
|
-
const out = roundTrip(source);
|
|
604
|
-
expect(out).toContain(' mcp: {');
|
|
605
|
-
expect(out).toContain(' name: "searchRoutes"');
|
|
606
|
-
expect(out).toContain(' hint: readOnly, idempotent, nonDestructive, closedWorld');
|
|
607
|
-
// Idempotent: printing the round-tripped output again is stable.
|
|
608
|
-
const diag = new DiagnosticCollector();
|
|
609
|
-
expect(printCk(parseCk(out, 'test.ck', diag))).toBe(out);
|
|
610
|
-
expect(diag.hasErrors()).toBe(false);
|
|
611
|
-
});
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
describe('printCk — options-level header globals (round-trip)', () => {
|
|
615
|
-
function roundTrip(source: string): string {
|
|
616
|
-
const diag = new DiagnosticCollector();
|
|
617
|
-
const ast = parseCk(source, 'test.ck', diag);
|
|
618
|
-
expect(diag.hasErrors()).toBe(false);
|
|
619
|
-
return printCk(ast);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
it('round-trips options.request.headers', () => {
|
|
623
|
-
const source = `\
|
|
624
|
-
options {
|
|
625
|
-
request: {
|
|
626
|
-
headers: {
|
|
627
|
-
x-request-id: uuid
|
|
628
|
-
authorization?: string
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
operation /widgets: {
|
|
634
|
-
get: {
|
|
635
|
-
response: {
|
|
636
|
-
200: {
|
|
637
|
-
application/json: Widget
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
`;
|
|
643
|
-
const out = roundTrip(source);
|
|
644
|
-
expect(out).toBe(source);
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
it('round-trips options.response.headers', () => {
|
|
648
|
-
const source = `\
|
|
649
|
-
options {
|
|
650
|
-
response: {
|
|
651
|
-
headers: {
|
|
652
|
-
x-request-id: uuid
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
operation /widgets: {
|
|
658
|
-
get: {
|
|
659
|
-
response: {
|
|
660
|
-
200: {
|
|
661
|
-
application/json: Widget
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
`;
|
|
667
|
-
expect(roundTrip(source)).toBe(source);
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
it('round-trips operation-level headers: none', () => {
|
|
671
|
-
const source = `\
|
|
672
|
-
operation /widgets: {
|
|
673
|
-
get: {
|
|
674
|
-
headers: none
|
|
675
|
-
response: {
|
|
676
|
-
200: {
|
|
677
|
-
application/json: Widget
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
`;
|
|
683
|
-
expect(roundTrip(source)).toBe(source);
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
it('round-trips per-status headers: none', () => {
|
|
687
|
-
const source = `\
|
|
688
|
-
operation /widgets: {
|
|
689
|
-
get: {
|
|
690
|
-
response: {
|
|
691
|
-
200: {
|
|
692
|
-
application/json: Widget
|
|
693
|
-
}
|
|
694
|
-
404: {
|
|
695
|
-
headers: none
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
`;
|
|
701
|
-
expect(roundTrip(source)).toBe(source);
|
|
702
|
-
});
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
describe('printCk — multi-base inheritance and override (round-trip)', () => {
|
|
706
|
-
function roundTrip(source: string): string {
|
|
707
|
-
const diag = new DiagnosticCollector();
|
|
708
|
-
const ast = parseCk(source, 'test.ck', diag);
|
|
709
|
-
expect(diag.hasErrors()).toBe(false);
|
|
710
|
-
return printCk(ast);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
it('round-trips multi-base inheritance', () => {
|
|
714
|
-
const source = `\
|
|
715
|
-
contract Test5: A & B & C & D & {
|
|
716
|
-
e: string
|
|
717
|
-
}
|
|
718
|
-
`;
|
|
719
|
-
expect(roundTrip(source)).toBe(source);
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
it('round-trips override modifier on a field', () => {
|
|
723
|
-
const source = `\
|
|
724
|
-
contract Test5: A & {
|
|
725
|
-
a: override int
|
|
726
|
-
e: string
|
|
727
|
-
}
|
|
728
|
-
`;
|
|
729
|
-
expect(roundTrip(source)).toBe(source);
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
it('emits canonical modifier order: override → deprecated → readonly', () => {
|
|
733
|
-
const source = `\
|
|
734
|
-
contract Test: A & {
|
|
735
|
-
a: deprecated override readonly string
|
|
736
|
-
}
|
|
737
|
-
`;
|
|
738
|
-
// Canonical order is override → deprecated → readonly|writeonly.
|
|
739
|
-
// Re-printing reorders the modifiers.
|
|
740
|
-
const printed = roundTrip(source);
|
|
741
|
-
expect(printed).toContain('a: override deprecated readonly string');
|
|
742
|
-
});
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
describe('printCk — typed path params (round-trip)', () => {
|
|
746
|
-
function roundTrip(source: string): string {
|
|
747
|
-
const diag = new DiagnosticCollector();
|
|
748
|
-
const ast = parseCk(source, 'test.ck', diag);
|
|
749
|
-
expect(diag.hasErrors()).toBe(false);
|
|
750
|
-
return printCk(ast);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
it('round-trips path params with constraint arguments', () => {
|
|
754
|
-
const source = `\
|
|
755
|
-
operation /orders/{orderId}: {
|
|
756
|
-
params: {
|
|
757
|
-
orderId: int(min=1, max=5)
|
|
758
|
-
}
|
|
759
|
-
get: {
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
`;
|
|
763
|
-
expect(roundTrip(source)).toBe(source);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
it('round-trips path params with enum types', () => {
|
|
767
|
-
const source = `\
|
|
768
|
-
operation /pets/{status}: {
|
|
769
|
-
params: {
|
|
770
|
-
status: enum(available, pending, sold)
|
|
771
|
-
}
|
|
772
|
-
get: {
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
`;
|
|
776
|
-
expect(roundTrip(source)).toBe(source);
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
it('round-trips path params with regex constraints and a comment', () => {
|
|
780
|
-
const source = `\
|
|
781
|
-
operation /users/{slug}: {
|
|
782
|
-
params: {
|
|
783
|
-
slug: string(regex=/^[a-z0-9-]+$/) # url slug
|
|
784
|
-
}
|
|
785
|
-
get: {
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
`;
|
|
789
|
-
expect(roundTrip(source)).toBe(source);
|
|
790
|
-
});
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
describe('printCk — options keys and services quoting', () => {
|
|
794
|
-
function roundTrip(source: string): string {
|
|
795
|
-
const diag = new DiagnosticCollector();
|
|
796
|
-
const ast = parseCk(source, 'test.ck', diag);
|
|
797
|
-
expect(diag.hasErrors()).toBe(false);
|
|
798
|
-
return printCk(ast);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
it('leaves simple identifier keys values unquoted', () => {
|
|
802
|
-
const source = `\
|
|
803
|
-
options {
|
|
804
|
-
keys: {
|
|
805
|
-
area: payments
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
`;
|
|
809
|
-
expect(roundTrip(source)).toBe(source);
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
it('preserves quotes on path-like values containing slashes', () => {
|
|
813
|
-
const source = `\
|
|
814
|
-
options {
|
|
815
|
-
keys: {
|
|
816
|
-
bruno: "../../bruno/"
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
`;
|
|
820
|
-
expect(roundTrip(source)).toBe(source);
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
it('quotes values that start with a dot', () => {
|
|
824
|
-
const source = `\
|
|
825
|
-
options {
|
|
826
|
-
keys: {
|
|
827
|
-
path: "../relative"
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
`;
|
|
831
|
-
expect(roundTrip(source)).toBe(source);
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
it('preserves quotes on service paths starting with #', () => {
|
|
835
|
-
const source = `\
|
|
836
|
-
options {
|
|
837
|
-
services: {
|
|
838
|
-
AuthService: "#src/modules/auth/auth.service.js"
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
`;
|
|
842
|
-
expect(roundTrip(source)).toBe(source);
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
it('leaves plain identifier service names unquoted', () => {
|
|
846
|
-
const source = `\
|
|
847
|
-
options {
|
|
848
|
-
services: {
|
|
849
|
-
AuthService: authService
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
`;
|
|
853
|
-
expect(roundTrip(source)).toBe(source);
|
|
854
|
-
});
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
describe('printCk — enum values with spaces (round-trip)', () => {
|
|
858
|
-
function roundTrip(source: string): string {
|
|
859
|
-
const diag = new DiagnosticCollector();
|
|
860
|
-
const ast = parseCk(source, 'test.ck', diag);
|
|
861
|
-
expect(diag.hasErrors()).toBe(false);
|
|
862
|
-
return printCk(ast);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
it('round-trips enum with bare identifiers unchanged', () => {
|
|
866
|
-
const source = `\
|
|
867
|
-
contract M: {
|
|
868
|
-
status: enum(active, inactive, pending)
|
|
869
|
-
}
|
|
870
|
-
`;
|
|
871
|
-
expect(roundTrip(source)).toBe(source);
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
it('round-trips enum with quoted multi-word values', () => {
|
|
875
|
-
const source = `\
|
|
876
|
-
contract M: {
|
|
877
|
-
entityType: enum("Sole Proprietorship", LLC, "Limited Partnership")
|
|
878
|
-
}
|
|
879
|
-
`;
|
|
880
|
-
expect(roundTrip(source)).toBe(source);
|
|
881
|
-
});
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
describe('printCk — trailing/orphan comments (round-trip)', () => {
|
|
885
|
-
function roundTrip(source: string): string {
|
|
886
|
-
const diag = new DiagnosticCollector();
|
|
887
|
-
const ast = parseCk(source, 'test.ck', diag);
|
|
888
|
-
expect(diag.hasErrors()).toBe(false);
|
|
889
|
-
return printCk(ast);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
it('preserves a comment on the last line of a contract body', () => {
|
|
893
|
-
const source = `\
|
|
894
|
-
contract Payment: {
|
|
895
|
-
id: uuid
|
|
896
|
-
# more fields to come
|
|
897
|
-
}
|
|
898
|
-
`;
|
|
899
|
-
const out = roundTrip(source);
|
|
900
|
-
expect(out).toContain('# more fields to come');
|
|
901
|
-
// Comment sits on its own line after the last field, before the closing brace.
|
|
902
|
-
expect(out).toBe(source);
|
|
903
|
-
// Idempotent: formatting the output again yields the same result.
|
|
904
|
-
expect(roundTrip(out)).toBe(out);
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
it('preserves multiple trailing comments in a contract body', () => {
|
|
908
|
-
const source = `\
|
|
909
|
-
contract Payment: {
|
|
910
|
-
id: uuid
|
|
911
|
-
# note one
|
|
912
|
-
# note two
|
|
913
|
-
}
|
|
914
|
-
`;
|
|
915
|
-
const out = roundTrip(source);
|
|
916
|
-
expect(out).toContain('# note one');
|
|
917
|
-
expect(out).toContain('# note two');
|
|
918
|
-
expect(out).toBe(source);
|
|
919
|
-
expect(roundTrip(out)).toBe(out);
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
it('preserves a comment as the only content of a contract body', () => {
|
|
923
|
-
const source = `\
|
|
924
|
-
contract Empty: {
|
|
925
|
-
# nothing here yet
|
|
926
|
-
}
|
|
927
|
-
`;
|
|
928
|
-
const out = roundTrip(source);
|
|
929
|
-
expect(out).toContain('# nothing here yet');
|
|
930
|
-
expect(out).toBe(source);
|
|
931
|
-
expect(roundTrip(out)).toBe(out);
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
it('preserves a comment on the last line of an operation/route body', () => {
|
|
935
|
-
const source = `\
|
|
936
|
-
operation /users: {
|
|
937
|
-
get: {
|
|
938
|
-
}
|
|
939
|
-
# TODO: add post
|
|
940
|
-
}
|
|
941
|
-
`;
|
|
942
|
-
const out = roundTrip(source);
|
|
943
|
-
expect(out).toContain('# TODO: add post');
|
|
944
|
-
expect(out).toBe(source);
|
|
945
|
-
expect(roundTrip(out)).toBe(out);
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
it('does not double-emit a header inline comment on a contract with no fields', () => {
|
|
949
|
-
const source = `\
|
|
950
|
-
contract Empty: { # nothing here yet
|
|
951
|
-
}
|
|
952
|
-
`;
|
|
953
|
-
const out = roundTrip(source);
|
|
954
|
-
// The inline header comment must survive exactly once, not be duplicated on a trailing line.
|
|
955
|
-
expect(out.match(/# nothing here yet/g)?.length).toBe(1);
|
|
956
|
-
expect(roundTrip(out)).toBe(out);
|
|
957
|
-
});
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
describe('printCk — inline object trailing comments (round-trip)', () => {
|
|
961
|
-
function roundTrip(source: string): string {
|
|
962
|
-
const diag = new DiagnosticCollector();
|
|
963
|
-
const ast = parseCk(source, 'test.ck', diag);
|
|
964
|
-
expect(diag.hasErrors()).toBe(false);
|
|
965
|
-
return printCk(ast);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
it('preserves a trailing comment as the last inner item of an inline object type', () => {
|
|
969
|
-
const source = `\
|
|
970
|
-
contract Foo: {
|
|
971
|
-
bar: {
|
|
972
|
-
a: string
|
|
973
|
-
# trailing note
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
`;
|
|
977
|
-
const out = roundTrip(source);
|
|
978
|
-
expect(out).toContain('# trailing note');
|
|
979
|
-
expect(out).toBe(source);
|
|
980
|
-
expect(roundTrip(out)).toBe(out);
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
it('preserves multiple trailing comments in an inline object type', () => {
|
|
984
|
-
const source = `\
|
|
985
|
-
contract Foo: {
|
|
986
|
-
bar: {
|
|
987
|
-
a: string
|
|
988
|
-
# note one
|
|
989
|
-
# note two
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
`;
|
|
993
|
-
const out = roundTrip(source);
|
|
994
|
-
expect(out).toContain('# note one');
|
|
995
|
-
expect(out).toContain('# note two');
|
|
996
|
-
expect(out).toBe(source);
|
|
997
|
-
expect(roundTrip(out)).toBe(out);
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
it('preserves a trailing comment as the only inner item of an inline object type', () => {
|
|
1001
|
-
const source = `\
|
|
1002
|
-
contract Foo: {
|
|
1003
|
-
bar: {
|
|
1004
|
-
# placeholder
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
`;
|
|
1008
|
-
const out = roundTrip(source);
|
|
1009
|
-
expect(out).toContain('# placeholder');
|
|
1010
|
-
expect(out).toBe(source);
|
|
1011
|
-
expect(roundTrip(out)).toBe(out);
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
it('still attaches an inline comment on an inline field (no regression)', () => {
|
|
1015
|
-
const source = `\
|
|
1016
|
-
contract Foo: {
|
|
1017
|
-
bar: {
|
|
1018
|
-
a: string # about a
|
|
1019
|
-
b: int
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
`;
|
|
1023
|
-
const out = roundTrip(source);
|
|
1024
|
-
expect(out).toContain('a: string # about a');
|
|
1025
|
-
expect(out).toBe(source);
|
|
1026
|
-
expect(roundTrip(out)).toBe(out);
|
|
1027
|
-
});
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
describe('printCk — options block sub-block comments (round-trip)', () => {
|
|
1031
|
-
function roundTrip(source: string): string {
|
|
1032
|
-
const diag = new DiagnosticCollector();
|
|
1033
|
-
const ast = parseCk(source, 'test.ck', diag);
|
|
1034
|
-
expect(diag.hasErrors()).toBe(false);
|
|
1035
|
-
return printCk(ast);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
it('preserves a leading comment on a keys entry', () => {
|
|
1039
|
-
const source = `\
|
|
1040
|
-
options {
|
|
1041
|
-
keys: {
|
|
1042
|
-
# the api area
|
|
1043
|
-
area: payments
|
|
1044
|
-
version: v2
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
`;
|
|
1048
|
-
const out = roundTrip(source);
|
|
1049
|
-
expect(out).toContain('# the api area');
|
|
1050
|
-
expect(out).toBe(source);
|
|
1051
|
-
expect(roundTrip(out)).toBe(out);
|
|
1052
|
-
});
|
|
1053
|
-
|
|
1054
|
-
it('preserves a trailing comment at the end of the keys sub-block', () => {
|
|
1055
|
-
const source = `\
|
|
1056
|
-
options {
|
|
1057
|
-
keys: {
|
|
1058
|
-
area: payments
|
|
1059
|
-
# more keys to come
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
`;
|
|
1063
|
-
const out = roundTrip(source);
|
|
1064
|
-
expect(out).toContain('# more keys to come');
|
|
1065
|
-
expect(out).toBe(source);
|
|
1066
|
-
expect(roundTrip(out)).toBe(out);
|
|
1067
|
-
});
|
|
1068
|
-
|
|
1069
|
-
it('preserves a leading comment on a services entry', () => {
|
|
1070
|
-
const source = `\
|
|
1071
|
-
options {
|
|
1072
|
-
services: {
|
|
1073
|
-
# payments backend
|
|
1074
|
-
PaymentsService: "#src/services/payments.service.js"
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
`;
|
|
1078
|
-
const out = roundTrip(source);
|
|
1079
|
-
expect(out).toContain('# payments backend');
|
|
1080
|
-
expect(out).toBe(source);
|
|
1081
|
-
expect(roundTrip(out)).toBe(out);
|
|
1082
|
-
});
|
|
1083
|
-
|
|
1084
|
-
it('preserves a comment at the end of the last options sub-block (end of options block)', () => {
|
|
1085
|
-
const source = `\
|
|
1086
|
-
options {
|
|
1087
|
-
keys: {
|
|
1088
|
-
area: payments
|
|
1089
|
-
}
|
|
1090
|
-
services: {
|
|
1091
|
-
PaymentsService: "#src/services/payments.service.js"
|
|
1092
|
-
# end of options
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
`;
|
|
1096
|
-
const out = roundTrip(source);
|
|
1097
|
-
expect(out).toContain('# end of options');
|
|
1098
|
-
expect(out).toBe(source);
|
|
1099
|
-
expect(roundTrip(out)).toBe(out);
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
it('preserves both leading and trailing comments across keys and services', () => {
|
|
1103
|
-
const source = `\
|
|
1104
|
-
options {
|
|
1105
|
-
keys: {
|
|
1106
|
-
# area comment
|
|
1107
|
-
area: payments
|
|
1108
|
-
# trailing keys
|
|
1109
|
-
}
|
|
1110
|
-
services: {
|
|
1111
|
-
# service comment
|
|
1112
|
-
PaymentsService: "#src/services/payments.service.js"
|
|
1113
|
-
# trailing services
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
`;
|
|
1117
|
-
const out = roundTrip(source);
|
|
1118
|
-
expect(out).toBe(source);
|
|
1119
|
-
expect(roundTrip(out)).toBe(out);
|
|
1120
|
-
});
|
|
1121
|
-
});
|