@scalar/openapi-to-markdown 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +66 -5
- package/dist/create-markdown-from-openapi.d.ts.map +1 -1
- package/dist/create-markdown-from-openapi.js +3 -2
- package/dist/document-anchors.d.ts +8 -0
- package/dist/document-anchors.d.ts.map +1 -0
- package/dist/document-anchors.js +25 -0
- package/dist/document-examples.d.ts +14 -0
- package/dist/document-examples.d.ts.map +1 -0
- package/dist/document-examples.js +54 -0
- package/dist/get-markdown-examples.d.ts +2 -0
- package/dist/get-markdown-examples.d.ts.map +1 -1
- package/dist/get-markdown-examples.js +11 -12
- package/dist/markdown-nodes.d.ts +22 -1
- package/dist/markdown-nodes.d.ts.map +1 -1
- package/dist/markdown-nodes.js +4 -0
- package/dist/parse-description.d.ts +2 -0
- package/dist/parse-description.d.ts.map +1 -1
- package/dist/parse-description.js +28 -1
- package/dist/render-document.d.ts +2 -2
- package/dist/render-document.d.ts.map +1 -1
- package/dist/render-document.js +217 -84
- package/dist/render-examples.d.ts +9 -1
- package/dist/render-examples.d.ts.map +1 -1
- package/dist/render-examples.js +26 -8
- package/dist/render-operation-details.d.ts +3 -2
- package/dist/render-operation-details.d.ts.map +1 -1
- package/dist/render-operation-details.js +8 -5
- package/dist/render-operation.d.ts +16 -1
- package/dist/render-operation.d.ts.map +1 -1
- package/dist/render-operation.js +168 -42
- package/dist/render-schema.d.ts +30 -3
- package/dist/render-schema.d.ts.map +1 -1
- package/dist/render-schema.js +474 -111
- package/dist/render-security.d.ts +7 -3
- package/dist/render-security.d.ts.map +1 -1
- package/dist/render-security.js +61 -12
- package/dist/select-document.d.ts +34 -3
- package/dist/select-document.d.ts.map +1 -1
- package/dist/select-document.js +58 -30
- package/package.json +18 -9
package/dist/render-schema.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isObject } from '@scalar/helpers/object/is-object';
|
|
2
2
|
import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
3
|
-
import { inlineCode, item, link, list, paragraph, safeUrl, strong, text } from './markdown-nodes.js';
|
|
3
|
+
import { describe, emphasis, inlineCode, item, link, list, paragraph, safeUrl, strong, text } from './markdown-nodes.js';
|
|
4
4
|
/** Guards against stack exhaustion. Past it, references to models point to their own section instead. */
|
|
5
5
|
const MAX_DEPTH = 64;
|
|
6
6
|
/**
|
|
@@ -25,7 +25,8 @@ const structuralKeywords = new Set([
|
|
|
25
25
|
]);
|
|
26
26
|
/** Link bookkeeping that is not a sibling keyword of a reference. */
|
|
27
27
|
const referenceKeys = new Set(['$ref', '$ref-value', '$global', '$status']);
|
|
28
|
-
|
|
28
|
+
/** The reference string of a Reference Object, if the schema is one. */
|
|
29
|
+
const getRef = (input) => isObject(input) && typeof input.$ref === 'string' ? input.$ref : undefined;
|
|
29
30
|
/** Prefer the component name, which is how model sections and other references identify the schema. */
|
|
30
31
|
const getReferenceName = (ref) => {
|
|
31
32
|
try {
|
|
@@ -51,6 +52,17 @@ const getSharedName = (input, view) => {
|
|
|
51
52
|
// Leaf schemas are as short as a reference to them, so they stay in place.
|
|
52
53
|
return structured ? getReferenceName(ref) : undefined;
|
|
53
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* A named model with structural reference siblings is its own schema, not another
|
|
57
|
+
* occurrence of its target. Otherwise either rendering order can hide properties.
|
|
58
|
+
* Follow the original target otherwise: merging reference siblings creates fresh objects.
|
|
59
|
+
*/
|
|
60
|
+
const getSharedIdentity = (input) => {
|
|
61
|
+
const identity = isObject(input) && '$ref' in input && Object.keys(input).some((key) => structuralKeywords.has(key))
|
|
62
|
+
? input
|
|
63
|
+
: (getResolvedRef(input) ?? input);
|
|
64
|
+
return typeof identity === 'object' && identity !== null ? identity : undefined;
|
|
65
|
+
};
|
|
54
66
|
/** Boolean targets still combine with adjacent schema keywords. */
|
|
55
67
|
const resolveMarkdownSchema = (input) => {
|
|
56
68
|
const target = getResolvedRef(input);
|
|
@@ -65,9 +77,90 @@ const resolveMarkdownSchema = (input) => {
|
|
|
65
77
|
// A false target remains impossible, even when siblings describe a type or annotations.
|
|
66
78
|
return { ...merged, allOf: [false, ...(Array.isArray(merged.allOf) ? merged.allOf : [])] };
|
|
67
79
|
};
|
|
80
|
+
/** Annotations that a wrapper schema contributes on top of the schema it wraps. */
|
|
81
|
+
const annotationKeys = ['title', 'description', 'default', 'readOnly', 'writeOnly', 'deprecated'];
|
|
82
|
+
/** Only annotation-only wrappers can be replaced without losing independent constraints. */
|
|
83
|
+
const wrapperKeys = new Set([...annotationKeys, ...referenceKeys, '__scalar_', 'allOf', 'anyOf', 'oneOf']);
|
|
84
|
+
/** Types that never need their own page: their whole definition fits on one line. */
|
|
85
|
+
const primitiveTypes = new Set(['string', 'number', 'integer', 'boolean', 'null']);
|
|
86
|
+
/** Infer the JSON type of a literal, for `const` and `enum` schemas that do not declare one. */
|
|
87
|
+
const getJsonType = (value) => {
|
|
88
|
+
if (value === null)
|
|
89
|
+
return 'null';
|
|
90
|
+
if (Array.isArray(value))
|
|
91
|
+
return 'array';
|
|
92
|
+
if (typeof value === 'number')
|
|
93
|
+
return Number.isInteger(value) ? 'integer' : 'number';
|
|
94
|
+
return typeof value;
|
|
95
|
+
};
|
|
96
|
+
/** A schema that only allows `null`, as in `anyOf: [X, { type: 'null' }]`. */
|
|
97
|
+
const isNullSchema = (input) => {
|
|
98
|
+
const schema = getResolvedRef(input);
|
|
99
|
+
if (!isObject(schema))
|
|
100
|
+
return false;
|
|
101
|
+
const type = Array.isArray(schema.type) && schema.type.length === 1 ? schema.type[0] : schema.type;
|
|
102
|
+
return (type === 'null' &&
|
|
103
|
+
Object.keys(schema).every((key) => key === 'type' || key === 'title' || key === 'description' || referenceKeys.has(key)));
|
|
104
|
+
};
|
|
105
|
+
/** Wrappers that only annotate one schema, or make it nullable, describe that schema. */
|
|
106
|
+
const getWrapped = (value) => {
|
|
107
|
+
if (typeof value.schema !== 'object' ||
|
|
108
|
+
Object.keys(value.schema).some((key) => !wrapperKeys.has(key)) ||
|
|
109
|
+
value.type !== undefined ||
|
|
110
|
+
value.properties.length ||
|
|
111
|
+
value.required.size ||
|
|
112
|
+
value.items !== undefined ||
|
|
113
|
+
value.additionalProperties !== undefined ||
|
|
114
|
+
value.not !== undefined ||
|
|
115
|
+
value.discriminator ||
|
|
116
|
+
value.enum ||
|
|
117
|
+
value.const !== undefined)
|
|
118
|
+
return undefined;
|
|
119
|
+
const allOf = value.allOf ?? [];
|
|
120
|
+
const anyOf = value.anyOf ?? [];
|
|
121
|
+
const oneOf = value.oneOf ?? [];
|
|
122
|
+
if (allOf.length === 1 && !anyOf.length && !oneOf.length)
|
|
123
|
+
return { core: allOf[0], nullable: false };
|
|
124
|
+
const union = anyOf.length ? anyOf : oneOf;
|
|
125
|
+
if (allOf.length || (anyOf.length && oneOf.length) || union.length !== 2)
|
|
126
|
+
return undefined;
|
|
127
|
+
const core = union.filter((branch) => !isNullSchema(branch));
|
|
128
|
+
return core.length === 1 ? { core: core[0], nullable: true } : undefined;
|
|
129
|
+
};
|
|
130
|
+
/** Join alternatives with `|`, keeping neighbouring plain types in one code span. */
|
|
131
|
+
const joinAlternatives = (alternatives) => {
|
|
132
|
+
// `array of string | null` would read as an array of nullable strings.
|
|
133
|
+
if (alternatives.length > 1)
|
|
134
|
+
alternatives = alternatives.map((alternative) => {
|
|
135
|
+
const only = alternative.length === 1 ? alternative[0] : undefined;
|
|
136
|
+
return only?.type === 'inlineCode' && only.value.startsWith('array of ')
|
|
137
|
+
? [inlineCode(`(${only.value})`)]
|
|
138
|
+
: alternative;
|
|
139
|
+
});
|
|
140
|
+
const nodes = [];
|
|
141
|
+
let plain = [];
|
|
142
|
+
const flushPlain = () => {
|
|
143
|
+
if (plain.length)
|
|
144
|
+
nodes.push(...(nodes.length ? [text(' | ')] : []), inlineCode(plain.join(' | ')));
|
|
145
|
+
plain = [];
|
|
146
|
+
};
|
|
147
|
+
for (const alternative of alternatives) {
|
|
148
|
+
const only = alternative.length === 1 ? alternative[0] : undefined;
|
|
149
|
+
if (only?.type === 'inlineCode') {
|
|
150
|
+
plain.push(only.value);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
flushPlain();
|
|
154
|
+
nodes.push(...(nodes.length ? [text(' | ')] : []), ...alternative);
|
|
155
|
+
}
|
|
156
|
+
flushPlain();
|
|
157
|
+
return nodes;
|
|
158
|
+
};
|
|
68
159
|
/** Keep merged reference siblings and sorted properties stable throughout an export. */
|
|
69
160
|
export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
|
|
70
161
|
const views = new WeakMap();
|
|
162
|
+
/** Wrappers being unwrapped, so a wrapper that refers to itself stops unwrapping. */
|
|
163
|
+
const unwrapping = new Set();
|
|
71
164
|
const view = (input) => {
|
|
72
165
|
const cached = typeof input === 'object' ? views.get(input) : undefined;
|
|
73
166
|
if (cached)
|
|
@@ -80,7 +173,7 @@ export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
|
|
|
80
173
|
const properties = Object.entries(value.properties ?? {})
|
|
81
174
|
.filter(([, child]) => typeof child === 'boolean' || (child !== null && typeof child === 'object'))
|
|
82
175
|
.sort(([a], [b]) => Number(required.has(b)) - Number(required.has(a)) || a.localeCompare(b));
|
|
83
|
-
|
|
176
|
+
let result = {
|
|
84
177
|
...value,
|
|
85
178
|
type: typeof schema === 'boolean' ? (schema ? 'any' : 'never') : value.type,
|
|
86
179
|
schema: schema,
|
|
@@ -88,87 +181,279 @@ export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
|
|
|
88
181
|
properties,
|
|
89
182
|
};
|
|
90
183
|
result.name = getSharedName(input, result);
|
|
184
|
+
const wrapped = typeof input === 'object' && !unwrapping.has(input) ? getWrapped(result) : undefined;
|
|
185
|
+
if (wrapped && typeof input === 'object') {
|
|
186
|
+
unwrapping.add(input);
|
|
187
|
+
const inner = view(wrapped.core);
|
|
188
|
+
unwrapping.delete(input);
|
|
189
|
+
const annotations = Object.fromEntries(annotationKeys.flatMap((key) => (result[key] === undefined ? [] : [[key, result[key]]])));
|
|
190
|
+
result = { ...inner, ...annotations, core: wrapped.core, nullable: wrapped.nullable || inner.nullable };
|
|
191
|
+
}
|
|
91
192
|
if (typeof input === 'object')
|
|
92
193
|
views.set(input, result);
|
|
93
194
|
return result;
|
|
94
195
|
};
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
196
|
+
const primitives = new WeakMap();
|
|
197
|
+
/** Primitives, enums, consts and aliases of them, whose whole definition fits on one line. */
|
|
198
|
+
const isPrimitive = (input) => {
|
|
199
|
+
if (typeof input !== 'object')
|
|
200
|
+
return true;
|
|
201
|
+
const cached = primitives.get(input);
|
|
202
|
+
if (cached !== undefined)
|
|
203
|
+
return cached;
|
|
204
|
+
// Assume a recursive alias is not primitive while it is being checked.
|
|
205
|
+
primitives.set(input, false);
|
|
206
|
+
const value = view(input);
|
|
207
|
+
const types = value.type === undefined ? [] : [value.type].flat();
|
|
208
|
+
const result = value.core !== undefined
|
|
209
|
+
? isPrimitive(value.core)
|
|
210
|
+
: typeof value.schema === 'boolean' ||
|
|
211
|
+
(!value.properties.length &&
|
|
212
|
+
value.items === undefined &&
|
|
213
|
+
(value.additionalProperties === undefined || typeof value.additionalProperties === 'boolean') &&
|
|
214
|
+
!value.allOf?.length &&
|
|
215
|
+
!value.anyOf?.length &&
|
|
216
|
+
!value.oneOf?.length &&
|
|
217
|
+
value.not === undefined &&
|
|
218
|
+
!value.discriminator &&
|
|
219
|
+
types.every((type) => primitiveTypes.has(type)));
|
|
220
|
+
primitives.set(input, result);
|
|
221
|
+
return result;
|
|
222
|
+
};
|
|
223
|
+
/** Whether the one-line summary of a schema has anything besides its type. */
|
|
224
|
+
const hasAnnotations = (value) => value.description !== undefined ||
|
|
225
|
+
value.format !== undefined ||
|
|
226
|
+
value.enum !== undefined ||
|
|
227
|
+
value.const !== undefined ||
|
|
228
|
+
value.default !== undefined ||
|
|
229
|
+
value.readOnly === true ||
|
|
230
|
+
value.writeOnly === true ||
|
|
231
|
+
value.deprecated === true ||
|
|
232
|
+
value.additionalProperties === false ||
|
|
233
|
+
numericAnnotations.some((key) => value[key] !== undefined);
|
|
234
|
+
const numericAnnotations = [
|
|
235
|
+
'minimum',
|
|
236
|
+
'maximum',
|
|
237
|
+
'exclusiveMinimum',
|
|
238
|
+
'exclusiveMaximum',
|
|
239
|
+
'multipleOf',
|
|
240
|
+
'minLength',
|
|
241
|
+
'maxLength',
|
|
242
|
+
'pattern',
|
|
243
|
+
'minProperties',
|
|
244
|
+
'maxProperties',
|
|
245
|
+
'minItems',
|
|
246
|
+
'maxItems',
|
|
247
|
+
'uniqueItems',
|
|
248
|
+
];
|
|
249
|
+
/** The declared types, or the types implied by `const`, `enum`, properties or items. */
|
|
250
|
+
const getTypes = (value) => {
|
|
251
|
+
if (value.type !== undefined)
|
|
252
|
+
return [value.type].flat();
|
|
109
253
|
if (value.const !== undefined)
|
|
110
|
-
|
|
111
|
-
if (value.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
'
|
|
115
|
-
|
|
116
|
-
'
|
|
117
|
-
|
|
118
|
-
'multipleOf',
|
|
119
|
-
'minLength',
|
|
120
|
-
'maxLength',
|
|
121
|
-
'pattern',
|
|
122
|
-
'minProperties',
|
|
123
|
-
'maxProperties',
|
|
124
|
-
])
|
|
125
|
-
add(key, value[key]);
|
|
126
|
-
for (const key of ['readOnly', 'writeOnly']) {
|
|
127
|
-
if (value[key])
|
|
128
|
-
nodes.push(text(`${nodes.length ? ', ' : ''}${key}`));
|
|
129
|
-
}
|
|
130
|
-
if (!hideDescription && value.description)
|
|
131
|
-
nodes.push(text(`${nodes.length ? ' — ' : ''}${value.description}`));
|
|
132
|
-
return nodes;
|
|
254
|
+
return [getJsonType(value.const)];
|
|
255
|
+
if (value.enum?.length)
|
|
256
|
+
return [...new Set(value.enum.map(getJsonType))];
|
|
257
|
+
if (value.properties.length || isObject(value.additionalProperties))
|
|
258
|
+
return ['object'];
|
|
259
|
+
if (value.items !== undefined)
|
|
260
|
+
return ['array'];
|
|
261
|
+
return [];
|
|
133
262
|
};
|
|
134
|
-
const forDocument = (models = {}, settings = {}) => {
|
|
263
|
+
const forDocument = (models = {}, settings = {}, destinations) => {
|
|
135
264
|
const linked = settings.schemaReferences?.mode === 'linked';
|
|
265
|
+
const inlinePrimitives = settings.schemaReferences?.inlinePrimitives !== false;
|
|
136
266
|
/** Shared schemas this document already expanded, with the name later references use. */
|
|
137
267
|
const shown = new Map();
|
|
138
268
|
/** Models that the document renders in their own sections after the operations. */
|
|
139
269
|
const sections = new Set(Object.values(models).map((model) => getResolvedRef(model) ?? model));
|
|
140
270
|
let nodeCount = 0;
|
|
271
|
+
/** The reference a nested schema links to in linked mode, instead of expanding its target. */
|
|
272
|
+
const getLink = (input) => {
|
|
273
|
+
if ((!linked && !destinations) || !isObject(input))
|
|
274
|
+
return undefined;
|
|
275
|
+
const ref = getRef(input);
|
|
276
|
+
if (ref !== undefined) {
|
|
277
|
+
// A primitive alias is shorter than a link to it, and saves the reader a page.
|
|
278
|
+
const resolved = getResolvedRef(input) !== undefined;
|
|
279
|
+
if (!linked && !destinations?.has(getReferenceName(ref)))
|
|
280
|
+
return undefined;
|
|
281
|
+
return resolved && inlinePrimitives && isPrimitive(input) ? undefined : ref;
|
|
282
|
+
}
|
|
283
|
+
const core = view(input).core;
|
|
284
|
+
return core === undefined ? undefined : getLink(core);
|
|
285
|
+
};
|
|
286
|
+
const referenceNode = (ref) => {
|
|
287
|
+
const name = getReferenceName(ref);
|
|
288
|
+
const url = settings.schemaReferences?.resolveUrl
|
|
289
|
+
? settings.schemaReferences.resolveUrl({ ref, name })
|
|
290
|
+
: destinations?.get(name);
|
|
291
|
+
return url && safeUrl(url) ? link(url, name) : inlineCode(name);
|
|
292
|
+
};
|
|
293
|
+
/** Annotations written next to a linked reference; the linked page documents its target. */
|
|
294
|
+
const getLinkAnnotations = (input) => {
|
|
295
|
+
const own = {};
|
|
296
|
+
let current = input;
|
|
297
|
+
while (isObject(current)) {
|
|
298
|
+
for (const [key, entry] of Object.entries(current)) {
|
|
299
|
+
if (!referenceKeys.has(key) &&
|
|
300
|
+
(!structuralKeywords.has(key) ||
|
|
301
|
+
numericAnnotations.some((annotation) => annotation === key) ||
|
|
302
|
+
(key === 'additionalProperties' && entry === false)) &&
|
|
303
|
+
!(key in own))
|
|
304
|
+
own[key] = entry;
|
|
305
|
+
}
|
|
306
|
+
if (getRef(current) !== undefined)
|
|
307
|
+
break;
|
|
308
|
+
current = view(current).core;
|
|
309
|
+
}
|
|
310
|
+
return view(own);
|
|
311
|
+
};
|
|
312
|
+
/** The annotations a nested schema prints next to its type: for a link, only those beside the reference. */
|
|
313
|
+
const getOwnView = (input) => getLink(input) === undefined ? view(input) : getLinkAnnotations(input);
|
|
314
|
+
const getLabel = (input, nested, depth = 0) => {
|
|
315
|
+
const value = view(input);
|
|
316
|
+
const ref = nested || destinations ? getLink(input) : undefined;
|
|
317
|
+
if (ref !== undefined) {
|
|
318
|
+
// A reference to a nullable model is already nullable; a nullable wrapper around one is not.
|
|
319
|
+
const nullable = getRef(input) === undefined && value.nullable;
|
|
320
|
+
const structural = isObject(input) && Object.keys(input).some((key) => structuralKeywords.has(key));
|
|
321
|
+
return {
|
|
322
|
+
nodes: joinAlternatives([[referenceNode(ref)], ...(nullable ? [[inlineCode('null')]] : [])]),
|
|
323
|
+
complete: !structural,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (typeof value.schema === 'boolean')
|
|
327
|
+
return { nodes: [], complete: true };
|
|
328
|
+
const types = getTypes(value);
|
|
329
|
+
const alternatives = [];
|
|
330
|
+
let complete = !value.properties.length &&
|
|
331
|
+
!isObject(value.additionalProperties) &&
|
|
332
|
+
value.not === undefined &&
|
|
333
|
+
!value.discriminator &&
|
|
334
|
+
!value.allOf?.length &&
|
|
335
|
+
!(value.anyOf?.length && value.oneOf?.length);
|
|
336
|
+
const union = value.anyOf?.length ? value.anyOf : value.oneOf;
|
|
337
|
+
if (union?.length) {
|
|
338
|
+
const branches = depth < 8 ? union.map((branch) => getLabel(branch, true, depth + 1)) : [];
|
|
339
|
+
// A union of fully described branches reads as one type, for example `string | integer`.
|
|
340
|
+
if (complete &&
|
|
341
|
+
!types.length &&
|
|
342
|
+
branches.length === union.length &&
|
|
343
|
+
branches.every((branch, index) => branch.complete && branch.nodes.length && !hasAnnotations(getOwnView(union[index]))))
|
|
344
|
+
alternatives.push(...branches.map((branch) => branch.nodes));
|
|
345
|
+
else
|
|
346
|
+
complete = false;
|
|
347
|
+
}
|
|
348
|
+
for (const type of types) {
|
|
349
|
+
if (type !== 'array' || value.items === undefined) {
|
|
350
|
+
alternatives.push([inlineCode(type)]);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const items = depth < 8 ? getLabel(value.items, true, depth + 1) : undefined;
|
|
354
|
+
if (!items?.complete || !items.nodes.length || hasAnnotations(getOwnView(value.items))) {
|
|
355
|
+
complete = false;
|
|
356
|
+
alternatives.push([inlineCode('array')]);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const only = items.nodes.length === 1 ? items.nodes[0] : undefined;
|
|
360
|
+
alternatives.push(only?.type === 'inlineCode'
|
|
361
|
+
? [inlineCode(only.value.includes(' | ') ? `array of (${only.value})` : `array of ${only.value}`)]
|
|
362
|
+
: [text('array of '), ...(items.nodes.length > 1 ? [text('('), ...items.nodes, text(')')] : items.nodes)]);
|
|
363
|
+
}
|
|
364
|
+
if (!types.length && value.items !== undefined)
|
|
365
|
+
complete = false;
|
|
366
|
+
// Without a type to name, a nullable schema says so and leaves its structure to the lines below.
|
|
367
|
+
if (value.nullable && !types.includes('null'))
|
|
368
|
+
alternatives.push(alternatives.length || complete ? [inlineCode('null')] : [text('nullable')]);
|
|
369
|
+
// A schema without types or structure accepts any value.
|
|
370
|
+
if (!alternatives.length && complete)
|
|
371
|
+
alternatives.push([inlineCode('any')]);
|
|
372
|
+
return { nodes: joinAlternatives(alternatives), complete };
|
|
373
|
+
};
|
|
374
|
+
/** The one-line summary of a schema: its type, then its annotations. */
|
|
375
|
+
const summarize = (input, { nested, label = getLabel(input, nested), showType = true, }) => {
|
|
376
|
+
const linkedLabel = (nested || destinations !== undefined) && getLink(input) !== undefined;
|
|
377
|
+
const value = linkedLabel ? getLinkAnnotations(input) : view(input);
|
|
378
|
+
if (typeof value.schema === 'boolean')
|
|
379
|
+
return { line: [text(value.schema ? 'any (true schema)' : 'never (false schema)')] };
|
|
380
|
+
const nodes = showType ? [...label.nodes] : [];
|
|
381
|
+
const add = (name, entry) => {
|
|
382
|
+
if (entry !== undefined)
|
|
383
|
+
nodes.push(text(`${nodes.length ? ', ' : ''}${name}: `), inlineCode(entry));
|
|
384
|
+
};
|
|
385
|
+
const flag = (name, entry) => {
|
|
386
|
+
if (entry)
|
|
387
|
+
nodes.push(text(`${nodes.length ? ', ' : ''}${name}`));
|
|
388
|
+
};
|
|
389
|
+
// Later references to a shared schema point back to this name.
|
|
390
|
+
const ref = getRef(input);
|
|
391
|
+
if (!linkedLabel && value.name !== undefined) {
|
|
392
|
+
// In linked mode the expanded schema also has its own page.
|
|
393
|
+
if (linked && ref !== undefined)
|
|
394
|
+
nodes.push(text(`${nodes.length ? ', ' : ''}schema: `), referenceNode(ref));
|
|
395
|
+
else
|
|
396
|
+
add('schema', value.name);
|
|
397
|
+
}
|
|
398
|
+
if (linkedLabel && value.type !== undefined)
|
|
399
|
+
add('type', [value.type].flat().join(' | '));
|
|
400
|
+
add('format', value.format);
|
|
401
|
+
if (value.enum)
|
|
402
|
+
add('possible values', value.enum.map((entry) => JSON.stringify(entry)).join(', '));
|
|
403
|
+
if (value.const !== undefined)
|
|
404
|
+
add('const', JSON.stringify(value.const));
|
|
405
|
+
if (value.default !== undefined)
|
|
406
|
+
add('default', JSON.stringify(value.default));
|
|
407
|
+
for (const key of numericAnnotations)
|
|
408
|
+
add(key, value[key]);
|
|
409
|
+
for (const key of ['readOnly', 'writeOnly', 'deprecated'])
|
|
410
|
+
flag(key, value[key]);
|
|
411
|
+
flag('no additional properties', value.additionalProperties === false);
|
|
412
|
+
// A reference without its own description still says what it is, from the schema it links to.
|
|
413
|
+
return { line: nodes, description: value.description ?? (linkedLabel ? view(input).description : undefined) };
|
|
414
|
+
};
|
|
141
415
|
/** Refer to a schema expanded elsewhere, keeping annotations the reference itself adds. */
|
|
142
|
-
const reference = (input,
|
|
416
|
+
const reference = (input, options, name, location) => {
|
|
143
417
|
const nodes = [];
|
|
144
|
-
const siblings = isObject(input)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
418
|
+
const siblings = isObject(input)
|
|
419
|
+
? Object.fromEntries(Object.entries(input).filter(([key]) => !referenceKeys.has(key)))
|
|
420
|
+
: {};
|
|
421
|
+
if (Object.keys(siblings).length && !options.hideDetails) {
|
|
422
|
+
const { line, description } = summarize(siblings, { nested: false, showType: false });
|
|
423
|
+
if (line.length)
|
|
424
|
+
nodes.push(paragraph(...line));
|
|
425
|
+
if (!options.hideDescription)
|
|
426
|
+
nodes.push(...describe(description));
|
|
149
427
|
}
|
|
150
428
|
nodes.push(paragraph(emphasis(text('Schema '), inlineCode(name), text(` is shown ${location}.`))));
|
|
151
429
|
return nodes;
|
|
152
430
|
};
|
|
431
|
+
/** Print a schema's summary line and description, unless the caller already did. */
|
|
432
|
+
const header = (input, depth, options, showType = true) => {
|
|
433
|
+
if (options.hideDetails)
|
|
434
|
+
return [];
|
|
435
|
+
const { line, description } = summarize(input, { nested: depth > 0, showType });
|
|
436
|
+
return [...(line.length ? [paragraph(...line)] : []), ...(options.hideDescription ? [] : describe(description))];
|
|
437
|
+
};
|
|
153
438
|
const render = (input, depth = 0, ancestors = [], options = {}) => {
|
|
154
|
-
if (linked && isObject(input) && '$ref' in input && typeof input.$ref === 'string') {
|
|
439
|
+
if ((linked || destinations) && isObject(input) && '$ref' in input && typeof input.$ref === 'string') {
|
|
155
440
|
const target = getResolvedRef(input);
|
|
156
441
|
// Reference siblings are independent constraints, not replacements for target keywords.
|
|
157
442
|
const siblings = Object.fromEntries(Object.entries(input).filter(([key]) => !referenceKeys.has(key)));
|
|
158
443
|
const hasSiblings = Object.keys(siblings).length > 0;
|
|
159
|
-
if (depth > 0 || target === undefined) {
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
444
|
+
if (((depth > 0 || destinations) && getLink(input) !== undefined) || target === undefined) {
|
|
445
|
+
// The summary links to the model page; only structural siblings still need rendering.
|
|
446
|
+
const structural = Object.fromEntries(Object.entries(siblings).filter(([key]) => structuralKeywords.has(key)));
|
|
447
|
+
return [
|
|
448
|
+
...header(input, Math.max(depth, 1), options),
|
|
449
|
+
...(Object.keys(structural).length
|
|
450
|
+
? render(structural, depth, ancestors, { hideDetails: true })
|
|
451
|
+
: []),
|
|
164
452
|
];
|
|
165
|
-
if (hasSiblings)
|
|
166
|
-
nodes.push(...render(siblings, depth, ancestors));
|
|
167
|
-
return nodes;
|
|
168
453
|
}
|
|
169
454
|
// An alias with siblings is another reference boundary. Keep its reference visible
|
|
170
455
|
// instead of overwriting it with the outer reference during normalization.
|
|
171
|
-
if (hasSiblings || (isObject(target) && '$ref' in target)) {
|
|
456
|
+
if (!destinations && depth === 0 && (hasSiblings || (isObject(target) && '$ref' in target))) {
|
|
172
457
|
return [
|
|
173
458
|
...(hasSiblings ? [paragraph(strong(text('All of:')))] : []),
|
|
174
459
|
...render(target, depth + 1, ancestors),
|
|
@@ -182,23 +467,33 @@ export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
|
|
|
182
467
|
return [paragraph(emphasis(text('[Circular Reference]')))];
|
|
183
468
|
}
|
|
184
469
|
const value = view(input);
|
|
185
|
-
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
470
|
+
const label = getLabel(input, depth > 0);
|
|
471
|
+
// A wrapper prints its merged summary, then its core schema's structure.
|
|
472
|
+
if (value.core !== undefined) {
|
|
473
|
+
const nodes = header(input, depth, options);
|
|
474
|
+
if (label.complete)
|
|
475
|
+
return nodes;
|
|
476
|
+
return [
|
|
477
|
+
...nodes,
|
|
478
|
+
...render(value.core, depth, ancestors, {
|
|
479
|
+
hideDetails: true,
|
|
480
|
+
hideDescription: true,
|
|
481
|
+
property: options.property,
|
|
482
|
+
}),
|
|
483
|
+
];
|
|
484
|
+
}
|
|
485
|
+
const shared = getSharedIdentity(input);
|
|
191
486
|
const name = options.name ?? value.name;
|
|
192
|
-
if (shared && name !== undefined) {
|
|
487
|
+
if (!destinations && shared && name !== undefined) {
|
|
193
488
|
// Expanding every path through a shared schema grows exponentially, so expand it once.
|
|
194
489
|
const previous = shown.get(shared);
|
|
195
490
|
if (previous !== undefined) {
|
|
196
491
|
// A model section already prints its own annotations above the schema.
|
|
197
492
|
const referenceOptions = options.name === undefined ? options : { ...options, hideDetails: true };
|
|
198
|
-
return reference(input,
|
|
493
|
+
return reference(input, referenceOptions, previous.name, 'above');
|
|
199
494
|
}
|
|
200
495
|
if (value.name !== undefined && options.name === undefined && depth >= MAX_DEPTH && sections.has(shared))
|
|
201
|
-
return reference(input,
|
|
496
|
+
return reference(input, options, value.name, 'below under Schemas');
|
|
202
497
|
}
|
|
203
498
|
if (depth >= MAX_DEPTH)
|
|
204
499
|
return [paragraph(text('[Maximum schema depth reached]'))];
|
|
@@ -206,31 +501,51 @@ export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
|
|
|
206
501
|
return [paragraph(emphasis(text('[Schema output truncated]')))];
|
|
207
502
|
nodeCount++;
|
|
208
503
|
if (shared && name !== undefined && !shown.has(shared))
|
|
209
|
-
shown.set(shared, name);
|
|
504
|
+
shown.set(shared, { name, description: value.description });
|
|
210
505
|
if (typeof value.schema === 'boolean')
|
|
211
|
-
return
|
|
506
|
+
return header(input, depth, options);
|
|
212
507
|
const childAncestors = [...ancestors, identity];
|
|
213
508
|
const nodes = [];
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
509
|
+
// Discriminator values that name a branch are printed beside it instead of in a separate list.
|
|
510
|
+
const mappings = Object.entries(value.discriminator?.mapping ?? {});
|
|
511
|
+
const mapped = new Set();
|
|
512
|
+
let discriminatorShown = false;
|
|
513
|
+
if (!label.complete) {
|
|
514
|
+
for (const [key, title] of [
|
|
515
|
+
['allOf', 'All of:'],
|
|
516
|
+
['anyOf', 'Any of:'],
|
|
517
|
+
['oneOf', 'One of:'],
|
|
518
|
+
]) {
|
|
519
|
+
if (!value[key]?.length)
|
|
520
|
+
continue;
|
|
521
|
+
// One list item per branch keeps the boundary between branches visible.
|
|
522
|
+
const branches = value[key].map((child) => {
|
|
523
|
+
const blocks = render(child, depth + 1, childAncestors, { showType: true });
|
|
524
|
+
const ref = getRef(child);
|
|
525
|
+
const values = key === 'allOf' || ref === undefined ? [] : mappings.filter(([, target]) => target === ref);
|
|
526
|
+
const first = blocks[0];
|
|
527
|
+
if (values.length && first?.type === 'paragraph') {
|
|
528
|
+
for (const [name] of values)
|
|
529
|
+
mapped.add(name);
|
|
530
|
+
first.children.push(text(`${first.children.length ? ', ' : ''}${value.discriminator.propertyName}: `), inlineCode(values.map(([name]) => name).join(', ')));
|
|
531
|
+
}
|
|
532
|
+
return item(...blocks);
|
|
533
|
+
});
|
|
534
|
+
const discriminated = key !== 'allOf' && value.discriminator;
|
|
535
|
+
nodes.push(paragraph(strong(text(title)), ...(discriminated ? [text(' discriminated by '), inlineCode(value.discriminator.propertyName)] : [])), list(branches));
|
|
536
|
+
if (discriminated)
|
|
537
|
+
discriminatorShown = true;
|
|
538
|
+
}
|
|
539
|
+
if (value.not !== undefined)
|
|
540
|
+
nodes.push(paragraph(strong(text('Not:'))), ...render(value.not, depth + 1, childAncestors));
|
|
232
541
|
}
|
|
233
|
-
|
|
542
|
+
// Child sections imply a single container type, but never its nullable alternatives.
|
|
543
|
+
const impliedType = !options.showType &&
|
|
544
|
+
!label.complete &&
|
|
545
|
+
((value.type === 'object' && value.properties.length > 0) ||
|
|
546
|
+
(value.type === 'array' && value.items !== undefined));
|
|
547
|
+
nodes.push(...header(input, depth, options, !impliedType));
|
|
548
|
+
if (linked || destinations) {
|
|
234
549
|
// A reference sibling may require a field declared only in the target schema.
|
|
235
550
|
const declared = new Set(value.properties.map(([name]) => name));
|
|
236
551
|
const required = [...value.required].filter((name) => !declared.has(name));
|
|
@@ -239,48 +554,96 @@ export const createSchemaRenderer = ({ maxNodes = MAX_NODES } = {}) => {
|
|
|
239
554
|
}
|
|
240
555
|
if (value.properties.length) {
|
|
241
556
|
const properties = value.properties.map(([name, schema]) => {
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
557
|
+
const childLabel = getLabel(schema, true);
|
|
558
|
+
const { line, description } = summarize(schema, { nested: true, label: childLabel });
|
|
559
|
+
const title = [
|
|
560
|
+
strong(inlineCode(name), ...(value.required.has(name) ? [text(' (required)')] : [])),
|
|
561
|
+
];
|
|
562
|
+
if (line.length)
|
|
563
|
+
title.push(text(': '), ...line);
|
|
564
|
+
const blocks = [paragraph(...title), ...describe(description)];
|
|
565
|
+
if (!childLabel.complete)
|
|
566
|
+
blocks.push(...render(schema, depth + 1, childAncestors, {
|
|
567
|
+
hideDetails: true,
|
|
568
|
+
hideDescription: true,
|
|
569
|
+
property: true,
|
|
570
|
+
}));
|
|
251
571
|
return item(...blocks);
|
|
252
572
|
});
|
|
253
573
|
nodes.push(list(properties));
|
|
254
574
|
}
|
|
255
|
-
if (
|
|
575
|
+
if (!label.complete &&
|
|
576
|
+
value.items !== undefined &&
|
|
577
|
+
(value.type === 'array' || value.type === undefined || [value.type].flat().includes('array'))) {
|
|
256
578
|
nodes.push(paragraph(strong(text(options.property ? 'Items:' : 'Array of:'))), ...render(value.items, depth + 1, childAncestors));
|
|
257
579
|
}
|
|
258
|
-
|
|
259
|
-
if (value.minItems !== undefined)
|
|
260
|
-
constraints.push(item(paragraph(text('Min items: '), inlineCode(value.minItems))));
|
|
261
|
-
if (value.maxItems !== undefined)
|
|
262
|
-
constraints.push(item(paragraph(text('Max items: '), inlineCode(value.maxItems))));
|
|
263
|
-
if (value.uniqueItems !== undefined)
|
|
264
|
-
constraints.push(item(paragraph(text('Unique items: '), inlineCode(value.uniqueItems))));
|
|
265
|
-
if (constraints.length)
|
|
266
|
-
nodes.push(list(constraints));
|
|
267
|
-
if (value.additionalProperties !== undefined)
|
|
580
|
+
if (isObject(value.additionalProperties))
|
|
268
581
|
nodes.push(paragraph(strong(text('Additional properties:'))), ...render(value.additionalProperties, depth + 1, childAncestors));
|
|
269
|
-
|
|
582
|
+
const unmapped = mappings.filter(([name]) => !mapped.has(name));
|
|
583
|
+
if (value.discriminator && (!discriminatorShown || unmapped.length)) {
|
|
270
584
|
nodes.push(paragraph(strong(text('Discriminator:')), text(' '), inlineCode(value.discriminator.propertyName)));
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
nodes.push(list(mappings.map(([name, target]) => item(paragraph(inlineCode(name), text(': '), inlineCode(target))))));
|
|
585
|
+
if (unmapped.length)
|
|
586
|
+
nodes.push(list(unmapped.map(([name, target]) => item(paragraph(inlineCode(name), text(': '), linked || destinations ? referenceNode(target) : inlineCode(target))))));
|
|
274
587
|
}
|
|
275
588
|
return nodes;
|
|
276
589
|
};
|
|
590
|
+
/** Keywords whose values are schemas, and whether each holds a list or a map of them. */
|
|
591
|
+
const schemaKeywords = {
|
|
592
|
+
items: 'one',
|
|
593
|
+
additionalProperties: 'one',
|
|
594
|
+
not: 'one',
|
|
595
|
+
prefixItems: 'list',
|
|
596
|
+
allOf: 'list',
|
|
597
|
+
anyOf: 'list',
|
|
598
|
+
oneOf: 'list',
|
|
599
|
+
properties: 'map',
|
|
600
|
+
patternProperties: 'map',
|
|
601
|
+
};
|
|
602
|
+
const getExampleSchema = (input, root) => {
|
|
603
|
+
if (!isObject(input))
|
|
604
|
+
return input;
|
|
605
|
+
if (!root && getLink(input) !== undefined) {
|
|
606
|
+
const types = getTypes(view(input));
|
|
607
|
+
return types.includes('array') && !types.includes('object') ? { type: 'array', items: {} } : { type: 'object' };
|
|
608
|
+
}
|
|
609
|
+
// Primitive references stay linked; a spread copy would lose the non-enumerable target.
|
|
610
|
+
if (getRef(input) !== undefined) {
|
|
611
|
+
if (!root)
|
|
612
|
+
return input;
|
|
613
|
+
// Merging keeps the `$ref` key, which must not be followed again.
|
|
614
|
+
const merged = getResolvedRef(input, mergeSiblingReferences);
|
|
615
|
+
if (!isObject(merged))
|
|
616
|
+
return merged;
|
|
617
|
+
const { $ref: _ref, ...target } = merged;
|
|
618
|
+
return getExampleSchema(target, true);
|
|
619
|
+
}
|
|
620
|
+
const copy = { ...input };
|
|
621
|
+
for (const [key, kind] of Object.entries(schemaKeywords)) {
|
|
622
|
+
const value = input[key];
|
|
623
|
+
if (kind === 'one')
|
|
624
|
+
copy[key] = getExampleSchema(value, false);
|
|
625
|
+
else if (kind === 'list' && Array.isArray(value))
|
|
626
|
+
copy[key] = value.map((entry) => getExampleSchema(entry, false));
|
|
627
|
+
else if (kind === 'map' && isObject(value))
|
|
628
|
+
copy[key] = Object.fromEntries(Object.entries(value).map(([name, entry]) => [name, getExampleSchema(entry, false)]));
|
|
629
|
+
if (copy[key] === undefined)
|
|
630
|
+
delete copy[key];
|
|
631
|
+
}
|
|
632
|
+
return copy;
|
|
633
|
+
};
|
|
277
634
|
return {
|
|
278
635
|
linked,
|
|
279
636
|
view,
|
|
280
637
|
render,
|
|
638
|
+
exampleSchema: (schema) => (linked ? getExampleSchema(schema, true) : schema),
|
|
639
|
+
summarize: (schema) => summarize(schema, { nested: false }).line,
|
|
281
640
|
beginSection: () => {
|
|
282
641
|
nodeCount = 0;
|
|
283
642
|
},
|
|
643
|
+
shownAs: (schema) => {
|
|
644
|
+
const shared = getSharedIdentity(schema);
|
|
645
|
+
return shared ? shown.get(shared) : undefined;
|
|
646
|
+
},
|
|
284
647
|
forDocument,
|
|
285
648
|
};
|
|
286
649
|
};
|