@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/round-trip.test.ts
DELETED
|
@@ -1,694 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { printCk } from '../src/print-ck.js';
|
|
5
|
-
import { parseCk, DiagnosticCollector } from '@contractkit/core';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Formatting a `.ck` file must not change it.
|
|
9
|
-
*
|
|
10
|
-
* The prettier plugin used to fold standalone `#` comment blocks into trailing comments on the
|
|
11
|
-
* following declaration, reorder operation body keys, drop blank lines between operations, and
|
|
12
|
-
* expand single-line response bodies — all of which silently rewrote a user's file on
|
|
13
|
-
* `pnpm format`. These tests pin the guarantee: parse → print is the identity on well-formed
|
|
14
|
-
* source, and printing is idempotent.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
function format(source: string, file = 'test.ck'): string {
|
|
18
|
-
const diag = new DiagnosticCollector();
|
|
19
|
-
const ast = parseCk(source, file, diag);
|
|
20
|
-
expect(diag.hasErrors()).toBe(false);
|
|
21
|
-
return printCk(ast);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ─── Repository contracts ────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
const CONTRACTS_DIR = new URL('../../../contracts', import.meta.url).pathname;
|
|
27
|
-
const ckFiles = readdirSync(CONTRACTS_DIR).filter(f => f.endsWith('.ck'));
|
|
28
|
-
|
|
29
|
-
describe('round-trip — repository .ck files', () => {
|
|
30
|
-
it('finds .ck files to check', () => {
|
|
31
|
-
expect(ckFiles.length).toBeGreaterThan(0);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
for (const name of ckFiles) {
|
|
35
|
-
it(`formats ${name} to itself`, () => {
|
|
36
|
-
const source = readFileSync(join(CONTRACTS_DIR, name), 'utf8');
|
|
37
|
-
expect(format(source, name)).toBe(source);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// ─── Constructs that previously round-tripped lossily ────────────────────────
|
|
43
|
-
|
|
44
|
-
describe('round-trip — comment placement', () => {
|
|
45
|
-
it('keeps a standalone comment block above the declaration it precedes', () => {
|
|
46
|
-
const source = `# ─── Pet endpoints ───
|
|
47
|
-
|
|
48
|
-
operation /pet: {
|
|
49
|
-
get: {
|
|
50
|
-
response: {
|
|
51
|
-
200:
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
`;
|
|
56
|
-
expect(format(source)).toBe(source);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('keeps a contract doc comment on its own line', () => {
|
|
60
|
-
const source = `# A pet for sale
|
|
61
|
-
contract Pet: {
|
|
62
|
-
id: int
|
|
63
|
-
}
|
|
64
|
-
`;
|
|
65
|
-
expect(format(source)).toBe(source);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('keeps a contract doc comment inline when written inline', () => {
|
|
69
|
-
const source = `contract Pet: { # A pet for sale
|
|
70
|
-
id: int
|
|
71
|
-
}
|
|
72
|
-
`;
|
|
73
|
-
expect(format(source)).toBe(source);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('distinguishes a divider from the doc comment below it', () => {
|
|
77
|
-
const source = `# ─── Models ───
|
|
78
|
-
|
|
79
|
-
# A pet for sale
|
|
80
|
-
contract Pet: {
|
|
81
|
-
id: int
|
|
82
|
-
}
|
|
83
|
-
`;
|
|
84
|
-
expect(format(source)).toBe(source);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('keeps an operation doc comment on its own line', () => {
|
|
88
|
-
const source = `operation /pet: {
|
|
89
|
-
# update an existing pet
|
|
90
|
-
put: {
|
|
91
|
-
response: {
|
|
92
|
-
200:
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
`;
|
|
97
|
-
expect(format(source)).toBe(source);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('keeps an operation doc comment inline when written inline', () => {
|
|
101
|
-
const source = `operation /pet: {
|
|
102
|
-
put: { # update an existing pet
|
|
103
|
-
response: {
|
|
104
|
-
200:
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
`;
|
|
109
|
-
expect(format(source)).toBe(source);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('round-trip — comments in the options block', () => {
|
|
114
|
-
it('keeps a header comment above the options keyword', () => {
|
|
115
|
-
const source = `# ContractKit contracts for billing.
|
|
116
|
-
# Owned by the payments team.
|
|
117
|
-
options {
|
|
118
|
-
keys: {
|
|
119
|
-
area: billing
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
contract Pet: {
|
|
124
|
-
id: uuid
|
|
125
|
-
}
|
|
126
|
-
`;
|
|
127
|
-
expect(format(source)).toBe(source);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('keeps a trailing comment on an options entry, braces and all', () => {
|
|
131
|
-
// The value used to swallow the comment and stop at the first `}`, which closed the
|
|
132
|
-
// block early and silently mis-parsed everything after it.
|
|
133
|
-
const source = `options {
|
|
134
|
-
keys: {
|
|
135
|
-
area: billing # interpolated elsewhere as {{area}}
|
|
136
|
-
}
|
|
137
|
-
services: {
|
|
138
|
-
PetService: "#modules/pet/pet.service.js"
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
contract Pet: {
|
|
143
|
-
id: uuid
|
|
144
|
-
}
|
|
145
|
-
`;
|
|
146
|
-
expect(format(source)).toBe(source);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('keeps an unquoted subpath value unquoted', () => {
|
|
150
|
-
// Both forms parse to the same string, so the parser records which one the author wrote;
|
|
151
|
-
// without that the formatter normalizes every value to the quoted form.
|
|
152
|
-
const source = `options {
|
|
153
|
-
services: {
|
|
154
|
-
PetService: #modules/pet/pet.service.js
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
contract Pet: {
|
|
159
|
-
id: uuid
|
|
160
|
-
}
|
|
161
|
-
`;
|
|
162
|
-
expect(format(source)).toBe(source);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('keeps a quoted value quoted', () => {
|
|
166
|
-
const source = `options {
|
|
167
|
-
services: {
|
|
168
|
-
PetService: "#modules/pet/pet.service.js"
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
contract Pet: {
|
|
173
|
-
id: uuid
|
|
174
|
-
}
|
|
175
|
-
`;
|
|
176
|
-
expect(format(source)).toBe(source);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('keeps both forms side by side, with their comments', () => {
|
|
180
|
-
const source = `options {
|
|
181
|
-
services: {
|
|
182
|
-
Bare: #modules/a/a.service.js # unquoted
|
|
183
|
-
Quoted: "#modules/b/b.service.js" # quoted
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
contract Pet: {
|
|
188
|
-
id: uuid
|
|
189
|
-
}
|
|
190
|
-
`;
|
|
191
|
-
expect(format(source)).toBe(source);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('keeps a comment above a sub-block', () => {
|
|
195
|
-
const source = `options {
|
|
196
|
-
# where these come from
|
|
197
|
-
keys: {
|
|
198
|
-
area: ledger
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
`;
|
|
202
|
-
expect(format(source)).toBe(source);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('keeps a comment run above the sub-block it precedes', () => {
|
|
206
|
-
const source = `options {
|
|
207
|
-
keys: {
|
|
208
|
-
area: ledger
|
|
209
|
-
}
|
|
210
|
-
# service wiring
|
|
211
|
-
# one per module
|
|
212
|
-
services: {
|
|
213
|
-
UserService: "#src/user.js"
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
`;
|
|
217
|
-
expect(format(source)).toBe(source);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('keeps a trailing comment before the closing brace', () => {
|
|
221
|
-
const source = `options {
|
|
222
|
-
keys: {
|
|
223
|
-
area: ledger
|
|
224
|
-
}
|
|
225
|
-
# nothing below
|
|
226
|
-
}
|
|
227
|
-
`;
|
|
228
|
-
expect(format(source)).toBe(source);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('keeps an options block that holds nothing but a comment', () => {
|
|
232
|
-
const source = `options {
|
|
233
|
-
# a note
|
|
234
|
-
}
|
|
235
|
-
`;
|
|
236
|
-
expect(format(source)).toBe(source);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
describe('round-trip — operation body key order', () => {
|
|
241
|
-
it('does not reorder keys into a canonical order', () => {
|
|
242
|
-
const source = `operation /pet: {
|
|
243
|
-
put: {
|
|
244
|
-
sdk: updatePet
|
|
245
|
-
service: PetService.update
|
|
246
|
-
response: {
|
|
247
|
-
200:
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
`;
|
|
252
|
-
expect(format(source)).toBe(source);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('preserves the opposite order just as faithfully', () => {
|
|
256
|
-
const source = `operation /pet: {
|
|
257
|
-
put: {
|
|
258
|
-
service: PetService.update
|
|
259
|
-
sdk: updatePet
|
|
260
|
-
response: {
|
|
261
|
-
200:
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
`;
|
|
266
|
-
expect(format(source)).toBe(source);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe('round-trip — layout', () => {
|
|
271
|
-
it('keeps blank lines between operations', () => {
|
|
272
|
-
const source = `operation /pet: {
|
|
273
|
-
get: {
|
|
274
|
-
response: {
|
|
275
|
-
200:
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
post: {
|
|
280
|
-
response: {
|
|
281
|
-
201:
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
`;
|
|
286
|
-
expect(format(source)).toBe(source);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('keeps operations packed when the source has no blank line', () => {
|
|
290
|
-
const source = `operation /pet: {
|
|
291
|
-
get: {
|
|
292
|
-
response: {
|
|
293
|
-
200:
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
post: {
|
|
297
|
-
response: {
|
|
298
|
-
201:
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
`;
|
|
303
|
-
expect(format(source)).toBe(source);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('keeps a single-line response body on one line', () => {
|
|
307
|
-
const source = `operation /pet: {
|
|
308
|
-
get: {
|
|
309
|
-
response: {
|
|
310
|
-
200: { application/json: Pet }
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
`;
|
|
315
|
-
expect(format(source)).toBe(source);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('keeps an expanded response body expanded', () => {
|
|
319
|
-
const source = `operation /pet: {
|
|
320
|
-
get: {
|
|
321
|
-
response: {
|
|
322
|
-
200: {
|
|
323
|
-
application/json: Pet
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
`;
|
|
329
|
-
expect(format(source)).toBe(source);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('keeps an empty status block, which means emitted with no body', () => {
|
|
333
|
-
const source = `operation /art/{id}: {
|
|
334
|
-
get: {
|
|
335
|
-
response: {
|
|
336
|
-
200: { application/json: Art }
|
|
337
|
-
304: {}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
`;
|
|
342
|
-
expect(format(source)).toBe(source);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('keeps a bare status bare', () => {
|
|
346
|
-
const source = `operation /art/{id}: {
|
|
347
|
-
get: {
|
|
348
|
-
response: {
|
|
349
|
-
200: { application/json: Art }
|
|
350
|
-
304:
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
`;
|
|
355
|
-
expect(format(source)).toBe(source);
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it('keeps the documented modifier on a status', () => {
|
|
359
|
-
const source = `operation /pet: {
|
|
360
|
-
get: {
|
|
361
|
-
response: {
|
|
362
|
-
200: { application/json: Pet }
|
|
363
|
-
404(documented): { application/json: Problem }
|
|
364
|
-
410(documented):
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
`;
|
|
369
|
-
expect(format(source)).toBe(source);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it('keeps every declared mime for a status, in source order', () => {
|
|
373
|
-
const source = `operation /art/{id}: {
|
|
374
|
-
get: {
|
|
375
|
-
response: {
|
|
376
|
-
200: {
|
|
377
|
-
image/png: binary
|
|
378
|
-
image/jpeg: binary
|
|
379
|
-
headers: {
|
|
380
|
-
etag?: string
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
`;
|
|
387
|
-
expect(format(source)).toBe(source);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it('keeps several mimes on one line when the source has them there', () => {
|
|
391
|
-
const source = `operation /art/{id}: {
|
|
392
|
-
get: {
|
|
393
|
-
response: {
|
|
394
|
-
200: { image/png: binary image/jpeg: binary }
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
`;
|
|
399
|
-
expect(format(source)).toBe(source);
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
// ─── Comments in the response block ──────────────────────────────────────────
|
|
404
|
-
|
|
405
|
-
describe('round-trip — comments in the response block', () => {
|
|
406
|
-
it('keeps a comment run above a status code', () => {
|
|
407
|
-
const source = `operation /art/{id}: {
|
|
408
|
-
get: {
|
|
409
|
-
response: {
|
|
410
|
-
# One declared mime per format, because the router pins ctx.type from this line
|
|
411
|
-
# and a service cannot vary it per request.
|
|
412
|
-
200: {
|
|
413
|
-
image/png: binary
|
|
414
|
-
}
|
|
415
|
-
# Produced by the conditional-GET middleware, not by the handler.
|
|
416
|
-
304:
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
`;
|
|
421
|
-
expect(format(source)).toBe(source);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it('keeps a comment above a mime line inside a status block', () => {
|
|
425
|
-
const source = `operation /art/{id}: {
|
|
426
|
-
get: {
|
|
427
|
-
response: {
|
|
428
|
-
200: {
|
|
429
|
-
# served straight from object storage
|
|
430
|
-
image/png: binary
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
`;
|
|
436
|
-
expect(format(source)).toBe(source);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it('keeps a comment above a headers block', () => {
|
|
440
|
-
const source = `operation /art/{id}: {
|
|
441
|
-
get: {
|
|
442
|
-
response: {
|
|
443
|
-
200: {
|
|
444
|
-
image/png: binary
|
|
445
|
-
# set by the CDN, echoed here so the SDK types it
|
|
446
|
-
headers: {
|
|
447
|
-
etag?: string
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
`;
|
|
454
|
-
expect(format(source)).toBe(source);
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
it('keeps trailing comments before either closing brace', () => {
|
|
458
|
-
const source = `operation /art/{id}: {
|
|
459
|
-
get: {
|
|
460
|
-
response: {
|
|
461
|
-
200: {
|
|
462
|
-
image/png: binary
|
|
463
|
-
# TODO: add image/avif once the encoder lands
|
|
464
|
-
}
|
|
465
|
-
# TODO: document the 429 the rate limiter returns
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
`;
|
|
470
|
-
expect(format(source)).toBe(source);
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// ─── Security blocks ─────────────────────────────────────────────────────────
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* A `security { }` block is where authors record *why* a policy floor sits where it does, so the
|
|
478
|
-
* prose in and around it is load-bearing. Every position here used to be deleted outright on
|
|
479
|
-
* format: the parser skipped standalone comments inside the block, dropped comment runs above a
|
|
480
|
-
* body key, and let a comment run above a verb lose to the verb's own inline doc comment.
|
|
481
|
-
*/
|
|
482
|
-
describe('round-trip — security block comments', () => {
|
|
483
|
-
it('keeps a comment above the policy line in the options block', () => {
|
|
484
|
-
const source = `options {
|
|
485
|
-
security: {
|
|
486
|
-
# The floor for every operation in this file.
|
|
487
|
-
# Reads override it downward at their own verb.
|
|
488
|
-
policy: platform.manage
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
`;
|
|
492
|
-
expect(format(source)).toBe(source);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it('keeps a comment after the policy line, before the closing brace', () => {
|
|
496
|
-
const source = `options {
|
|
497
|
-
security: {
|
|
498
|
-
policy: platform.manage
|
|
499
|
-
# Object-scoped permissions cannot be named here yet.
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
`;
|
|
503
|
-
expect(format(source)).toBe(source);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
it('distinguishes an inline policy comment from one on the next line', () => {
|
|
507
|
-
const source = `options {
|
|
508
|
-
security: {
|
|
509
|
-
# Why this floor
|
|
510
|
-
policy: platform.manage # the operator gate
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
`;
|
|
514
|
-
expect(format(source)).toBe(source);
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
it('keeps a comment above a security block inside an operation body', () => {
|
|
518
|
-
const source = `operation /plugins: {
|
|
519
|
-
get: {
|
|
520
|
-
name: List plugins
|
|
521
|
-
# A read, so it drops to the view floor.
|
|
522
|
-
security: {
|
|
523
|
-
policy: platform.view
|
|
524
|
-
}
|
|
525
|
-
response: {
|
|
526
|
-
200:
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
`;
|
|
531
|
-
expect(format(source)).toBe(source);
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
it('keeps a comment above a security: none line', () => {
|
|
535
|
-
const source = `operation /plugins/callback: {
|
|
536
|
-
get: {
|
|
537
|
-
name: OAuth callback
|
|
538
|
-
# The provider redirects a browser here, so it cannot carry a session.
|
|
539
|
-
security: none
|
|
540
|
-
response: {
|
|
541
|
-
200:
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
`;
|
|
546
|
-
expect(format(source)).toBe(source);
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
it('keeps a comment run above a verb that also has an inline doc comment', () => {
|
|
550
|
-
const source = `operation /plugins/{id}/logs: {
|
|
551
|
-
params: {
|
|
552
|
-
id: uuid
|
|
553
|
-
}
|
|
554
|
-
# Log output is whatever the plugin chose to write, including upstream error
|
|
555
|
-
# bodies. Treat it as operator-only until the log store can promise otherwise.
|
|
556
|
-
get: { # Returns the plugin's buffered log lines
|
|
557
|
-
name: Get plugin logs
|
|
558
|
-
response: {
|
|
559
|
-
200:
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
`;
|
|
564
|
-
expect(format(source)).toBe(source);
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
it('keeps a comment left after the last key in an operation body', () => {
|
|
568
|
-
const source = `operation /plugins: {
|
|
569
|
-
get: {
|
|
570
|
-
name: List plugins
|
|
571
|
-
response: {
|
|
572
|
-
200:
|
|
573
|
-
}
|
|
574
|
-
# TODO: gate this behind the object-scoped permission once it exists
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
`;
|
|
578
|
-
expect(format(source)).toBe(source);
|
|
579
|
-
});
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
// ─── Declaration-boundary comments ───────────────────────────────────────────
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* A comment between two top-level declarations is ambiguous: it can be an inline comment trailing
|
|
586
|
-
* the one above, a doc comment for the one below, or a standalone block. A declaration could not
|
|
587
|
-
* see what preceded it, so `ModelBody`'s optional trailing comment claimed the *next* line's
|
|
588
|
-
* comment — deleting a doc comment, or silently re-filing it as the previous contract's
|
|
589
|
-
* description. `Root` places these now, by comparing source lines.
|
|
590
|
-
*/
|
|
591
|
-
describe('round-trip — comments between declarations', () => {
|
|
592
|
-
it('keeps a doc comment on a contract that follows a one-line alias contract', () => {
|
|
593
|
-
const source = `contract ConfigFieldType: enum(string, url, secret)
|
|
594
|
-
|
|
595
|
-
# One choice of a \`select\` config field
|
|
596
|
-
contract ConfigFieldOption: {
|
|
597
|
-
value: string
|
|
598
|
-
}
|
|
599
|
-
`;
|
|
600
|
-
expect(format(source)).toBe(source);
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it('keeps an inline comment on an alias contract where it was written', () => {
|
|
604
|
-
const source = `contract PluginStatus: enum(discovered, active) # Lifecycle state
|
|
605
|
-
|
|
606
|
-
contract ConfigFieldOption: {
|
|
607
|
-
value: string
|
|
608
|
-
}
|
|
609
|
-
`;
|
|
610
|
-
expect(format(source)).toBe(source);
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
it('tells an inline comment apart from the doc comment below it', () => {
|
|
614
|
-
const source = `contract PluginStatus: enum(discovered, active) # Lifecycle state
|
|
615
|
-
|
|
616
|
-
# One choice of a \`select\` config field
|
|
617
|
-
contract ConfigFieldOption: {
|
|
618
|
-
value: string
|
|
619
|
-
}
|
|
620
|
-
`;
|
|
621
|
-
expect(format(source)).toBe(source);
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
it('keeps an inline comment on the last declaration in the file', () => {
|
|
625
|
-
const source = `contract PluginStatus: enum(discovered, active) # Lifecycle state
|
|
626
|
-
`;
|
|
627
|
-
expect(format(source)).toBe(source);
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
it('keeps a comment after the last declaration', () => {
|
|
631
|
-
const source = `contract PluginStatus: enum(discovered, active)
|
|
632
|
-
|
|
633
|
-
# TODO: add the archived state once the store supports it
|
|
634
|
-
`;
|
|
635
|
-
expect(format(source)).toBe(source);
|
|
636
|
-
});
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
// ─── Field comments ──────────────────────────────────────────────────────────
|
|
640
|
-
|
|
641
|
-
describe('round-trip — field comments', () => {
|
|
642
|
-
it('keeps a comment on a nested inline object opening brace', () => {
|
|
643
|
-
const source = `contract FidoFactorAttestation: {
|
|
644
|
-
rp: { # The relying party
|
|
645
|
-
name: string # The relying party name
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
`;
|
|
649
|
-
expect(format(source)).toBe(source);
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
it('keeps a nested brace comment that repeats the contract header wording', () => {
|
|
653
|
-
// The header comment is reclaimed from the field list by matching text; when a nested
|
|
654
|
-
// field carried the same wording, the reclaim deleted the wrong one.
|
|
655
|
-
const source = `contract FidoFactorAttestation: { # The FIDO factor attestation information
|
|
656
|
-
rp: { # The FIDO factor attestation information
|
|
657
|
-
name: string # The relying party name
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
`;
|
|
661
|
-
expect(format(source)).toBe(source);
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
it('keeps a trailing comment on a field whose enum type wraps', () => {
|
|
665
|
-
const source = `contract LineupEntry: {
|
|
666
|
-
segmentState?: enum(
|
|
667
|
-
planned,
|
|
668
|
-
rendering,
|
|
669
|
-
ready,
|
|
670
|
-
failed,
|
|
671
|
-
gone
|
|
672
|
-
) # How far along the segment is
|
|
673
|
-
}
|
|
674
|
-
`;
|
|
675
|
-
expect(format(source)).toBe(source);
|
|
676
|
-
});
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
// ─── Idempotence ─────────────────────────────────────────────────────────────
|
|
680
|
-
|
|
681
|
-
describe('round-trip — idempotence', () => {
|
|
682
|
-
const sources = [
|
|
683
|
-
...ckFiles.map(name => readFileSync(join(CONTRACTS_DIR, name), 'utf8')),
|
|
684
|
-
// Non-canonical spacing: formatting once must reach a fixed point.
|
|
685
|
-
`operation /pet: {\n get: {\n response: {\n 200:\n }\n }\n}\n`,
|
|
686
|
-
];
|
|
687
|
-
|
|
688
|
-
for (const [i, source] of sources.entries()) {
|
|
689
|
-
it(`formatting is a fixed point for source #${i}`, () => {
|
|
690
|
-
const once = format(source);
|
|
691
|
-
expect(format(once)).toBe(once);
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
});
|