@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/src/print-contract.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import type { ModelNode } from '@contractkit/core';
|
|
2
|
-
import { printField, printInlineObjectExpanded, extractTrailingInlineObject, printType, printEnumExpanded } from './print-type.js';
|
|
3
|
-
import { INDENT } from './indent.js';
|
|
4
|
-
|
|
5
|
-
// ─── Model declaration ───────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Render a `contract` body from its `Name: ...` onward (the `contract ` keyword and any
|
|
9
|
-
* doc comment written above the declaration are prepended by the caller). Handles both the
|
|
10
|
-
* type-alias form (`Name: <type>`) and the regular field-block form, including modifiers,
|
|
11
|
-
* base chain, and per-field printing. Any `model.trailingComments` (comments after the last
|
|
12
|
-
* field, before `}`) are emitted as indented `# text` lines so they round-trip.
|
|
13
|
-
*
|
|
14
|
-
* `model.description` is emitted here only when `model.descriptionInline` says the author wrote
|
|
15
|
-
* it on the header line; otherwise it belongs above the declaration and the caller emits it.
|
|
16
|
-
*/
|
|
17
|
-
export function printModelDecl(model: ModelNode, printWidth: number = 80): string {
|
|
18
|
-
// Type alias form: Name : typeExpression
|
|
19
|
-
if (model.type !== undefined) {
|
|
20
|
-
return printTypeAlias(model, printWidth);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Regular model with fields (possibly inherited)
|
|
24
|
-
// A doc comment written above the declaration is re-emitted there by the caller; only an
|
|
25
|
-
// inline one belongs on the header line.
|
|
26
|
-
const commentSuffix = model.description && model.descriptionInline ? ` # ${model.description}` : '';
|
|
27
|
-
const modifiers = [
|
|
28
|
-
model.deprecated ? 'deprecated' : '',
|
|
29
|
-
model.inputCase || model.outputCase
|
|
30
|
-
? `format(${[model.inputCase ? `input=${model.inputCase}` : '', model.outputCase ? `output=${model.outputCase}` : ''].filter(Boolean).join(', ')})`
|
|
31
|
-
: '',
|
|
32
|
-
model.mode ? `mode(${model.mode})` : '',
|
|
33
|
-
]
|
|
34
|
-
.filter(Boolean)
|
|
35
|
-
.join(' ');
|
|
36
|
-
const modePrefix = modifiers ? `${modifiers} ` : '';
|
|
37
|
-
const baseChain = model.bases && model.bases.length > 0 ? `${model.bases.join(' & ')} & ` : '';
|
|
38
|
-
const header = `${modePrefix}${model.name}: ${baseChain}{${commentSuffix}`;
|
|
39
|
-
|
|
40
|
-
const lines: string[] = [header];
|
|
41
|
-
for (const field of model.fields) {
|
|
42
|
-
lines.push(printField(field, INDENT, printWidth));
|
|
43
|
-
}
|
|
44
|
-
for (const comment of model.trailingComments ?? []) {
|
|
45
|
-
lines.push(`${INDENT}# ${comment}`);
|
|
46
|
-
}
|
|
47
|
-
lines.push('}');
|
|
48
|
-
return lines.join('\n');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function printTypeAlias(model: ModelNode, printWidth: number): string {
|
|
52
|
-
const type = model.type!;
|
|
53
|
-
// A doc comment written above the declaration is re-emitted there by the caller; only an
|
|
54
|
-
// inline one belongs on the header line.
|
|
55
|
-
const commentSuffix = model.description && model.descriptionInline ? ` # ${model.description}` : '';
|
|
56
|
-
const modifiers = [
|
|
57
|
-
model.deprecated ? 'deprecated' : '',
|
|
58
|
-
model.inputCase || model.outputCase
|
|
59
|
-
? `format(${[model.inputCase ? `input=${model.inputCase}` : '', model.outputCase ? `output=${model.outputCase}` : ''].filter(Boolean).join(', ')})`
|
|
60
|
-
: '',
|
|
61
|
-
model.mode ? `mode(${model.mode})` : '',
|
|
62
|
-
]
|
|
63
|
-
.filter(Boolean)
|
|
64
|
-
.join(' ');
|
|
65
|
-
const modePrefix = modifiers ? `${modifiers} ` : '';
|
|
66
|
-
|
|
67
|
-
// If the type ends with an inline brace object, expand it as a pseudo-model block.
|
|
68
|
-
const trailing = extractTrailingInlineObject(type);
|
|
69
|
-
if (trailing) {
|
|
70
|
-
const { prefix, inlineObj } = trailing;
|
|
71
|
-
const modePart = inlineObj.mode ? `mode(${inlineObj.mode}) ` : '';
|
|
72
|
-
const header = prefix
|
|
73
|
-
? `${modePrefix}${model.name}: ${prefix} & ${modePart}{${commentSuffix}`
|
|
74
|
-
: `${modePrefix}${model.name}: ${modePart}{${commentSuffix}`;
|
|
75
|
-
const lines: string[] = [header, ...printInlineObjectExpanded(inlineObj, INDENT, printWidth), '}'];
|
|
76
|
-
return lines.join('\n');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Simple type alias — single line, unless it's a long enum.
|
|
80
|
-
// Note: the contract prefix "contract " (9 chars) is prepended by the caller.
|
|
81
|
-
const singleLine = `${modePrefix}${model.name}: ${printType(type)}${commentSuffix}`;
|
|
82
|
-
if (type.kind === 'enum' && 'contract '.length + singleLine.length > printWidth) {
|
|
83
|
-
return `${modePrefix}${model.name}: ${printEnumExpanded(type.values, '')}${commentSuffix}`;
|
|
84
|
-
}
|
|
85
|
-
return singleLine;
|
|
86
|
-
}
|
package/src/print-operation.ts
DELETED
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
OpRouteNode,
|
|
3
|
-
OpOperationNode,
|
|
4
|
-
OpResponseNode,
|
|
5
|
-
ParamSource,
|
|
6
|
-
SecurityNode,
|
|
7
|
-
SecurityFields,
|
|
8
|
-
ContractTypeNode,
|
|
9
|
-
ObjectMode,
|
|
10
|
-
PluginValue,
|
|
11
|
-
McpConfigNode,
|
|
12
|
-
OpBodyKey,
|
|
13
|
-
} from '@contractkit/core';
|
|
14
|
-
import { SECURITY_NONE } from '@contractkit/core';
|
|
15
|
-
import { printType, formatDefault } from './print-type.js';
|
|
16
|
-
import { INDENT } from './indent.js';
|
|
17
|
-
|
|
18
|
-
const I1 = INDENT;
|
|
19
|
-
const I2 = INDENT.repeat(2);
|
|
20
|
-
const I3 = INDENT.repeat(3);
|
|
21
|
-
const I4 = INDENT.repeat(4);
|
|
22
|
-
|
|
23
|
-
// ─── Orphan comment helpers ──────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
type CommentEntry = { line: number; text: string };
|
|
26
|
-
/** A run of consecutive-line orphan comments, keyed by the source line it starts on. */
|
|
27
|
-
export type CommentBlock = { startLine: number; lines: string[] };
|
|
28
|
-
|
|
29
|
-
/** Group sorted orphan comment entries into consecutive-line blocks. */
|
|
30
|
-
export function groupComments(entries: CommentEntry[]): CommentBlock[] {
|
|
31
|
-
const blocks: CommentBlock[] = [];
|
|
32
|
-
let current: CommentBlock | null = null;
|
|
33
|
-
for (const { line, text } of entries) {
|
|
34
|
-
if (current && line === current.startLine + current.lines.length) {
|
|
35
|
-
current.lines.push(text);
|
|
36
|
-
} else {
|
|
37
|
-
if (current) blocks.push(current);
|
|
38
|
-
current = { startLine: line, lines: [text] };
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (current) blocks.push(current);
|
|
42
|
-
return blocks;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Emit any comment blocks whose startLine is < beforeLine.
|
|
47
|
-
* Lines are emitted verbatim — they already carry their original indentation.
|
|
48
|
-
*/
|
|
49
|
-
export function flushBlocks(out: string[], blocks: CommentBlock[], idx: { value: number }, beforeLine: number, _indent = '') {
|
|
50
|
-
while (idx.value < blocks.length && blocks[idx.value]!.startLine < beforeLine) {
|
|
51
|
-
for (const l of blocks[idx.value]!.lines) out.push(l);
|
|
52
|
-
idx.value++;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ─── Route ───────────────────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Render an `operation` route body from its `path: {` onward (the `operation` keyword, any
|
|
60
|
-
* modifier, and the route's leading comments are prepended by the caller). Emits the
|
|
61
|
-
* params/security blocks and each HTTP operation, interleaving orphan comment `blocks` at their
|
|
62
|
-
* original source positions — `idx` tracks how far through `blocks` we've consumed, and
|
|
63
|
-
* `nextRouteStart` bounds the flush to comments before the following route. Any
|
|
64
|
-
* `route.trailingComments` (comments after the last operation, before `}`) are emitted before
|
|
65
|
-
* the closing brace so they round-trip.
|
|
66
|
-
*
|
|
67
|
-
* Blank lines between operations come from each operation's `blankLineBefore`, so the author's
|
|
68
|
-
* spacing survives rather than being normalized to one rule or the other.
|
|
69
|
-
*/
|
|
70
|
-
export function printRoute(route: OpRouteNode, blocks: CommentBlock[], idx: { value: number }, nextRouteStart: number): string {
|
|
71
|
-
const lines: string[] = [];
|
|
72
|
-
lines.push(`${route.path}: {`);
|
|
73
|
-
|
|
74
|
-
if (route.params !== undefined) {
|
|
75
|
-
lines.push(...printParamsBlock(route.params, I1, route.paramsMode));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (route.security !== undefined) {
|
|
79
|
-
lines.push(...printSecurity(route.security, I1, I2));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
for (const op of route.operations) {
|
|
83
|
-
// Reproduce the author's spacing rather than imposing our own.
|
|
84
|
-
if (op.blankLineBefore && lines.length > 1) lines.push('');
|
|
85
|
-
// Flush comment blocks that appear before this operation (inside the route)
|
|
86
|
-
flushBlocks(lines, blocks, idx, op.loc.line, I1);
|
|
87
|
-
lines.push(...printOperation(op));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Flush comment blocks between last operation and the next route
|
|
91
|
-
flushBlocks(lines, blocks, idx, nextRouteStart, I1);
|
|
92
|
-
|
|
93
|
-
// Trailing/orphan comments after the last operation, before the closing brace.
|
|
94
|
-
for (const comment of route.trailingComments ?? []) {
|
|
95
|
-
lines.push(`${I1}# ${comment}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
lines.push('}');
|
|
99
|
-
return lines.join('\n');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ─── Params block ────────────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
function printParamsBlock(source: ParamSource, indent: string, mode?: ObjectMode): string[] {
|
|
105
|
-
const prefix = mode ? `mode(${mode}) ` : '';
|
|
106
|
-
if (source.kind === 'ref') {
|
|
107
|
-
return [`${indent}${prefix}params: ${source.name}`];
|
|
108
|
-
}
|
|
109
|
-
if (source.kind === 'params') {
|
|
110
|
-
const lines: string[] = [`${indent}${prefix}params: {`];
|
|
111
|
-
const inner = indent + INDENT;
|
|
112
|
-
for (const p of source.nodes) {
|
|
113
|
-
const opt = p.optional ? '?' : '';
|
|
114
|
-
let t = printType(p.type);
|
|
115
|
-
if (p.nullable) t += ' | null';
|
|
116
|
-
const def = p.default !== undefined ? ` = ${formatDefault(p.default)}` : '';
|
|
117
|
-
const comment = p.description ? ` # ${p.description}` : '';
|
|
118
|
-
lines.push(`${inner}${p.name}${opt}: ${t}${def}${comment}`);
|
|
119
|
-
}
|
|
120
|
-
lines.push(`${indent}}`);
|
|
121
|
-
return lines;
|
|
122
|
-
}
|
|
123
|
-
// ContractTypeNode
|
|
124
|
-
return [`${indent}${prefix}params: ${printType(source.node)}`];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ─── HTTP operation ──────────────────────────────────────────────────────────
|
|
128
|
-
|
|
129
|
-
/** Order the body keys are emitted in when the node carries no source order (built programmatically). */
|
|
130
|
-
const CANONICAL_KEY_ORDER: OpBodyKey[] = ['name', 'service', 'sdk', 'mcp', 'signature', 'security', 'plugins', 'query', 'headers', 'request', 'responses'];
|
|
131
|
-
|
|
132
|
-
/** Render a single operation-body key. Returns `[]` when the operation doesn't carry that key. */
|
|
133
|
-
function printOperationKey(op: OpOperationNode, key: OpBodyKey): string[] {
|
|
134
|
-
switch (key) {
|
|
135
|
-
case 'name':
|
|
136
|
-
return op.name ? [`${I2}name: ${op.name}`] : [];
|
|
137
|
-
case 'service':
|
|
138
|
-
return op.service ? [`${I2}service: ${op.service}`] : [];
|
|
139
|
-
case 'sdk':
|
|
140
|
-
return op.sdk ? [`${I2}sdk: ${op.sdk}`] : [];
|
|
141
|
-
case 'mcp':
|
|
142
|
-
if (op.mcp === true) return [`${I2}mcp: true`];
|
|
143
|
-
if (op.mcp === false) return [`${I2}mcp: false`];
|
|
144
|
-
return op.mcp ? printMcpBlock(op.mcp) : [];
|
|
145
|
-
case 'signature': {
|
|
146
|
-
if (!op.signature) return [];
|
|
147
|
-
const comment = op.signatureDescription ? ` # ${op.signatureDescription}` : '';
|
|
148
|
-
if (op.signaturePolicy) {
|
|
149
|
-
return [
|
|
150
|
-
`${I2}signature: {`,
|
|
151
|
-
`${I3}options: ${formatSignatureValue(op.signature)}${comment}`,
|
|
152
|
-
`${I3}policy: ${op.signaturePolicy}`,
|
|
153
|
-
`${I2}}`,
|
|
154
|
-
];
|
|
155
|
-
}
|
|
156
|
-
return [`${I2}signature: ${formatSignatureValue(op.signature)}${comment}`];
|
|
157
|
-
}
|
|
158
|
-
case 'security':
|
|
159
|
-
return op.security !== undefined ? printSecurity(op.security) : [];
|
|
160
|
-
case 'plugins': {
|
|
161
|
-
if (!op.plugins || Object.keys(op.plugins).length === 0) return [];
|
|
162
|
-
const lines = [`${I2}plugins: {`];
|
|
163
|
-
for (const [k, val] of Object.entries(op.plugins)) lines.push(...printPluginEntry(k, val, I3));
|
|
164
|
-
lines.push(`${I2}}`);
|
|
165
|
-
return lines;
|
|
166
|
-
}
|
|
167
|
-
case 'query':
|
|
168
|
-
return op.query !== undefined ? printQueryOrHeaders('query', op.query, op.queryMode) : [];
|
|
169
|
-
case 'headers':
|
|
170
|
-
if (op.requestHeadersOptOut) return [`${I2}headers: none`];
|
|
171
|
-
return op.headers !== undefined ? printQueryOrHeaders('headers', op.headers, op.headersMode) : [];
|
|
172
|
-
case 'request': {
|
|
173
|
-
if (!op.request) return [];
|
|
174
|
-
const lines = [`${I2}request: {`];
|
|
175
|
-
for (const body of op.request.bodies) lines.push(...printContentTypeLine(body.contentType, body.bodyType, I3));
|
|
176
|
-
lines.push(`${I2}}`);
|
|
177
|
-
return lines;
|
|
178
|
-
}
|
|
179
|
-
case 'responses':
|
|
180
|
-
return op.responses.length > 0 ? printResponseBlock(op.responses, op.responsesTrailingComments) : [];
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function printOperation(op: OpOperationNode): string[] {
|
|
185
|
-
const lines: string[] = [];
|
|
186
|
-
const modPart = op.modifiers?.length ? `(${op.modifiers[0]})` : '';
|
|
187
|
-
|
|
188
|
-
// A doc comment written above the method line goes back above it; only an inline one is
|
|
189
|
-
// re-emitted as a trailing `#` on the header. Nodes built programmatically carry no placement,
|
|
190
|
-
// and default to inline — the form most `.ck` sources use and one that round-trips as written.
|
|
191
|
-
const inlineDescription = op.descriptionInline ?? true;
|
|
192
|
-
// Standalone prose above the verb, kept apart from the doc comment when the verb carries an
|
|
193
|
-
// inline one. Emitted first so it stays above the line it was written above.
|
|
194
|
-
for (const c of op.leadingComments ?? []) lines.push(`${I1}# ${c}`);
|
|
195
|
-
if (op.description && !inlineDescription) {
|
|
196
|
-
for (const line of op.description.split('\n')) lines.push(`${I1}# ${line}`);
|
|
197
|
-
}
|
|
198
|
-
const commentSuffix = op.description && inlineDescription ? ` # ${op.description}` : '';
|
|
199
|
-
lines.push(`${I1}${op.method}${modPart}: {${commentSuffix}`);
|
|
200
|
-
|
|
201
|
-
// Emit in source order when the parser recorded it, so formatting never reorders a user's keys.
|
|
202
|
-
// Any key the source order doesn't mention (e.g. added by a later AST pass) follows in canonical order.
|
|
203
|
-
const order = op.keyOrder ?? [];
|
|
204
|
-
const rest = CANONICAL_KEY_ORDER.filter(k => !order.includes(k));
|
|
205
|
-
// A comment run whose key turned out to print nothing has nowhere to sit; rather than drop it,
|
|
206
|
-
// it falls through to the trailing run at the end of the body.
|
|
207
|
-
const orphaned: string[] = [];
|
|
208
|
-
for (const key of [...order, ...rest]) {
|
|
209
|
-
const keyLines = printOperationKey(op, key);
|
|
210
|
-
const comments = op.bodyLeadingComments?.[key] ?? [];
|
|
211
|
-
if (keyLines.length === 0) {
|
|
212
|
-
orphaned.push(...comments);
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
for (const c of comments) lines.push(`${I2}# ${c}`);
|
|
216
|
-
lines.push(...keyLines);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
for (const c of [...orphaned, ...(op.bodyTrailingComments ?? [])]) lines.push(`${I2}# ${c}`);
|
|
220
|
-
|
|
221
|
-
lines.push(`${I1}}`);
|
|
222
|
-
return lines;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ─── MCP block ───────────────────────────────────────────────────────────────
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Reconstruct the `hint:` token list from the four annotation booleans, in canonical
|
|
229
|
-
* order. Each set boolean contributes its positive or negative token; unset hints are omitted.
|
|
230
|
-
*/
|
|
231
|
-
function mcpHintTokens(mcp: McpConfigNode): string[] {
|
|
232
|
-
const tokens: string[] = [];
|
|
233
|
-
if (mcp.readOnlyHint !== undefined) tokens.push(mcp.readOnlyHint ? 'readOnly' : 'nonReadOnly');
|
|
234
|
-
if (mcp.idempotentHint !== undefined) tokens.push(mcp.idempotentHint ? 'idempotent' : 'nonIdempotent');
|
|
235
|
-
if (mcp.destructiveHint !== undefined) tokens.push(mcp.destructiveHint ? 'destructive' : 'nonDestructive');
|
|
236
|
-
if (mcp.openWorldHint !== undefined) tokens.push(mcp.openWorldHint ? 'openWorld' : 'closedWorld');
|
|
237
|
-
return tokens;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** Print an `mcp: { ... }` settings block. Fields are emitted in canonical order; `hint:` is omitted when no hints are set. */
|
|
241
|
-
function printMcpBlock(mcp: McpConfigNode): string[] {
|
|
242
|
-
const lines: string[] = [`${I2}mcp: {`];
|
|
243
|
-
if (mcp.name !== undefined) lines.push(`${I3}name: "${escapeString(mcp.name)}"`);
|
|
244
|
-
if (mcp.title !== undefined) lines.push(`${I3}title: "${escapeString(mcp.title)}"`);
|
|
245
|
-
if (mcp.description !== undefined) lines.push(`${I3}description: "${escapeString(mcp.description)}"`);
|
|
246
|
-
const tokens = mcpHintTokens(mcp);
|
|
247
|
-
if (tokens.length > 0) lines.push(`${I3}hint: ${tokens.join(', ')}`);
|
|
248
|
-
lines.push(`${I2}}`);
|
|
249
|
-
return lines;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ─── Plugins block ───────────────────────────────────────────────────────────
|
|
253
|
-
|
|
254
|
-
const IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
255
|
-
|
|
256
|
-
function escapeString(s: string): string {
|
|
257
|
-
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function printPluginEntry(key: string, value: PluginValue, indent: string): string[] {
|
|
261
|
-
const lines: string[] = [];
|
|
262
|
-
const inline = printPluginInline(value);
|
|
263
|
-
if (inline !== null) {
|
|
264
|
-
lines.push(`${indent}${key}: ${inline}`);
|
|
265
|
-
} else {
|
|
266
|
-
const head = `${indent}${key}: `;
|
|
267
|
-
const block = printPluginBlock(value, indent);
|
|
268
|
-
lines.push(`${head}${block[0]!.trimStart()}`);
|
|
269
|
-
for (let i = 1; i < block.length; i++) lines.push(block[i]!);
|
|
270
|
-
}
|
|
271
|
-
return lines;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function printPluginInline(value: PluginValue): string | null {
|
|
275
|
-
if (typeof value === 'string') return `"${escapeString(value)}"`;
|
|
276
|
-
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
277
|
-
if (value === null) return 'null';
|
|
278
|
-
if (Array.isArray(value) && value.length === 0) return '[]';
|
|
279
|
-
if (!Array.isArray(value) && typeof value === 'object' && Object.keys(value).length === 0) return '{}';
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function printPluginBlock(value: PluginValue, indent: string): string[] {
|
|
284
|
-
const inner = indent + INDENT;
|
|
285
|
-
const lines: string[] = [];
|
|
286
|
-
if (Array.isArray(value)) {
|
|
287
|
-
lines.push(`${indent}[`);
|
|
288
|
-
for (const item of value) {
|
|
289
|
-
const inline = printPluginInline(item);
|
|
290
|
-
if (inline !== null) {
|
|
291
|
-
lines.push(`${inner}${inline}`);
|
|
292
|
-
} else {
|
|
293
|
-
const block = printPluginBlock(item, inner);
|
|
294
|
-
for (const l of block) lines.push(l);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
lines.push(`${indent}]`);
|
|
298
|
-
return lines;
|
|
299
|
-
}
|
|
300
|
-
if (typeof value === 'object' && value !== null) {
|
|
301
|
-
lines.push(`${indent}{`);
|
|
302
|
-
for (const [k, v] of Object.entries(value)) {
|
|
303
|
-
const fieldKey = IDENT_RE.test(k) ? k : `"${escapeString(k)}"`;
|
|
304
|
-
const inline = printPluginInline(v);
|
|
305
|
-
if (inline !== null) {
|
|
306
|
-
lines.push(`${inner}${fieldKey}: ${inline}`);
|
|
307
|
-
} else {
|
|
308
|
-
const block = printPluginBlock(v, inner);
|
|
309
|
-
lines.push(`${inner}${fieldKey}: ${block[0]!.trimStart()}`);
|
|
310
|
-
for (let i = 1; i < block.length; i++) lines.push(block[i]!);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
lines.push(`${indent}}`);
|
|
314
|
-
return lines;
|
|
315
|
-
}
|
|
316
|
-
// Scalars are always inline; printPluginInline already handles them.
|
|
317
|
-
return [`${indent}${printPluginInline(value)}`];
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// ─── Security ────────────────────────────────────────────────────────────────
|
|
321
|
-
|
|
322
|
-
/** Print a signature key: unquoted when it's a plain identifier, quoted otherwise. */
|
|
323
|
-
function formatSignatureValue(value: string): string {
|
|
324
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value) ? value : `"${value}"`;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Print a `security:` declaration. Returns `["${indent}security: none"]` for the public-endpoint
|
|
329
|
-
* sentinel, a multi-line block when `policy` is set or the block carries comments, or an empty
|
|
330
|
-
* array when there is nothing meaningful to emit.
|
|
331
|
-
*
|
|
332
|
-
* A block holding only comments still prints: the rationale for a policy floor is the reason
|
|
333
|
-
* authors write in here, and collapsing the braces away would delete it.
|
|
334
|
-
*
|
|
335
|
-
* @param indent indentation for the `security` keyword line
|
|
336
|
-
* @param innerIndent indentation for field lines inside the block
|
|
337
|
-
*/
|
|
338
|
-
export function printSecurity(security: SecurityNode, indent = I2, innerIndent = I3): string[] {
|
|
339
|
-
if (security === SECURITY_NONE) return [`${indent}security: none`];
|
|
340
|
-
const fields = security as SecurityFields;
|
|
341
|
-
const leading = fields.leadingComments ?? [];
|
|
342
|
-
const trailing = fields.trailingComments ?? [];
|
|
343
|
-
// A block with no policy still has to be emitted when it carries comments, or the author's
|
|
344
|
-
// rationale disappears along with the empty braces.
|
|
345
|
-
if (fields.policy === undefined && leading.length === 0 && trailing.length === 0) return [];
|
|
346
|
-
const lines = [`${indent}security: {`];
|
|
347
|
-
for (const c of leading) lines.push(`${innerIndent}# ${c}`);
|
|
348
|
-
if (fields.policy !== undefined) {
|
|
349
|
-
const comment = fields.policyDescription ? ` # ${fields.policyDescription}` : '';
|
|
350
|
-
const value = fields.policy === false ? 'none' : fields.policy;
|
|
351
|
-
lines.push(`${innerIndent}policy: ${value}${comment}`);
|
|
352
|
-
}
|
|
353
|
-
for (const c of trailing) lines.push(`${innerIndent}# ${c}`);
|
|
354
|
-
lines.push(`${indent}}`);
|
|
355
|
-
return lines;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// ─── Query / headers ─────────────────────────────────────────────────────────
|
|
359
|
-
|
|
360
|
-
function printQueryOrHeaders(keyword: 'query' | 'headers', source: ParamSource, mode?: ObjectMode): string[] {
|
|
361
|
-
const prefix = mode ? `mode(${mode}) ` : '';
|
|
362
|
-
if (source.kind === 'ref') {
|
|
363
|
-
return [`${I2}${prefix}${keyword}: ${source.name}`];
|
|
364
|
-
}
|
|
365
|
-
if (source.kind === 'params') {
|
|
366
|
-
if (source.nodes.length === 0) return [];
|
|
367
|
-
const lines: string[] = [`${I2}${prefix}${keyword}: {`];
|
|
368
|
-
for (const p of source.nodes) {
|
|
369
|
-
const opt = p.optional ? '?' : '';
|
|
370
|
-
let t = printType(p.type);
|
|
371
|
-
if (p.nullable) t += ' | null';
|
|
372
|
-
const def = p.default !== undefined ? ` = ${formatDefault(p.default)}` : '';
|
|
373
|
-
const comment = p.description ? ` # ${p.description}` : '';
|
|
374
|
-
lines.push(`${I3}${p.name}${opt}: ${t}${def}${comment}`);
|
|
375
|
-
}
|
|
376
|
-
lines.push(`${I2}}`);
|
|
377
|
-
return lines;
|
|
378
|
-
}
|
|
379
|
-
// ContractTypeNode (e.g. intersection)
|
|
380
|
-
return [`${I2}${prefix}${keyword}: ${printType(source.node)}`];
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// ─── Content-type line ───────────────────────────────────────────────────────
|
|
384
|
-
|
|
385
|
-
/** Print a `contentType: bodyType` line, expanding inline brace objects onto separate lines. */
|
|
386
|
-
function printContentTypeLine(contentType: string, bodyType: ContractTypeNode, lineIndent: string): string[] {
|
|
387
|
-
if (bodyType.kind === 'inlineObject') {
|
|
388
|
-
const fieldIndent = lineIndent + INDENT;
|
|
389
|
-
const lines: string[] = [`${lineIndent}${contentType}: {`];
|
|
390
|
-
for (const f of bodyType.fields) {
|
|
391
|
-
const opt = f.optional ? '?' : '';
|
|
392
|
-
let t = printType(f.type);
|
|
393
|
-
if (f.nullable) t += ' | null';
|
|
394
|
-
const def = f.default !== undefined ? ` = ${formatDefault(f.default)}` : '';
|
|
395
|
-
const comment = f.description ? ` # ${f.description}` : '';
|
|
396
|
-
lines.push(`${fieldIndent}${f.name}${opt}: ${t}${def}${comment}`);
|
|
397
|
-
}
|
|
398
|
-
lines.push(`${lineIndent}}`);
|
|
399
|
-
return lines;
|
|
400
|
-
}
|
|
401
|
-
return [`${lineIndent}${contentType}: ${printType(bodyType)}`];
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// ─── Response block ──────────────────────────────────────────────────────────
|
|
405
|
-
|
|
406
|
-
function printResponseBlock(responses: OpResponseNode[], trailingComments?: string[]): string[] {
|
|
407
|
-
const lines: string[] = [`${I2}response: {`];
|
|
408
|
-
|
|
409
|
-
for (const resp of responses) {
|
|
410
|
-
for (const comment of resp.leadingComments ?? []) lines.push(`${I3}# ${comment}`);
|
|
411
|
-
const bodies = resp.bodies;
|
|
412
|
-
const hasHeaders = resp.headers && resp.headers.length > 0;
|
|
413
|
-
const optOut = resp.headersOptOut;
|
|
414
|
-
// `404(documented):` — the modifier changes what codegen does, so it has to survive.
|
|
415
|
-
const code = resp.emit ? `${resp.statusCode}(${resp.emit})` : `${resp.statusCode}`;
|
|
416
|
-
const inlinable = resp.inline && bodies.length > 0 && !hasHeaders && !optOut && bodies.every(b => b.bodyType.kind !== 'inlineObject');
|
|
417
|
-
if (inlinable) {
|
|
418
|
-
// Written on one line in the source, so keep it there: `200: { application/json: Pet }`.
|
|
419
|
-
// Several mimes on that line stay on it too, space-separated as the grammar has them.
|
|
420
|
-
const inner = bodies.map(b => `${b.contentType}: ${printType(b.bodyType)}`).join(' ');
|
|
421
|
-
lines.push(`${I3}${code}: { ${inner} }`);
|
|
422
|
-
} else if (bodies.length === 0 && !hasHeaders && !optOut && resp.hasBlock) {
|
|
423
|
-
// An empty block means "emitted, no body" — collapsing it to `304:` would change
|
|
424
|
-
// the generated router, so it is not a formatting detail.
|
|
425
|
-
lines.push(`${I3}${code}: {}`);
|
|
426
|
-
} else if (bodies.length > 0 || hasHeaders || optOut || (resp.trailingComments?.length ?? 0) > 0) {
|
|
427
|
-
lines.push(`${I3}${code}: {`);
|
|
428
|
-
for (const body of bodies) {
|
|
429
|
-
for (const comment of body.leadingComments ?? []) lines.push(`${I4}# ${comment}`);
|
|
430
|
-
lines.push(...printContentTypeLine(body.contentType, body.bodyType, I4));
|
|
431
|
-
}
|
|
432
|
-
for (const comment of resp.headersLeadingComments ?? []) lines.push(`${I4}# ${comment}`);
|
|
433
|
-
if (optOut) {
|
|
434
|
-
lines.push(`${I4}headers: none`);
|
|
435
|
-
} else if (hasHeaders) {
|
|
436
|
-
lines.push(`${I4}headers: {`);
|
|
437
|
-
for (const h of resp.headers!) {
|
|
438
|
-
const opt = h.optional ? '?' : '';
|
|
439
|
-
const trail = h.description ? ` # ${h.description}` : '';
|
|
440
|
-
lines.push(`${I4}${INDENT}${h.name}${opt}: ${printType(h.type)}${trail}`);
|
|
441
|
-
}
|
|
442
|
-
lines.push(`${I4}}`);
|
|
443
|
-
}
|
|
444
|
-
for (const comment of resp.trailingComments ?? []) lines.push(`${I4}# ${comment}`);
|
|
445
|
-
lines.push(`${I3}}`);
|
|
446
|
-
} else {
|
|
447
|
-
lines.push(`${I3}${code}:`);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
for (const comment of trailingComments ?? []) lines.push(`${I3}# ${comment}`);
|
|
452
|
-
lines.push(`${I2}}`);
|
|
453
|
-
return lines;
|
|
454
|
-
}
|