@aravindc26/velu 0.11.0 → 0.11.3
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/package.json +15 -6
- package/schema/velu.schema.json +1251 -115
- package/src/build.ts +1121 -304
- package/src/cli.ts +90 -26
- package/src/engine/_server.mjs +1684 -277
- package/src/engine/app/(docs)/[...slug]/layout.tsx +371 -0
- package/src/engine/app/(docs)/[...slug]/page.tsx +926 -0
- package/src/engine/app/api/proxy/route.ts +23 -0
- package/src/engine/app/copy-page.css +59 -1
- package/src/engine/app/global.css +3157 -3
- package/src/engine/app/layout.tsx +56 -1
- package/src/engine/app/llms-file/route.ts +87 -0
- package/src/engine/app/llms-full-file/route.ts +62 -0
- package/src/engine/app/md-file/[...slug]/route.ts +409 -0
- package/src/engine/app/page.tsx +45 -0
- package/src/engine/app/robots.txt/route.ts +63 -0
- package/src/engine/app/rss-file/[...slug]/route.ts +169 -0
- package/src/engine/app/sitemap.xml/route.ts +82 -0
- package/src/engine/components/assistant.tsx +16 -5
- package/src/engine/components/changelog-filters.tsx +114 -0
- package/src/engine/components/code-group.tsx +383 -0
- package/src/engine/components/color.tsx +118 -0
- package/src/engine/components/expandable.tsx +77 -0
- package/src/engine/components/icon.tsx +136 -0
- package/src/engine/components/image-zoom-fallback.tsx +147 -0
- package/src/engine/components/image.tsx +111 -0
- package/src/engine/components/manual-api-playground.tsx +154 -0
- package/src/engine/components/mermaid.tsx +142 -0
- package/src/engine/components/openapi-toc-sync.tsx +59 -0
- package/src/engine/components/openapi.tsx +1682 -0
- package/src/engine/components/page-feedback.tsx +153 -0
- package/src/engine/components/product-switcher.tsx +27 -3
- package/src/engine/components/prompt.tsx +90 -0
- package/src/engine/components/providers.tsx +1 -6
- package/src/engine/components/search.tsx +4 -0
- package/src/engine/components/sidebar-links.tsx +13 -15
- package/src/engine/components/synced-tabs.tsx +57 -0
- package/src/engine/components/toc-examples.tsx +110 -0
- package/src/engine/components/view.tsx +344 -0
- package/src/engine/generated/redirects.ts +3 -0
- package/src/engine/lib/changelog.ts +246 -0
- package/src/engine/lib/layout.shared.ts +30 -2
- package/src/engine/lib/llms.ts +444 -0
- package/src/engine/lib/navigation-normalize.mjs +481 -412
- package/src/engine/lib/navigation-normalize.ts +261 -54
- package/src/engine/lib/redirects.ts +194 -0
- package/src/engine/lib/source.ts +107 -4
- package/src/engine/lib/velu.ts +368 -2
- package/src/engine/mdx-components.tsx +648 -0
- package/src/engine/middleware.ts +66 -0
- package/src/engine/public/icons/cursor-dark.svg +12 -0
- package/src/engine/public/icons/cursor-light.svg +12 -0
- package/src/engine/source.config.ts +98 -1
- package/src/engine/src/components/PageTitle.astro +16 -5
- package/src/engine/src/lib/velu.ts +11 -3
- package/src/navigation-normalize.ts +252 -54
- package/src/themes.ts +6 -6
- package/src/validate.ts +119 -6
- package/src/engine/app/(docs)/[[...slug]]/layout.tsx +0 -87
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +0 -146
|
@@ -0,0 +1,1682 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { createCodeSample, createOpenAPI } from 'fumadocs-openapi/server';
|
|
4
|
+
import { createAPIPage } from 'fumadocs-openapi/ui';
|
|
5
|
+
import type { HttpMethods, MethodInformation, RenderContext } from 'fumadocs-openapi';
|
|
6
|
+
import type { OpenAPIServer } from 'fumadocs-openapi/server';
|
|
7
|
+
import { Tab, Tabs, TabsList, TabsTrigger } from 'fumadocs-ui/components/tabs';
|
|
8
|
+
import type { ReactNode } from 'react';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_SCHEMA_SOURCE = '/api-reference/openapi-example.json';
|
|
11
|
+
const DEFAULT_PROXY_URL = '/api/proxy';
|
|
12
|
+
const HTTP_METHODS = new Set<HttpMethods>(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace']);
|
|
13
|
+
type SelectorMethod = HttpMethods | 'webhook';
|
|
14
|
+
const SELECTOR_METHODS = new Set<SelectorMethod>([...HTTP_METHODS, 'webhook']);
|
|
15
|
+
type VeluOpenApiLayout = 'full' | 'playground' | 'example';
|
|
16
|
+
|
|
17
|
+
type APIPageRenderer = ReturnType<typeof createAPIPage>;
|
|
18
|
+
|
|
19
|
+
interface OpenApiRenderer {
|
|
20
|
+
renderer: APIPageRenderer;
|
|
21
|
+
document: string;
|
|
22
|
+
server: OpenAPIServer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rendererCache = new Map<string, OpenApiRenderer>();
|
|
26
|
+
|
|
27
|
+
function resolveSchemaSource(rawSource: string): string {
|
|
28
|
+
const source = rawSource.trim();
|
|
29
|
+
if (!source) return resolveSchemaSource(DEFAULT_SCHEMA_SOURCE);
|
|
30
|
+
if (/^https?:\/\//i.test(source) || source.startsWith('file://')) return source;
|
|
31
|
+
if (source.startsWith('/')) {
|
|
32
|
+
const publicPath = join(process.cwd(), 'public', source.replace(/^\/+/, ''));
|
|
33
|
+
if (existsSync(publicPath)) return publicPath;
|
|
34
|
+
return source;
|
|
35
|
+
}
|
|
36
|
+
if (isAbsolute(source)) return source;
|
|
37
|
+
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
const candidates = [resolve(cwd, source)];
|
|
40
|
+
|
|
41
|
+
for (const candidate of candidates) {
|
|
42
|
+
if (existsSync(candidate)) return candidate;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return source;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeMethod(value: unknown): SelectorMethod {
|
|
49
|
+
const lowered = String(value ?? 'get').trim().toLowerCase() as SelectorMethod;
|
|
50
|
+
return SELECTOR_METHODS.has(lowered) ? lowered : 'get';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeSampleId(value: string): string | undefined {
|
|
54
|
+
const normalized = value.trim().toLowerCase();
|
|
55
|
+
if (!normalized) return undefined;
|
|
56
|
+
if (normalized === 'curl') return 'curl';
|
|
57
|
+
if (normalized === 'javascript' || normalized === 'js' || normalized === 'node') return 'js';
|
|
58
|
+
if (normalized === 'python' || normalized === 'py') return 'python';
|
|
59
|
+
if (normalized === 'go' || normalized === 'golang') return 'go';
|
|
60
|
+
if (normalized === 'java') return 'java';
|
|
61
|
+
if (normalized === 'c#' || normalized === 'csharp' || normalized === 'cs' || normalized === 'dotnet') return 'csharp';
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildCodeSampleOverrides(exampleLanguages?: string[], exampleAutogenerate = true) {
|
|
66
|
+
const builtInIds = ['curl', 'js', 'go', 'python', 'java', 'csharp'];
|
|
67
|
+
if (!exampleAutogenerate) {
|
|
68
|
+
return builtInIds.map((id) => createCodeSample({ id, lang: 'text', source: false }));
|
|
69
|
+
}
|
|
70
|
+
if (!exampleLanguages || exampleLanguages.length === 0) return undefined;
|
|
71
|
+
|
|
72
|
+
const selected = new Set<string>();
|
|
73
|
+
for (const entry of exampleLanguages) {
|
|
74
|
+
const id = normalizeSampleId(entry);
|
|
75
|
+
if (id) selected.add(id);
|
|
76
|
+
}
|
|
77
|
+
if (selected.size === 0) {
|
|
78
|
+
return builtInIds.map((id) => createCodeSample({ id, lang: 'text', source: false }));
|
|
79
|
+
}
|
|
80
|
+
return builtInIds
|
|
81
|
+
.filter((id) => !selected.has(id))
|
|
82
|
+
.map((id) => createCodeSample({ id, lang: 'text', source: false }));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface RawCodeSample {
|
|
86
|
+
id?: unknown;
|
|
87
|
+
lang?: unknown;
|
|
88
|
+
label?: unknown;
|
|
89
|
+
source?: unknown;
|
|
90
|
+
serverContext?: unknown;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface NormalizedCodeSample {
|
|
94
|
+
id: string;
|
|
95
|
+
lang: string;
|
|
96
|
+
label?: string;
|
|
97
|
+
source: string;
|
|
98
|
+
serverContext?: unknown;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function slugifySampleId(value: string, fallback: string): string {
|
|
102
|
+
const normalized = value
|
|
103
|
+
.toLowerCase()
|
|
104
|
+
.trim()
|
|
105
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
106
|
+
.replace(/^-+|-+$/g, '');
|
|
107
|
+
return normalized || fallback;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeSdkCodeSamples(method: MethodInformation): ReturnType<typeof createCodeSample>[] {
|
|
111
|
+
const raw = (method as unknown as { 'x-codeSamples'?: unknown })['x-codeSamples'];
|
|
112
|
+
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
113
|
+
|
|
114
|
+
const labelCounts = new Map<string, number>();
|
|
115
|
+
const normalized: NormalizedCodeSample[] = [];
|
|
116
|
+
|
|
117
|
+
raw.forEach((entry, index) => {
|
|
118
|
+
if (!isRecord(entry)) return;
|
|
119
|
+
const sample = entry as RawCodeSample;
|
|
120
|
+
if (typeof sample.source !== 'string' || sample.source.length === 0) return;
|
|
121
|
+
|
|
122
|
+
const rawLang = typeof sample.lang === 'string' && sample.lang.trim().length > 0
|
|
123
|
+
? sample.lang.trim()
|
|
124
|
+
: 'text';
|
|
125
|
+
const label = typeof sample.label === 'string' && sample.label.trim().length > 0
|
|
126
|
+
? sample.label.trim()
|
|
127
|
+
: undefined;
|
|
128
|
+
const rawId = typeof sample.id === 'string' && sample.id.trim().length > 0
|
|
129
|
+
? sample.id.trim()
|
|
130
|
+
: `${rawLang}-${label ?? 'sample'}-${index + 1}`;
|
|
131
|
+
const id = slugifySampleId(rawId, `sample-${index + 1}`);
|
|
132
|
+
|
|
133
|
+
if (label) labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
|
|
134
|
+
normalized.push({
|
|
135
|
+
id,
|
|
136
|
+
lang: rawLang.toLowerCase(),
|
|
137
|
+
label,
|
|
138
|
+
source: sample.source,
|
|
139
|
+
serverContext: sample.serverContext,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (normalized.length === 0) return [];
|
|
144
|
+
|
|
145
|
+
// prevent fumadocs fallback from re-adding raw x-codeSamples and collapsing duplicate language ids
|
|
146
|
+
(method as unknown as { 'x-codeSamples'?: unknown })['x-codeSamples'] = [];
|
|
147
|
+
|
|
148
|
+
return normalized.map((sample) => {
|
|
149
|
+
const duplicateLabelCount = sample.label ? (labelCounts.get(sample.label) ?? 0) : 0;
|
|
150
|
+
const resolvedLabel = sample.label && duplicateLabelCount > 1
|
|
151
|
+
? `${sample.label} (${sample.lang})`
|
|
152
|
+
: sample.label;
|
|
153
|
+
return createCodeSample({
|
|
154
|
+
id: sample.id,
|
|
155
|
+
lang: sample.lang,
|
|
156
|
+
label: resolvedLabel,
|
|
157
|
+
source: sample.source,
|
|
158
|
+
serverContext: sample.serverContext,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildCodeSampleGenerator(exampleLanguages?: string[], exampleAutogenerate = true) {
|
|
164
|
+
const overrides = buildCodeSampleOverrides(exampleLanguages, exampleAutogenerate);
|
|
165
|
+
return async (method: MethodInformation) => {
|
|
166
|
+
const generated: ReturnType<typeof createCodeSample>[] = [
|
|
167
|
+
...normalizeSdkCodeSamples(method),
|
|
168
|
+
];
|
|
169
|
+
if (overrides) generated.push(...overrides);
|
|
170
|
+
return generated;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface OpenApiRenderOptions {
|
|
175
|
+
exampleLanguages?: string[];
|
|
176
|
+
exampleAutogenerate?: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface OpenApiRecord {
|
|
180
|
+
[key: string]: unknown;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface NormalizedField {
|
|
184
|
+
name: string;
|
|
185
|
+
type?: string;
|
|
186
|
+
location?: string;
|
|
187
|
+
required?: boolean;
|
|
188
|
+
deprecated?: boolean;
|
|
189
|
+
defaultValue?: string;
|
|
190
|
+
description?: string;
|
|
191
|
+
depth?: number;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface RichVariant {
|
|
195
|
+
value: string;
|
|
196
|
+
label: string;
|
|
197
|
+
fields: NormalizedField[];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface RichField extends NormalizedField {
|
|
201
|
+
childFields?: NormalizedField[];
|
|
202
|
+
childVariants?: RichVariant[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isRecord(value: unknown): value is OpenApiRecord {
|
|
206
|
+
return typeof value === 'object' && value !== null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function asStringList(value: unknown): string[] {
|
|
210
|
+
return Array.isArray(value) ? value.map(String) : [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function toRecord(value: unknown): OpenApiRecord | undefined {
|
|
214
|
+
return isRecord(value) ? value : undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function toSchema(value: unknown): OpenApiRecord | undefined {
|
|
218
|
+
return isRecord(value) ? value : undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function uniqueFields(fields: NormalizedField[]): NormalizedField[] {
|
|
222
|
+
const seen = new Set<string>();
|
|
223
|
+
const output: NormalizedField[] = [];
|
|
224
|
+
for (const field of fields) {
|
|
225
|
+
const key = `${field.name}::${field.type ?? ''}`;
|
|
226
|
+
if (seen.has(key)) continue;
|
|
227
|
+
seen.add(key);
|
|
228
|
+
output.push(field);
|
|
229
|
+
}
|
|
230
|
+
return output;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resolveSchemaType(schema: OpenApiRecord | undefined): string {
|
|
234
|
+
if (!schema) return 'any';
|
|
235
|
+
|
|
236
|
+
if (typeof schema.$ref === 'string' && schema.$ref) {
|
|
237
|
+
const ref = String(schema.$ref);
|
|
238
|
+
return ref.split('/').pop() ?? ref;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const typeValue = schema.type;
|
|
242
|
+
if (Array.isArray(typeValue) && typeValue.length > 0) return typeValue.map(String).join(' | ');
|
|
243
|
+
|
|
244
|
+
if (typeof typeValue === 'string') {
|
|
245
|
+
if (typeValue === 'array') {
|
|
246
|
+
const itemType = resolveSchemaType(toSchema(schema.items));
|
|
247
|
+
return `${itemType}[]`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const format = typeof schema.format === 'string' && schema.format.trim().length > 0
|
|
251
|
+
? schema.format.trim()
|
|
252
|
+
: '';
|
|
253
|
+
return format ? `${typeValue}<${format}>` : typeValue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) return 'enum';
|
|
257
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) return 'oneOf';
|
|
258
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) return 'anyOf';
|
|
259
|
+
if (Array.isArray(schema.allOf) && schema.allOf.length > 0) return 'allOf';
|
|
260
|
+
if (isRecord(schema.properties)) return 'object';
|
|
261
|
+
|
|
262
|
+
return 'any';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function stringifyDefaultValue(value: unknown): string | undefined {
|
|
266
|
+
if (value == null) return undefined;
|
|
267
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
268
|
+
try {
|
|
269
|
+
return JSON.stringify(value);
|
|
270
|
+
} catch {
|
|
271
|
+
return String(value);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function schemaHasNestedFields(schema: OpenApiRecord | undefined): boolean {
|
|
276
|
+
if (!schema) return false;
|
|
277
|
+
if (isRecord(schema.properties) && Object.keys(schema.properties).length > 0) return true;
|
|
278
|
+
if (Array.isArray(schema.allOf) && schema.allOf.length > 0) return true;
|
|
279
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) return true;
|
|
280
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) return true;
|
|
281
|
+
if (schema.type === 'array') {
|
|
282
|
+
const items = toSchema(schema.items);
|
|
283
|
+
return Boolean(items && (schemaHasNestedFields(items) || isRecord(items.properties)));
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function collectSchemaFields(
|
|
289
|
+
schema: OpenApiRecord | undefined,
|
|
290
|
+
options: { prefix?: string; required?: boolean; depth?: number } = {},
|
|
291
|
+
seen: WeakSet<object> = new WeakSet(),
|
|
292
|
+
): NormalizedField[] {
|
|
293
|
+
if (!schema) return [];
|
|
294
|
+
if (seen.has(schema)) return [];
|
|
295
|
+
seen.add(schema);
|
|
296
|
+
|
|
297
|
+
const compositionParts = [
|
|
298
|
+
...(Array.isArray(schema.allOf) ? schema.allOf : []),
|
|
299
|
+
...(Array.isArray(schema.oneOf) ? schema.oneOf : []),
|
|
300
|
+
...(Array.isArray(schema.anyOf) ? schema.anyOf : []),
|
|
301
|
+
]
|
|
302
|
+
.map((item) => toSchema(item))
|
|
303
|
+
.filter((item): item is OpenApiRecord => Boolean(item));
|
|
304
|
+
if (compositionParts.length > 0) {
|
|
305
|
+
const composed = compositionParts.flatMap((part) => collectSchemaFields(part, options, seen));
|
|
306
|
+
if (composed.length > 0) return composed;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const depth = options.depth ?? 0;
|
|
310
|
+
const fields: NormalizedField[] = [];
|
|
311
|
+
const properties = toRecord(schema.properties);
|
|
312
|
+
const requiredSet = new Set(asStringList(schema.required));
|
|
313
|
+
|
|
314
|
+
if (properties) {
|
|
315
|
+
for (const [rawName, rawFieldSchema] of Object.entries(properties)) {
|
|
316
|
+
const fieldSchema = toSchema(rawFieldSchema);
|
|
317
|
+
const fieldName = options.prefix ? `${options.prefix}.${rawName}` : rawName;
|
|
318
|
+
const childRequired = requiredSet.has(rawName);
|
|
319
|
+
fields.push({
|
|
320
|
+
name: fieldName,
|
|
321
|
+
type: resolveSchemaType(fieldSchema),
|
|
322
|
+
required: childRequired,
|
|
323
|
+
deprecated: Boolean(fieldSchema?.deprecated),
|
|
324
|
+
defaultValue: stringifyDefaultValue(fieldSchema?.default),
|
|
325
|
+
description: typeof fieldSchema?.description === 'string' ? fieldSchema.description : undefined,
|
|
326
|
+
depth,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (schemaHasNestedFields(fieldSchema)) {
|
|
330
|
+
if (fieldSchema?.type === 'array') {
|
|
331
|
+
const itemSchema = toSchema(fieldSchema.items);
|
|
332
|
+
fields.push(...collectSchemaFields(itemSchema, {
|
|
333
|
+
prefix: `${fieldName}[]`,
|
|
334
|
+
required: childRequired,
|
|
335
|
+
depth: depth + 1,
|
|
336
|
+
}, seen));
|
|
337
|
+
} else {
|
|
338
|
+
fields.push(...collectSchemaFields(fieldSchema, {
|
|
339
|
+
prefix: fieldName,
|
|
340
|
+
required: childRequired,
|
|
341
|
+
depth: depth + 1,
|
|
342
|
+
}, seen));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return fields;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (schema.type === 'array') {
|
|
350
|
+
const itemSchema = toSchema(schema.items);
|
|
351
|
+
const itemName = options.prefix ? `${options.prefix}[]` : 'items[]';
|
|
352
|
+
if (itemSchema) {
|
|
353
|
+
fields.push({
|
|
354
|
+
name: itemName,
|
|
355
|
+
type: resolveSchemaType(itemSchema),
|
|
356
|
+
required: options.required,
|
|
357
|
+
deprecated: Boolean(itemSchema.deprecated),
|
|
358
|
+
defaultValue: stringifyDefaultValue(itemSchema.default),
|
|
359
|
+
description: typeof itemSchema.description === 'string' ? itemSchema.description : undefined,
|
|
360
|
+
depth,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
if (schemaHasNestedFields(itemSchema)) {
|
|
364
|
+
fields.push(...collectSchemaFields(itemSchema, {
|
|
365
|
+
prefix: itemName,
|
|
366
|
+
required: options.required,
|
|
367
|
+
depth: depth + 1,
|
|
368
|
+
}, seen));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return fields;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function collectResponseFields(
|
|
377
|
+
schema: OpenApiRecord | undefined,
|
|
378
|
+
options: { prefix?: string; depth?: number } = {},
|
|
379
|
+
seen: WeakSet<object> = new WeakSet(),
|
|
380
|
+
): NormalizedField[] {
|
|
381
|
+
if (!schema) return [];
|
|
382
|
+
if (seen.has(schema)) return [];
|
|
383
|
+
seen.add(schema);
|
|
384
|
+
|
|
385
|
+
const compositionParts = [
|
|
386
|
+
...(Array.isArray(schema.allOf) ? schema.allOf : []),
|
|
387
|
+
...(Array.isArray(schema.oneOf) ? schema.oneOf : []),
|
|
388
|
+
...(Array.isArray(schema.anyOf) ? schema.anyOf : []),
|
|
389
|
+
]
|
|
390
|
+
.map((item) => toSchema(item))
|
|
391
|
+
.filter((item): item is OpenApiRecord => Boolean(item));
|
|
392
|
+
if (compositionParts.length > 0) {
|
|
393
|
+
const composed = compositionParts.flatMap((part) => collectResponseFields(part, options, seen));
|
|
394
|
+
if (composed.length > 0) return composed;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const depth = options.depth ?? 0;
|
|
398
|
+
const prefix = options.prefix;
|
|
399
|
+
const properties = toRecord(schema.properties);
|
|
400
|
+
const requiredSet = new Set(asStringList(schema.required));
|
|
401
|
+
const fields: NormalizedField[] = [];
|
|
402
|
+
|
|
403
|
+
if (properties && Object.keys(properties).length > 0) {
|
|
404
|
+
for (const [rawName, rawChild] of Object.entries(properties)) {
|
|
405
|
+
const child = toSchema(rawChild);
|
|
406
|
+
const fieldName = prefix ? `${prefix}.${rawName}` : rawName;
|
|
407
|
+
const childRequired = requiredSet.has(rawName);
|
|
408
|
+
fields.push({
|
|
409
|
+
name: fieldName,
|
|
410
|
+
type: resolveSchemaType(child),
|
|
411
|
+
required: childRequired,
|
|
412
|
+
deprecated: Boolean(child?.deprecated),
|
|
413
|
+
defaultValue: stringifyDefaultValue(child?.default),
|
|
414
|
+
description: typeof child?.description === 'string' ? child.description : undefined,
|
|
415
|
+
depth,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (child?.type === 'array') {
|
|
419
|
+
const itemSchema = toSchema(child.items);
|
|
420
|
+
if (itemSchema && (isRecord(itemSchema.properties) || itemSchema.type === 'array')) {
|
|
421
|
+
fields.push(...collectResponseFields(itemSchema, { prefix: fieldName, depth: depth + 1 }, seen));
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (child && isRecord(child.properties)) {
|
|
427
|
+
fields.push(...collectResponseFields(child, { prefix: fieldName, depth: depth + 1 }, seen));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return fields;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (schema.type === 'array') {
|
|
434
|
+
const itemSchema = toSchema(schema.items);
|
|
435
|
+
return collectResponseFields(itemSchema, { prefix, depth }, seen);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return fields;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function richVariantFields(schema: OpenApiRecord | undefined): NormalizedField[] {
|
|
442
|
+
if (!schema) return [];
|
|
443
|
+
const fields = collectSchemaFields(schema);
|
|
444
|
+
return uniqueFields(fields);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function buildRichField(name: string, schema: OpenApiRecord | undefined, required = false): RichField {
|
|
448
|
+
const field: RichField = {
|
|
449
|
+
name,
|
|
450
|
+
type: resolveSchemaType(schema),
|
|
451
|
+
required,
|
|
452
|
+
deprecated: Boolean(schema?.deprecated),
|
|
453
|
+
defaultValue: stringifyDefaultValue(schema?.default),
|
|
454
|
+
description: typeof schema?.description === 'string' ? schema.description : undefined,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
if (!schema) return field;
|
|
458
|
+
|
|
459
|
+
const oneOf = Array.isArray(schema.oneOf) ? schema.oneOf : [];
|
|
460
|
+
const anyOf = Array.isArray(schema.anyOf) ? schema.anyOf : [];
|
|
461
|
+
if (oneOf.length > 0 || anyOf.length > 0) {
|
|
462
|
+
const variants = (oneOf.length > 0 ? oneOf : anyOf)
|
|
463
|
+
.map((item, index) => {
|
|
464
|
+
const subSchema = toSchema(item);
|
|
465
|
+
if (!subSchema) return null;
|
|
466
|
+
const label = typeof subSchema.title === 'string' && subSchema.title.trim().length > 0
|
|
467
|
+
? subSchema.title.trim()
|
|
468
|
+
: `Option ${index + 1}`;
|
|
469
|
+
const fields = richVariantFields(subSchema);
|
|
470
|
+
return {
|
|
471
|
+
value: `variant-${index + 1}`,
|
|
472
|
+
label,
|
|
473
|
+
fields,
|
|
474
|
+
} satisfies RichVariant;
|
|
475
|
+
})
|
|
476
|
+
.filter((variant): variant is RichVariant => Boolean(variant));
|
|
477
|
+
if (variants.length > 0) field.childVariants = variants;
|
|
478
|
+
return field;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const allOf = Array.isArray(schema.allOf) ? schema.allOf : [];
|
|
482
|
+
if (allOf.length > 0) {
|
|
483
|
+
const merged = uniqueFields(
|
|
484
|
+
allOf.flatMap((item) => richVariantFields(toSchema(item))),
|
|
485
|
+
);
|
|
486
|
+
if (merged.length > 0) field.childFields = merged;
|
|
487
|
+
return field;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (schema.type === 'array') {
|
|
491
|
+
const itemSchema = toSchema(schema.items);
|
|
492
|
+
const childFields = richVariantFields(itemSchema);
|
|
493
|
+
if (childFields.length > 0) field.childFields = childFields;
|
|
494
|
+
return field;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (isRecord(schema.properties)) {
|
|
498
|
+
const childFields = uniqueFields(collectSchemaFields(schema));
|
|
499
|
+
if (childFields.length > 0) field.childFields = childFields;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return field;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function buildRichFieldsFromSchema(schema: OpenApiRecord | undefined, requiredFallback = false): RichField[] {
|
|
506
|
+
if (!schema) return [];
|
|
507
|
+
|
|
508
|
+
const rootOneOf = Array.isArray(schema.oneOf) ? schema.oneOf : [];
|
|
509
|
+
const rootAnyOf = Array.isArray(schema.anyOf) ? schema.anyOf : [];
|
|
510
|
+
if (rootOneOf.length > 0 || rootAnyOf.length > 0) {
|
|
511
|
+
const variants = (rootOneOf.length > 0 ? rootOneOf : rootAnyOf)
|
|
512
|
+
.map((item, index) => {
|
|
513
|
+
const subSchema = toSchema(item);
|
|
514
|
+
if (!subSchema) return null;
|
|
515
|
+
const label = typeof subSchema.title === 'string' && subSchema.title.trim().length > 0
|
|
516
|
+
? subSchema.title.trim()
|
|
517
|
+
: `Option ${index + 1}`;
|
|
518
|
+
return {
|
|
519
|
+
value: `variant-${index + 1}`,
|
|
520
|
+
label,
|
|
521
|
+
fields: richVariantFields(subSchema),
|
|
522
|
+
} satisfies RichVariant;
|
|
523
|
+
})
|
|
524
|
+
.filter((variant): variant is RichVariant => Boolean(variant));
|
|
525
|
+
if (variants.length > 0) {
|
|
526
|
+
return [{
|
|
527
|
+
name: 'value',
|
|
528
|
+
type: resolveSchemaType(schema),
|
|
529
|
+
required: requiredFallback,
|
|
530
|
+
deprecated: Boolean(schema.deprecated),
|
|
531
|
+
defaultValue: stringifyDefaultValue(schema.default),
|
|
532
|
+
description: typeof schema.description === 'string' ? schema.description : undefined,
|
|
533
|
+
childVariants: variants,
|
|
534
|
+
}];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const rootAllOf = Array.isArray(schema.allOf) ? schema.allOf : [];
|
|
539
|
+
if (rootAllOf.length > 0) {
|
|
540
|
+
const merged = uniqueFields(
|
|
541
|
+
rootAllOf.flatMap((item) => collectSchemaFields(toSchema(item), { required: requiredFallback })),
|
|
542
|
+
);
|
|
543
|
+
if (merged.length > 0) return merged;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const properties = toRecord(schema.properties);
|
|
547
|
+
const requiredSet = new Set(asStringList(schema.required));
|
|
548
|
+
if (properties && Object.keys(properties).length > 0) {
|
|
549
|
+
const entries = Object.entries(properties).map(([name, value]) => {
|
|
550
|
+
const childSchema = toSchema(value);
|
|
551
|
+
return buildRichField(name, childSchema, requiredSet.has(name));
|
|
552
|
+
});
|
|
553
|
+
return entries;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (schema.type === 'array') {
|
|
557
|
+
const itemSchema = toSchema(schema.items);
|
|
558
|
+
if (itemSchema && isRecord(itemSchema.properties)) {
|
|
559
|
+
const fields = buildRichFieldsFromSchema(itemSchema, requiredFallback);
|
|
560
|
+
if (fields.length > 0) return fields;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return [{
|
|
565
|
+
name: 'value',
|
|
566
|
+
type: resolveSchemaType(schema),
|
|
567
|
+
required: requiredFallback,
|
|
568
|
+
deprecated: Boolean(schema.deprecated),
|
|
569
|
+
defaultValue: stringifyDefaultValue(schema.default),
|
|
570
|
+
description: typeof schema.description === 'string' ? schema.description : undefined,
|
|
571
|
+
}];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function renderAnchor(anchorId: string, label: string): ReactNode {
|
|
575
|
+
return (
|
|
576
|
+
<a className="velu-param-anchor" href={`#${anchorId}`} aria-label={`Anchor for ${label}`}>
|
|
577
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
578
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
|
579
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
|
580
|
+
</svg>
|
|
581
|
+
</a>
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function fieldAnchorId(prefix: string, name: string): string {
|
|
586
|
+
const clean = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
587
|
+
return `${prefix}-${clean || 'field'}`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function renderParamField(field: NormalizedField, ctx: RenderContext, location: string): ReactNode {
|
|
591
|
+
const anchorId = fieldAnchorId(`param-${location}`, field.name);
|
|
592
|
+
const depthClass = field.depth && field.depth > 0 ? `velu-openapi-field-depth-${Math.min(field.depth, 4)}` : '';
|
|
593
|
+
return (
|
|
594
|
+
<section id={anchorId} className={['velu-param-field-item', depthClass].filter(Boolean).join(' ')}>
|
|
595
|
+
<div className="velu-param-head">
|
|
596
|
+
{renderAnchor(anchorId, field.name)}
|
|
597
|
+
<code>{field.name}</code>
|
|
598
|
+
{field.type ? <span className="velu-pill velu-pill-type">{field.type}</span> : null}
|
|
599
|
+
{field.location ? <span className="velu-pill velu-pill-type">{field.location}</span> : null}
|
|
600
|
+
{field.required ? <span className="velu-pill velu-pill-required">required</span> : null}
|
|
601
|
+
{field.deprecated ? <span className="velu-pill velu-pill-deprecated">deprecated</span> : null}
|
|
602
|
+
{field.defaultValue ? <em>default: {field.defaultValue}</em> : null}
|
|
603
|
+
</div>
|
|
604
|
+
{field.description ? <div className="velu-param-body">{ctx.renderMarkdown(field.description)}</div> : null}
|
|
605
|
+
</section>
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function renderResponseField(field: NormalizedField, ctx: RenderContext, suffix: string): ReactNode {
|
|
610
|
+
const anchorId = fieldAnchorId(`response-${suffix}`, field.name);
|
|
611
|
+
const depthClass = field.depth && field.depth > 0 ? `velu-openapi-field-depth-${Math.min(field.depth, 4)}` : '';
|
|
612
|
+
return (
|
|
613
|
+
<section id={anchorId} className={['velu-response-field-item', depthClass].filter(Boolean).join(' ')}>
|
|
614
|
+
<div className="velu-property-head">
|
|
615
|
+
{renderAnchor(anchorId, field.name)}
|
|
616
|
+
<code>{field.name}</code>
|
|
617
|
+
{field.type ? <span className="velu-pill velu-pill-type">{field.type}</span> : null}
|
|
618
|
+
{field.required ? <span className="velu-pill velu-pill-required">required</span> : null}
|
|
619
|
+
{field.deprecated ? <span className="velu-pill velu-pill-deprecated">deprecated</span> : null}
|
|
620
|
+
{field.defaultValue ? <em>default: {field.defaultValue}</em> : null}
|
|
621
|
+
</div>
|
|
622
|
+
{field.description ? <div className="velu-property-body">{ctx.renderMarkdown(field.description)}</div> : null}
|
|
623
|
+
</section>
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function renderParamFieldWithChildren(field: RichField, ctx: RenderContext, location: string): ReactNode {
|
|
628
|
+
const hasChildren = (field.childFields?.length ?? 0) > 0 || (field.childVariants?.length ?? 0) > 0;
|
|
629
|
+
return (
|
|
630
|
+
<div className="velu-openapi-complex-field">
|
|
631
|
+
{renderParamField(field, ctx, location)}
|
|
632
|
+
{hasChildren ? (
|
|
633
|
+
<details className="velu-openapi-child-attrs" open>
|
|
634
|
+
<summary className="velu-openapi-child-attrs-summary">Hide child attributes</summary>
|
|
635
|
+
{field.childVariants && field.childVariants.length > 0 ? (
|
|
636
|
+
<Tabs
|
|
637
|
+
defaultValue={field.childVariants[0]?.value}
|
|
638
|
+
className="velu-openapi-child-variants !border-0 !rounded-none !bg-transparent !my-0 !overflow-visible"
|
|
639
|
+
>
|
|
640
|
+
<TabsList className="velu-openapi-child-variant-list !px-0 !gap-1 !overflow-visible">
|
|
641
|
+
{field.childVariants.map((variant) => (
|
|
642
|
+
<TabsTrigger
|
|
643
|
+
key={`${location}-${field.name}-${variant.value}`}
|
|
644
|
+
value={variant.value}
|
|
645
|
+
className="velu-openapi-child-variant-trigger"
|
|
646
|
+
>
|
|
647
|
+
{variant.label}
|
|
648
|
+
</TabsTrigger>
|
|
649
|
+
))}
|
|
650
|
+
</TabsList>
|
|
651
|
+
{field.childVariants.map((variant) => (
|
|
652
|
+
<Tab
|
|
653
|
+
key={`${location}-${field.name}-${variant.value}-panel`}
|
|
654
|
+
value={variant.value}
|
|
655
|
+
className="velu-openapi-response-panel !p-0 !bg-transparent !rounded-none"
|
|
656
|
+
>
|
|
657
|
+
<div className="velu-openapi-field-list">
|
|
658
|
+
{variant.fields.map((childField) => (
|
|
659
|
+
<div key={`${location}-${field.name}-${variant.value}-${childField.name}`}>
|
|
660
|
+
{renderParamField(childField, ctx, `${location}-${field.name}-${variant.value}`)}
|
|
661
|
+
</div>
|
|
662
|
+
))}
|
|
663
|
+
</div>
|
|
664
|
+
</Tab>
|
|
665
|
+
))}
|
|
666
|
+
</Tabs>
|
|
667
|
+
) : (
|
|
668
|
+
<div className="velu-openapi-field-list">
|
|
669
|
+
{(field.childFields ?? []).map((childField) => (
|
|
670
|
+
<div key={`${location}-${field.name}-${childField.name}`}>
|
|
671
|
+
{renderParamField(childField, ctx, `${location}-${field.name}`)}
|
|
672
|
+
</div>
|
|
673
|
+
))}
|
|
674
|
+
</div>
|
|
675
|
+
)}
|
|
676
|
+
</details>
|
|
677
|
+
) : null}
|
|
678
|
+
</div>
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function renderResponseFieldWithChildren(field: RichField, ctx: RenderContext, suffix: string): ReactNode {
|
|
683
|
+
const hasChildren = (field.childFields?.length ?? 0) > 0 || (field.childVariants?.length ?? 0) > 0;
|
|
684
|
+
return (
|
|
685
|
+
<div className="velu-openapi-complex-field">
|
|
686
|
+
{renderResponseField(field, ctx, suffix)}
|
|
687
|
+
{hasChildren ? (
|
|
688
|
+
<details className="velu-openapi-child-attrs" open>
|
|
689
|
+
<summary className="velu-openapi-child-attrs-summary">Hide child attributes</summary>
|
|
690
|
+
{field.childVariants && field.childVariants.length > 0 ? (
|
|
691
|
+
<Tabs
|
|
692
|
+
defaultValue={field.childVariants[0]?.value}
|
|
693
|
+
className="velu-openapi-child-variants !border-0 !rounded-none !bg-transparent !my-0 !overflow-visible"
|
|
694
|
+
>
|
|
695
|
+
<TabsList className="velu-openapi-child-variant-list !px-0 !gap-1 !overflow-visible">
|
|
696
|
+
{field.childVariants.map((variant) => (
|
|
697
|
+
<TabsTrigger
|
|
698
|
+
key={`${suffix}-${field.name}-${variant.value}`}
|
|
699
|
+
value={variant.value}
|
|
700
|
+
className="velu-openapi-child-variant-trigger"
|
|
701
|
+
>
|
|
702
|
+
{variant.label}
|
|
703
|
+
</TabsTrigger>
|
|
704
|
+
))}
|
|
705
|
+
</TabsList>
|
|
706
|
+
{field.childVariants.map((variant) => (
|
|
707
|
+
<Tab
|
|
708
|
+
key={`${suffix}-${field.name}-${variant.value}-panel`}
|
|
709
|
+
value={variant.value}
|
|
710
|
+
className="velu-openapi-response-panel !p-0 !bg-transparent !rounded-none"
|
|
711
|
+
>
|
|
712
|
+
<div className="velu-openapi-field-list">
|
|
713
|
+
{variant.fields.map((childField) => (
|
|
714
|
+
<div key={`${suffix}-${field.name}-${variant.value}-${childField.name}`}>
|
|
715
|
+
{renderResponseField(childField, ctx, `${suffix}-${field.name}-${variant.value}`)}
|
|
716
|
+
</div>
|
|
717
|
+
))}
|
|
718
|
+
</div>
|
|
719
|
+
</Tab>
|
|
720
|
+
))}
|
|
721
|
+
</Tabs>
|
|
722
|
+
) : (
|
|
723
|
+
<div className="velu-openapi-field-list">
|
|
724
|
+
{(field.childFields ?? []).map((childField) => (
|
|
725
|
+
<div key={`${suffix}-${field.name}-${childField.name}`}>
|
|
726
|
+
{renderResponseField(childField, ctx, `${suffix}-${field.name}`)}
|
|
727
|
+
</div>
|
|
728
|
+
))}
|
|
729
|
+
</div>
|
|
730
|
+
)}
|
|
731
|
+
</details>
|
|
732
|
+
) : null}
|
|
733
|
+
</div>
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function parameterSchema(parameter: OpenApiRecord): OpenApiRecord | undefined {
|
|
738
|
+
const direct = toSchema(parameter.schema);
|
|
739
|
+
if (direct) return direct;
|
|
740
|
+
|
|
741
|
+
const content = toRecord(parameter.content);
|
|
742
|
+
if (!content) return undefined;
|
|
743
|
+
for (const rawMedia of Object.values(content)) {
|
|
744
|
+
const media = toRecord(rawMedia);
|
|
745
|
+
const schema = toSchema(media?.schema);
|
|
746
|
+
if (schema) return schema;
|
|
747
|
+
}
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function headerObjectSchema(headerObject: OpenApiRecord): OpenApiRecord | undefined {
|
|
752
|
+
const direct = toSchema(headerObject.schema);
|
|
753
|
+
if (direct) return direct;
|
|
754
|
+
|
|
755
|
+
const content = toRecord(headerObject.content);
|
|
756
|
+
if (!content) return undefined;
|
|
757
|
+
for (const rawMedia of Object.values(content)) {
|
|
758
|
+
const media = toRecord(rawMedia);
|
|
759
|
+
const schema = toSchema(media?.schema);
|
|
760
|
+
if (schema) return schema;
|
|
761
|
+
}
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function collectResponseHeaderFields(response: OpenApiRecord): NormalizedField[] {
|
|
766
|
+
const headers = toRecord(response.headers);
|
|
767
|
+
if (!headers) return [];
|
|
768
|
+
|
|
769
|
+
const fields: NormalizedField[] = [];
|
|
770
|
+
for (const [headerName, rawHeader] of Object.entries(headers)) {
|
|
771
|
+
const headerObject = toRecord(rawHeader);
|
|
772
|
+
if (!headerObject) continue;
|
|
773
|
+
|
|
774
|
+
const schema = headerObjectSchema(headerObject);
|
|
775
|
+
const description = typeof headerObject.description === 'string'
|
|
776
|
+
? headerObject.description
|
|
777
|
+
: (typeof schema?.description === 'string' ? schema.description : undefined);
|
|
778
|
+
|
|
779
|
+
fields.push({
|
|
780
|
+
name: headerName,
|
|
781
|
+
type: resolveSchemaType(schema),
|
|
782
|
+
required: Boolean(headerObject.required),
|
|
783
|
+
deprecated: Boolean(headerObject.deprecated ?? schema?.deprecated),
|
|
784
|
+
defaultValue: stringifyDefaultValue(schema?.default),
|
|
785
|
+
description,
|
|
786
|
+
} satisfies NormalizedField);
|
|
787
|
+
}
|
|
788
|
+
return fields;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function renderAuthorizationSection(method: MethodInformation, ctx: RenderContext): ReactNode {
|
|
792
|
+
const schemaRoot = toRecord((ctx.schema as unknown as OpenApiRecord)?.dereferenced);
|
|
793
|
+
const securitySchemes = toRecord(toRecord(schemaRoot?.components)?.securitySchemes);
|
|
794
|
+
const securityEntries = Array.isArray(method.security) && method.security.length > 0
|
|
795
|
+
? method.security
|
|
796
|
+
: (Array.isArray(schemaRoot?.security) ? schemaRoot.security : []);
|
|
797
|
+
|
|
798
|
+
const fields: Array<NormalizedField & { location: string }> = [];
|
|
799
|
+
for (const rawSecurity of securityEntries) {
|
|
800
|
+
const security = toRecord(rawSecurity);
|
|
801
|
+
if (!security) continue;
|
|
802
|
+
|
|
803
|
+
for (const [schemeId, rawScopes] of Object.entries(security)) {
|
|
804
|
+
const scheme = toRecord(securitySchemes?.[schemeId]);
|
|
805
|
+
if (!scheme) continue;
|
|
806
|
+
|
|
807
|
+
const scopes = asStringList(rawScopes);
|
|
808
|
+
const schemeDescription = typeof scheme.description === 'string' ? scheme.description : '';
|
|
809
|
+
const scopeDescription = scopes.length > 0 ? `Scopes: ${scopes.join(', ')}` : '';
|
|
810
|
+
const combinedDescription = [schemeDescription, scopeDescription].filter(Boolean).join('\n\n');
|
|
811
|
+
const deprecated = Boolean(scheme.deprecated);
|
|
812
|
+
|
|
813
|
+
if (scheme.type === 'apiKey') {
|
|
814
|
+
const inLocation = typeof scheme.in === 'string' ? scheme.in : 'header';
|
|
815
|
+
const fallbackDescription = `API key sent via \`${inLocation}\` as \`${typeof scheme.name === 'string' && scheme.name ? scheme.name : schemeId}\`.`;
|
|
816
|
+
fields.push({
|
|
817
|
+
location: inLocation,
|
|
818
|
+
name: typeof scheme.name === 'string' && scheme.name ? scheme.name : schemeId,
|
|
819
|
+
type: 'string',
|
|
820
|
+
required: true,
|
|
821
|
+
deprecated,
|
|
822
|
+
description: combinedDescription || fallbackDescription,
|
|
823
|
+
});
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (scheme.type === 'http') {
|
|
828
|
+
const httpScheme = typeof scheme.scheme === 'string' ? scheme.scheme.toLowerCase() : '';
|
|
829
|
+
const fallbackDescription = httpScheme === 'basic'
|
|
830
|
+
? 'Basic authentication header with base64-encoded credentials.'
|
|
831
|
+
: 'Bearer authentication header of the form `Bearer <token>`, where `<token>` is your auth token.';
|
|
832
|
+
fields.push({
|
|
833
|
+
location: 'header',
|
|
834
|
+
name: 'Authorization',
|
|
835
|
+
type: 'string',
|
|
836
|
+
required: true,
|
|
837
|
+
deprecated,
|
|
838
|
+
description: combinedDescription || fallbackDescription,
|
|
839
|
+
});
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (scheme.type === 'oauth2' || scheme.type === 'openIdConnect') {
|
|
844
|
+
fields.push({
|
|
845
|
+
location: 'header',
|
|
846
|
+
name: 'Authorization',
|
|
847
|
+
type: 'string',
|
|
848
|
+
required: true,
|
|
849
|
+
deprecated,
|
|
850
|
+
description: combinedDescription || 'Bearer authentication header of the form `Bearer <token>`, where `<token>` is your auth token.',
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (fields.length === 0) return null;
|
|
857
|
+
|
|
858
|
+
return (
|
|
859
|
+
<section className="velu-openapi-section velu-openapi-parameter-group velu-openapi-auth-section">
|
|
860
|
+
{ctx.renderHeading(2, 'Authorizations')}
|
|
861
|
+
<div className="velu-openapi-field-list">
|
|
862
|
+
{fields.map((field, index) => (
|
|
863
|
+
<div key={`${field.location}-${field.name}-${index}`}>
|
|
864
|
+
{renderParamField(field, ctx, `auth-${field.location}`)}
|
|
865
|
+
</div>
|
|
866
|
+
))}
|
|
867
|
+
</div>
|
|
868
|
+
</section>
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function renderParameterSections(method: MethodInformation, ctx: RenderContext): ReactNode {
|
|
873
|
+
const parameters = Array.isArray(method.parameters) ? method.parameters : [];
|
|
874
|
+
if (parameters.length === 0) return null;
|
|
875
|
+
|
|
876
|
+
const sections: Array<{ title: string; location: string; fields: NormalizedField[] }> = [];
|
|
877
|
+
const order: Array<{ key: string; title: string }> = [
|
|
878
|
+
{ key: 'path', title: 'Path Parameters' },
|
|
879
|
+
{ key: 'query', title: 'Query Parameters' },
|
|
880
|
+
{ key: 'header', title: 'Request Headers' },
|
|
881
|
+
{ key: 'cookie', title: 'Cookie Parameters' },
|
|
882
|
+
];
|
|
883
|
+
|
|
884
|
+
for (const item of order) {
|
|
885
|
+
const fields = parameters
|
|
886
|
+
.map((raw) => toRecord(raw))
|
|
887
|
+
.filter((parameter): parameter is OpenApiRecord => Boolean(parameter && parameter.in === item.key))
|
|
888
|
+
.map((parameter) => {
|
|
889
|
+
const schema = parameterSchema(parameter);
|
|
890
|
+
const description = typeof parameter.description === 'string'
|
|
891
|
+
? parameter.description
|
|
892
|
+
: (typeof schema?.description === 'string' ? schema.description : undefined);
|
|
893
|
+
return {
|
|
894
|
+
name: typeof parameter.name === 'string' ? parameter.name : item.key,
|
|
895
|
+
type: resolveSchemaType(schema),
|
|
896
|
+
required: Boolean(parameter.required),
|
|
897
|
+
deprecated: Boolean(parameter.deprecated ?? schema?.deprecated),
|
|
898
|
+
defaultValue: stringifyDefaultValue(schema?.default),
|
|
899
|
+
description,
|
|
900
|
+
} satisfies NormalizedField;
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
if (fields.length > 0) {
|
|
904
|
+
sections.push({ title: item.title, location: item.key, fields });
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (sections.length === 0) return null;
|
|
909
|
+
|
|
910
|
+
return (
|
|
911
|
+
<section className="velu-openapi-section">
|
|
912
|
+
{sections.map((section) => (
|
|
913
|
+
<div key={section.location} className="velu-openapi-field-group velu-openapi-parameter-group">
|
|
914
|
+
{ctx.renderHeading(2, section.title)}
|
|
915
|
+
<div className="velu-openapi-field-list">
|
|
916
|
+
{section.fields.map((field) => (
|
|
917
|
+
<div key={`${section.location}-${field.name}`}>
|
|
918
|
+
{renderParamField(field, ctx, section.location)}
|
|
919
|
+
</div>
|
|
920
|
+
))}
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
))}
|
|
924
|
+
</section>
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function renderRequestBodySection(method: MethodInformation, ctx: RenderContext): ReactNode {
|
|
929
|
+
const requestBody = toRecord(method.requestBody);
|
|
930
|
+
const content = toRecord(requestBody?.content);
|
|
931
|
+
if (!content || Object.keys(content).length === 0) return null;
|
|
932
|
+
|
|
933
|
+
const mediaSections: Array<{ mediaType: string; fields: RichField[]; description?: string }> = [];
|
|
934
|
+
for (const [mediaType, rawMedia] of Object.entries(content)) {
|
|
935
|
+
const media = toRecord(rawMedia);
|
|
936
|
+
const schema = toSchema(media?.schema);
|
|
937
|
+
if (!schema) continue;
|
|
938
|
+
const fields = buildRichFieldsFromSchema(schema, Boolean(requestBody?.required));
|
|
939
|
+
mediaSections.push({
|
|
940
|
+
mediaType,
|
|
941
|
+
fields,
|
|
942
|
+
description: typeof requestBody?.description === 'string' ? requestBody.description : undefined,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (mediaSections.length === 0) return null;
|
|
947
|
+
|
|
948
|
+
const orderedMediaSections = mediaSections.map((section) => {
|
|
949
|
+
const withIndex = section.fields.map((field, index) => ({ field, index }));
|
|
950
|
+
withIndex.sort((a, b) => {
|
|
951
|
+
const aRequired = a.field.required ? 1 : 0;
|
|
952
|
+
const bRequired = b.field.required ? 1 : 0;
|
|
953
|
+
if (aRequired !== bRequired) return bRequired - aRequired;
|
|
954
|
+
return a.index - b.index;
|
|
955
|
+
});
|
|
956
|
+
return {
|
|
957
|
+
...section,
|
|
958
|
+
fields: withIndex.map((item) => item.field),
|
|
959
|
+
};
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
return (
|
|
963
|
+
<section className="velu-openapi-section velu-openapi-parameter-group velu-openapi-body-section">
|
|
964
|
+
{orderedMediaSections.length === 1 ? (
|
|
965
|
+
<>
|
|
966
|
+
<div className="velu-openapi-body-header">
|
|
967
|
+
{ctx.renderHeading(2, 'Body')}
|
|
968
|
+
<span className="velu-openapi-body-content-type">{orderedMediaSections[0].mediaType}</span>
|
|
969
|
+
</div>
|
|
970
|
+
{orderedMediaSections[0].description ? (
|
|
971
|
+
<div className="velu-openapi-field-description">{ctx.renderMarkdown(orderedMediaSections[0].description)}</div>
|
|
972
|
+
) : null}
|
|
973
|
+
<div className="velu-openapi-field-list">
|
|
974
|
+
{orderedMediaSections[0].fields.map((field) => (
|
|
975
|
+
<div key={`${orderedMediaSections[0].mediaType}-${field.name}`}>
|
|
976
|
+
{renderParamFieldWithChildren(field, ctx, 'body')}
|
|
977
|
+
</div>
|
|
978
|
+
))}
|
|
979
|
+
</div>
|
|
980
|
+
</>
|
|
981
|
+
) : (
|
|
982
|
+
<Tabs defaultValue={orderedMediaSections[0]?.mediaType} className="velu-openapi-body-tabs !border-0 !rounded-none !bg-transparent !my-0 !overflow-visible">
|
|
983
|
+
<div className="velu-openapi-body-header">
|
|
984
|
+
{ctx.renderHeading(2, 'Body')}
|
|
985
|
+
<TabsList className="velu-openapi-body-media-list !px-0 !gap-1 !overflow-visible">
|
|
986
|
+
{orderedMediaSections.map((mediaSection) => (
|
|
987
|
+
<TabsTrigger
|
|
988
|
+
key={`body-media-${mediaSection.mediaType}`}
|
|
989
|
+
value={mediaSection.mediaType}
|
|
990
|
+
className="velu-openapi-body-media-trigger"
|
|
991
|
+
>
|
|
992
|
+
{mediaSection.mediaType}
|
|
993
|
+
</TabsTrigger>
|
|
994
|
+
))}
|
|
995
|
+
</TabsList>
|
|
996
|
+
</div>
|
|
997
|
+
{orderedMediaSections.map((mediaSection) => (
|
|
998
|
+
<Tab
|
|
999
|
+
key={`body-panel-${mediaSection.mediaType}`}
|
|
1000
|
+
value={mediaSection.mediaType}
|
|
1001
|
+
className="velu-openapi-response-panel !p-0 !bg-transparent !rounded-none"
|
|
1002
|
+
>
|
|
1003
|
+
{mediaSection.description ? <div className="velu-openapi-field-description">{ctx.renderMarkdown(mediaSection.description)}</div> : null}
|
|
1004
|
+
<div className="velu-openapi-field-list">
|
|
1005
|
+
{mediaSection.fields.map((field) => (
|
|
1006
|
+
<div key={`${mediaSection.mediaType}-${field.name}`}>
|
|
1007
|
+
{renderParamFieldWithChildren(field, ctx, 'body')}
|
|
1008
|
+
</div>
|
|
1009
|
+
))}
|
|
1010
|
+
</div>
|
|
1011
|
+
</Tab>
|
|
1012
|
+
))}
|
|
1013
|
+
</Tabs>
|
|
1014
|
+
)}
|
|
1015
|
+
</section>
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
interface ResponseExampleEntry {
|
|
1020
|
+
tabValue: string;
|
|
1021
|
+
label: string;
|
|
1022
|
+
description?: string;
|
|
1023
|
+
code: string;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
interface ResponseMediaEntry {
|
|
1027
|
+
mediaType?: string;
|
|
1028
|
+
tabValue: string;
|
|
1029
|
+
fields: RichField[];
|
|
1030
|
+
isEmpty: boolean;
|
|
1031
|
+
examples: ResponseExampleEntry[];
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function mediaTypeToCodeLang(mediaType?: string): string {
|
|
1035
|
+
const value = String(mediaType ?? '').toLowerCase();
|
|
1036
|
+
if (value.includes('json')) return 'json';
|
|
1037
|
+
if (value.includes('xml')) return 'xml';
|
|
1038
|
+
if (value.includes('yaml') || value.includes('yml')) return 'yaml';
|
|
1039
|
+
if (value.includes('html')) return 'html';
|
|
1040
|
+
return 'text';
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function stringifyExampleCode(value: unknown): string {
|
|
1044
|
+
if (typeof value === 'string') return value;
|
|
1045
|
+
try {
|
|
1046
|
+
return JSON.stringify(value, null, 2);
|
|
1047
|
+
} catch {
|
|
1048
|
+
return String(value);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function collectResponseExamples(media: OpenApiRecord): ResponseExampleEntry[] {
|
|
1053
|
+
const entries: ResponseExampleEntry[] = [];
|
|
1054
|
+
const examples = toRecord(media.examples);
|
|
1055
|
+
|
|
1056
|
+
if (examples) {
|
|
1057
|
+
let index = 0;
|
|
1058
|
+
for (const [name, rawExample] of Object.entries(examples)) {
|
|
1059
|
+
index += 1;
|
|
1060
|
+
|
|
1061
|
+
if (isRecord(rawExample) && typeof rawExample.$ref === 'string') continue;
|
|
1062
|
+
|
|
1063
|
+
const example = toRecord(rawExample);
|
|
1064
|
+
const value = example ? (example.value ?? undefined) : rawExample;
|
|
1065
|
+
if (value === undefined) continue;
|
|
1066
|
+
|
|
1067
|
+
const label = example && typeof example.summary === 'string' && example.summary.trim().length > 0
|
|
1068
|
+
? example.summary.trim()
|
|
1069
|
+
: name;
|
|
1070
|
+
const description = example && typeof example.description === 'string' && example.description.trim().length > 0
|
|
1071
|
+
? example.description.trim()
|
|
1072
|
+
: undefined;
|
|
1073
|
+
|
|
1074
|
+
entries.push({
|
|
1075
|
+
tabValue: `example-${index}`,
|
|
1076
|
+
label,
|
|
1077
|
+
description,
|
|
1078
|
+
code: stringifyExampleCode(value),
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (entries.length === 0 && media.example !== undefined) {
|
|
1084
|
+
entries.push({
|
|
1085
|
+
tabValue: 'example-default',
|
|
1086
|
+
label: 'Example',
|
|
1087
|
+
code: stringifyExampleCode(media.example),
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return entries;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function renderResponseExamples(
|
|
1095
|
+
examples: ResponseExampleEntry[],
|
|
1096
|
+
mediaType: string | undefined,
|
|
1097
|
+
ctx: RenderContext,
|
|
1098
|
+
scopeKey: string,
|
|
1099
|
+
): ReactNode {
|
|
1100
|
+
if (examples.length === 0) return null;
|
|
1101
|
+
|
|
1102
|
+
const codeLang = mediaTypeToCodeLang(mediaType);
|
|
1103
|
+
|
|
1104
|
+
if (examples.length === 1) {
|
|
1105
|
+
const example = examples[0];
|
|
1106
|
+
return (
|
|
1107
|
+
<div className="velu-openapi-response-examples">
|
|
1108
|
+
{example.description ? (
|
|
1109
|
+
<div className="velu-openapi-field-description">{ctx.renderMarkdown(example.description)}</div>
|
|
1110
|
+
) : null}
|
|
1111
|
+
<div className="velu-openapi-response-example-code">{ctx.renderCodeBlock(codeLang, example.code)}</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return (
|
|
1117
|
+
<Tabs
|
|
1118
|
+
defaultValue={examples[0]?.tabValue}
|
|
1119
|
+
className="velu-openapi-response-example-tabs !border-0 !rounded-none !bg-transparent !my-0 !overflow-visible"
|
|
1120
|
+
>
|
|
1121
|
+
<div className="velu-openapi-response-example-switch-header">
|
|
1122
|
+
<TabsList className="velu-openapi-response-example-list !px-0 !gap-1 !overflow-visible">
|
|
1123
|
+
{examples.map((example) => (
|
|
1124
|
+
<TabsTrigger
|
|
1125
|
+
key={`${scopeKey}-${example.tabValue}`}
|
|
1126
|
+
value={example.tabValue}
|
|
1127
|
+
className="velu-openapi-response-example-trigger"
|
|
1128
|
+
>
|
|
1129
|
+
{example.label}
|
|
1130
|
+
</TabsTrigger>
|
|
1131
|
+
))}
|
|
1132
|
+
</TabsList>
|
|
1133
|
+
</div>
|
|
1134
|
+
{examples.map((example) => (
|
|
1135
|
+
<Tab
|
|
1136
|
+
key={`${scopeKey}-${example.tabValue}-panel`}
|
|
1137
|
+
value={example.tabValue}
|
|
1138
|
+
className="velu-openapi-response-panel !p-0 !bg-transparent !rounded-none"
|
|
1139
|
+
>
|
|
1140
|
+
{example.description ? (
|
|
1141
|
+
<div className="velu-openapi-field-description">{ctx.renderMarkdown(example.description)}</div>
|
|
1142
|
+
) : null}
|
|
1143
|
+
<div className="velu-openapi-response-example-code">{ctx.renderCodeBlock(codeLang, example.code)}</div>
|
|
1144
|
+
</Tab>
|
|
1145
|
+
))}
|
|
1146
|
+
</Tabs>
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function renderResponseMediaContent(
|
|
1151
|
+
media: ResponseMediaEntry,
|
|
1152
|
+
ctx: RenderContext,
|
|
1153
|
+
suffix: string,
|
|
1154
|
+
): ReactNode {
|
|
1155
|
+
const exampleBlock = renderResponseExamples(media.examples, media.mediaType, ctx, suffix);
|
|
1156
|
+
const fieldsBlock = media.isEmpty ? null : (
|
|
1157
|
+
<div className="velu-openapi-field-list">
|
|
1158
|
+
{media.fields.map((field) => (
|
|
1159
|
+
<div key={`${suffix}-${field.name}`}>
|
|
1160
|
+
{renderResponseFieldWithChildren(field, ctx, suffix)}
|
|
1161
|
+
</div>
|
|
1162
|
+
))}
|
|
1163
|
+
</div>
|
|
1164
|
+
);
|
|
1165
|
+
|
|
1166
|
+
if (exampleBlock && fieldsBlock) {
|
|
1167
|
+
return (
|
|
1168
|
+
<>
|
|
1169
|
+
{exampleBlock}
|
|
1170
|
+
{fieldsBlock}
|
|
1171
|
+
</>
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (exampleBlock) return exampleBlock;
|
|
1176
|
+
if (fieldsBlock) return fieldsBlock;
|
|
1177
|
+
return <div className="velu-openapi-response-empty">Empty</div>;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function responseSortKey(status: string): number {
|
|
1181
|
+
const num = Number(status);
|
|
1182
|
+
return Number.isFinite(num) ? num : Number.MAX_SAFE_INTEGER;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function renderResponsesSection(method: MethodInformation, ctx: RenderContext): ReactNode {
|
|
1186
|
+
const responses = toRecord(method.responses);
|
|
1187
|
+
if (!responses) return null;
|
|
1188
|
+
const statuses = Object.keys(responses).sort((a, b) => responseSortKey(a) - responseSortKey(b) || a.localeCompare(b));
|
|
1189
|
+
if (statuses.length === 0) return null;
|
|
1190
|
+
|
|
1191
|
+
const statusEntries = statuses.map((status) => {
|
|
1192
|
+
const response = toRecord(responses[status]);
|
|
1193
|
+
const content = toRecord(response?.content);
|
|
1194
|
+
const responseDescription = typeof response?.description === 'string' ? response.description : undefined;
|
|
1195
|
+
const responseHeaders = response ? collectResponseHeaderFields(response) : [];
|
|
1196
|
+
const mediaEntries: ResponseMediaEntry[] = (content ? Object.entries(content) : [])
|
|
1197
|
+
.map(([mediaType, rawMedia]) => {
|
|
1198
|
+
const media = toRecord(rawMedia);
|
|
1199
|
+
const schema = toSchema(media?.schema);
|
|
1200
|
+
const fields = buildRichFieldsFromSchema(schema);
|
|
1201
|
+
const isEmpty = !schema;
|
|
1202
|
+
const examples = media ? collectResponseExamples(media) : [];
|
|
1203
|
+
|
|
1204
|
+
return {
|
|
1205
|
+
mediaType,
|
|
1206
|
+
tabValue: mediaType,
|
|
1207
|
+
fields,
|
|
1208
|
+
isEmpty,
|
|
1209
|
+
examples,
|
|
1210
|
+
};
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
if (mediaEntries.length === 0) {
|
|
1214
|
+
mediaEntries.push({
|
|
1215
|
+
mediaType: undefined,
|
|
1216
|
+
tabValue: '__default',
|
|
1217
|
+
fields: [],
|
|
1218
|
+
isEmpty: true,
|
|
1219
|
+
examples: [],
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
return {
|
|
1224
|
+
status,
|
|
1225
|
+
responseDescription,
|
|
1226
|
+
responseHeaders,
|
|
1227
|
+
mediaEntries,
|
|
1228
|
+
};
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
const mediaTypeSet = new Set(
|
|
1232
|
+
statusEntries
|
|
1233
|
+
.flatMap((entry) => entry.mediaEntries.map((media) => media.mediaType))
|
|
1234
|
+
.filter((mediaType): mediaType is string => typeof mediaType === 'string' && mediaType.length > 0),
|
|
1235
|
+
);
|
|
1236
|
+
const staticMediaType = mediaTypeSet.size === 1 ? Array.from(mediaTypeSet)[0] : undefined;
|
|
1237
|
+
|
|
1238
|
+
return (
|
|
1239
|
+
<section className="velu-openapi-section velu-openapi-response-section">
|
|
1240
|
+
<Tabs defaultValue={statusEntries[0]?.status} className="velu-openapi-response-tabs !border-0 !rounded-none !bg-transparent !my-0 !overflow-visible">
|
|
1241
|
+
<div className="velu-openapi-response-header">
|
|
1242
|
+
{ctx.renderHeading(2, 'Response')}
|
|
1243
|
+
<div className="velu-openapi-response-controls">
|
|
1244
|
+
<TabsList className="velu-openapi-response-status-list !px-0 !gap-1 !overflow-visible">
|
|
1245
|
+
{statusEntries.map((entry) => (
|
|
1246
|
+
<TabsTrigger
|
|
1247
|
+
key={`status-${entry.status}`}
|
|
1248
|
+
value={entry.status}
|
|
1249
|
+
className="velu-openapi-response-status-trigger"
|
|
1250
|
+
>
|
|
1251
|
+
{entry.status}
|
|
1252
|
+
</TabsTrigger>
|
|
1253
|
+
))}
|
|
1254
|
+
</TabsList>
|
|
1255
|
+
{staticMediaType ? (
|
|
1256
|
+
<span className="velu-openapi-response-content-type">{staticMediaType}</span>
|
|
1257
|
+
) : null}
|
|
1258
|
+
</div>
|
|
1259
|
+
</div>
|
|
1260
|
+
|
|
1261
|
+
{statusEntries.map((entry) => (
|
|
1262
|
+
<Tab
|
|
1263
|
+
key={entry.status}
|
|
1264
|
+
value={entry.status}
|
|
1265
|
+
className="velu-openapi-response-panel !p-0 !bg-transparent !rounded-none"
|
|
1266
|
+
>
|
|
1267
|
+
<div className="velu-openapi-response-group">
|
|
1268
|
+
{entry.responseDescription ? (
|
|
1269
|
+
<div className="velu-openapi-response-head">
|
|
1270
|
+
<div className="velu-openapi-field-description">{ctx.renderMarkdown(entry.responseDescription)}</div>
|
|
1271
|
+
</div>
|
|
1272
|
+
) : null}
|
|
1273
|
+
|
|
1274
|
+
{entry.responseHeaders.length > 0 ? (
|
|
1275
|
+
<div className="velu-openapi-response-headers">
|
|
1276
|
+
{ctx.renderHeading(3, 'Response Headers')}
|
|
1277
|
+
<div className="velu-openapi-field-list">
|
|
1278
|
+
{entry.responseHeaders.map((field) => (
|
|
1279
|
+
<div key={`${entry.status}-header-${field.name}`}>
|
|
1280
|
+
{renderResponseField(field, ctx, `${entry.status}-header`)}
|
|
1281
|
+
</div>
|
|
1282
|
+
))}
|
|
1283
|
+
</div>
|
|
1284
|
+
</div>
|
|
1285
|
+
) : null}
|
|
1286
|
+
|
|
1287
|
+
{entry.mediaEntries.length > 1 ? (
|
|
1288
|
+
<Tabs
|
|
1289
|
+
defaultValue={entry.mediaEntries[0]?.tabValue}
|
|
1290
|
+
className="velu-openapi-response-media-switch !border-0 !rounded-none !bg-transparent !my-0 !overflow-visible"
|
|
1291
|
+
>
|
|
1292
|
+
<div className="velu-openapi-response-media-switch-header">
|
|
1293
|
+
<TabsList className="velu-openapi-response-media-list !px-0 !gap-1 !overflow-visible">
|
|
1294
|
+
{entry.mediaEntries.map((media) => (
|
|
1295
|
+
<TabsTrigger
|
|
1296
|
+
key={`${entry.status}-${media.tabValue}`}
|
|
1297
|
+
value={media.tabValue}
|
|
1298
|
+
className="velu-openapi-response-media-trigger"
|
|
1299
|
+
>
|
|
1300
|
+
{media.mediaType ?? 'default'}
|
|
1301
|
+
</TabsTrigger>
|
|
1302
|
+
))}
|
|
1303
|
+
</TabsList>
|
|
1304
|
+
</div>
|
|
1305
|
+
|
|
1306
|
+
{entry.mediaEntries.map((media) => (
|
|
1307
|
+
<Tab
|
|
1308
|
+
key={`${entry.status}-${media.tabValue}-panel`}
|
|
1309
|
+
value={media.tabValue}
|
|
1310
|
+
className="velu-openapi-response-panel !p-0 !bg-transparent !rounded-none"
|
|
1311
|
+
>
|
|
1312
|
+
{renderResponseMediaContent(media, ctx, `${entry.status}-${media.tabValue}`)}
|
|
1313
|
+
</Tab>
|
|
1314
|
+
))}
|
|
1315
|
+
</Tabs>
|
|
1316
|
+
) : (
|
|
1317
|
+
renderResponseMediaContent(entry.mediaEntries[0], ctx, `${entry.status}`)
|
|
1318
|
+
)}
|
|
1319
|
+
</div>
|
|
1320
|
+
</Tab>
|
|
1321
|
+
))}
|
|
1322
|
+
</Tabs>
|
|
1323
|
+
</section>
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function createRendererForLayout(
|
|
1328
|
+
server: OpenAPIServer,
|
|
1329
|
+
layout: VeluOpenApiLayout,
|
|
1330
|
+
options: OpenApiRenderOptions,
|
|
1331
|
+
): APIPageRenderer {
|
|
1332
|
+
const generateCodeSamples = buildCodeSampleGenerator(
|
|
1333
|
+
options.exampleLanguages,
|
|
1334
|
+
options.exampleAutogenerate !== false,
|
|
1335
|
+
);
|
|
1336
|
+
const apiPageOptions = { generateCodeSamples };
|
|
1337
|
+
|
|
1338
|
+
if (layout === 'full') return createAPIPage(server, apiPageOptions);
|
|
1339
|
+
|
|
1340
|
+
if (layout === 'playground') {
|
|
1341
|
+
return createAPIPage(server, {
|
|
1342
|
+
...apiPageOptions,
|
|
1343
|
+
content: {
|
|
1344
|
+
renderOperationLayout: async (slots, ctx, method) => (
|
|
1345
|
+
<div className="velu-openapi-operation-layout">
|
|
1346
|
+
{slots.header}
|
|
1347
|
+
{slots.apiPlayground}
|
|
1348
|
+
{slots.description}
|
|
1349
|
+
{renderAuthorizationSection(method as MethodInformation, ctx)}
|
|
1350
|
+
{renderParameterSections(method as MethodInformation, ctx)}
|
|
1351
|
+
{renderRequestBodySection(method as MethodInformation, ctx)}
|
|
1352
|
+
{renderResponsesSection(method as MethodInformation, ctx)}
|
|
1353
|
+
{slots.callbacks}
|
|
1354
|
+
<div className="velu-openapi-example-source" data-velu-openapi-example-source="true">
|
|
1355
|
+
{slots.apiExample}
|
|
1356
|
+
</div>
|
|
1357
|
+
</div>
|
|
1358
|
+
),
|
|
1359
|
+
},
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
return createAPIPage(server, {
|
|
1364
|
+
...apiPageOptions,
|
|
1365
|
+
content: {
|
|
1366
|
+
renderOperationLayout: async (slots) => {
|
|
1367
|
+
return slots.apiExample;
|
|
1368
|
+
},
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function getRenderer(
|
|
1374
|
+
schemaSource: string,
|
|
1375
|
+
proxyUrl: string | undefined,
|
|
1376
|
+
layout: VeluOpenApiLayout,
|
|
1377
|
+
options: OpenApiRenderOptions,
|
|
1378
|
+
): OpenApiRenderer {
|
|
1379
|
+
const cacheKey = `${proxyUrl ?? 'direct'}::${layout}::${options.exampleAutogenerate !== false ? 'auto' : 'manual'}::${(options.exampleLanguages ?? []).join(',')}::${schemaSource}`;
|
|
1380
|
+
const cached = rendererCache.get(cacheKey);
|
|
1381
|
+
if (cached) return cached;
|
|
1382
|
+
|
|
1383
|
+
const server = createOpenAPI({
|
|
1384
|
+
input: [schemaSource],
|
|
1385
|
+
...(proxyUrl ? { proxyUrl } : {}),
|
|
1386
|
+
});
|
|
1387
|
+
const renderer = createRendererForLayout(server, layout, options);
|
|
1388
|
+
const nextValue: OpenApiRenderer = {
|
|
1389
|
+
renderer,
|
|
1390
|
+
document: schemaSource,
|
|
1391
|
+
server,
|
|
1392
|
+
};
|
|
1393
|
+
rendererCache.set(cacheKey, nextValue);
|
|
1394
|
+
return nextValue;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function getInlineRenderer(
|
|
1398
|
+
inlineDocumentId: string,
|
|
1399
|
+
inlineDocument: Record<string, unknown>,
|
|
1400
|
+
proxyUrl: string | undefined,
|
|
1401
|
+
layout: VeluOpenApiLayout,
|
|
1402
|
+
options: OpenApiRenderOptions,
|
|
1403
|
+
): OpenApiRenderer {
|
|
1404
|
+
const cacheKey = `${proxyUrl ?? 'direct'}::inline::${layout}::${options.exampleAutogenerate !== false ? 'auto' : 'manual'}::${(options.exampleLanguages ?? []).join(',')}::${inlineDocumentId}`;
|
|
1405
|
+
const cached = rendererCache.get(cacheKey);
|
|
1406
|
+
if (cached) return cached;
|
|
1407
|
+
|
|
1408
|
+
const server = createOpenAPI({
|
|
1409
|
+
input: async () => ({
|
|
1410
|
+
[inlineDocumentId]: inlineDocument,
|
|
1411
|
+
}),
|
|
1412
|
+
...(proxyUrl ? { proxyUrl } : {}),
|
|
1413
|
+
});
|
|
1414
|
+
const renderer = createRendererForLayout(server, layout, options);
|
|
1415
|
+
const nextValue: OpenApiRenderer = {
|
|
1416
|
+
renderer,
|
|
1417
|
+
document: inlineDocumentId,
|
|
1418
|
+
server,
|
|
1419
|
+
};
|
|
1420
|
+
rendererCache.set(cacheKey, nextValue);
|
|
1421
|
+
return nextValue;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
interface VeluOpenAPIProps {
|
|
1425
|
+
schemaSource?: string;
|
|
1426
|
+
inlineDocument?: Record<string, unknown>;
|
|
1427
|
+
inlineDocumentId?: string;
|
|
1428
|
+
layout?: VeluOpenApiLayout;
|
|
1429
|
+
endpoint?: string;
|
|
1430
|
+
method?: unknown;
|
|
1431
|
+
proxyUrl?: string;
|
|
1432
|
+
exampleLanguages?: string[];
|
|
1433
|
+
exampleAutogenerate?: boolean;
|
|
1434
|
+
className?: string;
|
|
1435
|
+
showTitle?: boolean;
|
|
1436
|
+
showDescription?: boolean;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
interface ResolvedOperation {
|
|
1440
|
+
path: string;
|
|
1441
|
+
method: HttpMethods;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
interface ResolvedWebhook {
|
|
1445
|
+
name: string;
|
|
1446
|
+
method: HttpMethods;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
interface ResolvedTargetOperation {
|
|
1450
|
+
type: 'operation';
|
|
1451
|
+
item: ResolvedOperation;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
interface ResolvedTargetWebhook {
|
|
1455
|
+
type: 'webhook';
|
|
1456
|
+
item: ResolvedWebhook;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
type ResolvedTarget = ResolvedTargetOperation | ResolvedTargetWebhook;
|
|
1460
|
+
|
|
1461
|
+
function pickWebhookMethod(pathItem: Record<string, unknown>): HttpMethods | null {
|
|
1462
|
+
for (const method of HTTP_METHODS) {
|
|
1463
|
+
if (pathItem[method]) return method;
|
|
1464
|
+
}
|
|
1465
|
+
return null;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function resolveOperation(schema: any, endpoint: string, method: HttpMethods): ResolvedOperation | null {
|
|
1469
|
+
const paths = schema?.dereferenced?.paths;
|
|
1470
|
+
if (!paths || typeof paths !== 'object') return null;
|
|
1471
|
+
|
|
1472
|
+
const exact = paths[endpoint];
|
|
1473
|
+
if (exact && typeof exact === 'object' && exact[method]) {
|
|
1474
|
+
return { path: endpoint, method };
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const entries = Object.entries(paths) as Array<[string, Record<string, unknown>]>;
|
|
1478
|
+
const sameMethod = entries.find(([, pathItem]) => Boolean(pathItem?.[method]));
|
|
1479
|
+
if (sameMethod) {
|
|
1480
|
+
return { path: sameMethod[0], method };
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
for (const [pathKey, pathItem] of entries) {
|
|
1484
|
+
for (const methodKey of HTTP_METHODS) {
|
|
1485
|
+
if (pathItem?.[methodKey]) return { path: pathKey, method: methodKey };
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function normalizeWebhookName(value: string): string {
|
|
1493
|
+
const trimmed = value.trim();
|
|
1494
|
+
if (!trimmed) return trimmed;
|
|
1495
|
+
if (trimmed.startsWith('/')) return trimmed;
|
|
1496
|
+
return `/${trimmed}`;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function resolveWebhook(schema: any, endpoint: string): ResolvedWebhook | null {
|
|
1500
|
+
const webhooks = schema?.dereferenced?.webhooks;
|
|
1501
|
+
if (!webhooks || typeof webhooks !== 'object') return null;
|
|
1502
|
+
|
|
1503
|
+
const webhookEntries = Object.entries(webhooks) as Array<[string, Record<string, unknown>]>;
|
|
1504
|
+
if (webhookEntries.length === 0) return null;
|
|
1505
|
+
|
|
1506
|
+
const normalizedTarget = normalizeWebhookName(endpoint);
|
|
1507
|
+
const exactMatch = webhookEntries.find(([name]) => name === endpoint || name === normalizedTarget);
|
|
1508
|
+
if (exactMatch) {
|
|
1509
|
+
const method = pickWebhookMethod(exactMatch[1]);
|
|
1510
|
+
if (method) return { name: exactMatch[0], method };
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
for (const [name, pathItem] of webhookEntries) {
|
|
1514
|
+
const method = pickWebhookMethod(pathItem);
|
|
1515
|
+
if (method) return { name, method };
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
return null;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function resolveTarget(schema: any, endpoint: string, method: SelectorMethod): ResolvedTarget | null {
|
|
1522
|
+
if (method === 'webhook') {
|
|
1523
|
+
const webhook = resolveWebhook(schema, endpoint);
|
|
1524
|
+
return webhook ? { type: 'webhook', item: webhook } : null;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
const operation = resolveOperation(schema, endpoint, method);
|
|
1528
|
+
return operation ? { type: 'operation', item: operation } : null;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
export async function VeluOpenAPI({
|
|
1532
|
+
schemaSource = DEFAULT_SCHEMA_SOURCE,
|
|
1533
|
+
inlineDocument,
|
|
1534
|
+
inlineDocumentId = 'velu-inline-openapi',
|
|
1535
|
+
layout = 'full',
|
|
1536
|
+
endpoint,
|
|
1537
|
+
method,
|
|
1538
|
+
proxyUrl = DEFAULT_PROXY_URL,
|
|
1539
|
+
exampleLanguages,
|
|
1540
|
+
exampleAutogenerate = true,
|
|
1541
|
+
className,
|
|
1542
|
+
showTitle = false,
|
|
1543
|
+
showDescription = false,
|
|
1544
|
+
}: VeluOpenAPIProps) {
|
|
1545
|
+
const resolvedSource = resolveSchemaSource(schemaSource);
|
|
1546
|
+
const resolvedProxyUrl = typeof proxyUrl === 'string' && proxyUrl.trim().length > 0
|
|
1547
|
+
? proxyUrl.trim()
|
|
1548
|
+
: undefined;
|
|
1549
|
+
const { renderer: APIPage, document, server } = inlineDocument
|
|
1550
|
+
? getInlineRenderer(inlineDocumentId, inlineDocument, resolvedProxyUrl, layout, {
|
|
1551
|
+
exampleLanguages,
|
|
1552
|
+
exampleAutogenerate,
|
|
1553
|
+
})
|
|
1554
|
+
: getRenderer(resolvedSource, resolvedProxyUrl, layout, {
|
|
1555
|
+
exampleLanguages,
|
|
1556
|
+
exampleAutogenerate,
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
const endpointPath = endpoint ? String(endpoint) : undefined;
|
|
1560
|
+
const endpointMethod = normalizeMethod(method);
|
|
1561
|
+
const resolvedSchema = endpointPath ? await server.getSchema(document) : undefined;
|
|
1562
|
+
const resolvedTarget = endpointPath && resolvedSchema
|
|
1563
|
+
? resolveTarget(resolvedSchema, endpointPath, endpointMethod)
|
|
1564
|
+
: null;
|
|
1565
|
+
const operations = endpointPath && resolvedTarget?.type === 'operation'
|
|
1566
|
+
? [{ path: resolvedTarget.item.path, method: resolvedTarget.item.method }]
|
|
1567
|
+
: undefined;
|
|
1568
|
+
const webhooks = endpointPath && resolvedTarget?.type === 'webhook'
|
|
1569
|
+
? [{ name: resolvedTarget.item.name, method: resolvedTarget.item.method }]
|
|
1570
|
+
: undefined;
|
|
1571
|
+
|
|
1572
|
+
if (endpointPath && !resolvedTarget) {
|
|
1573
|
+
return (
|
|
1574
|
+
<section className={className}>
|
|
1575
|
+
<div className="velu-openapi-warning">
|
|
1576
|
+
<p>
|
|
1577
|
+
Could not find a usable operation in <code>{inlineDocument ? inlineDocumentId : schemaSource}</code>.
|
|
1578
|
+
</p>
|
|
1579
|
+
</div>
|
|
1580
|
+
</section>
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
const isFallbackOperation = Boolean(
|
|
1585
|
+
endpointPath
|
|
1586
|
+
&& resolvedTarget
|
|
1587
|
+
&& (
|
|
1588
|
+
(resolvedTarget.type === 'operation'
|
|
1589
|
+
&& (resolvedTarget.item.path !== endpointPath || resolvedTarget.item.method !== endpointMethod))
|
|
1590
|
+
|| (resolvedTarget.type === 'webhook'
|
|
1591
|
+
&& normalizeWebhookName(resolvedTarget.item.name) !== normalizeWebhookName(endpointPath))
|
|
1592
|
+
),
|
|
1593
|
+
);
|
|
1594
|
+
const fallbackTargetLabel = resolvedTarget
|
|
1595
|
+
? (resolvedTarget.type === 'operation'
|
|
1596
|
+
? `${resolvedTarget.item.method.toUpperCase()} ${resolvedTarget.item.path}`
|
|
1597
|
+
: `WEBHOOK ${resolvedTarget.item.name}`)
|
|
1598
|
+
: '';
|
|
1599
|
+
|
|
1600
|
+
return (
|
|
1601
|
+
<section className={className}>
|
|
1602
|
+
{isFallbackOperation ? (
|
|
1603
|
+
<div className="velu-openapi-warning">
|
|
1604
|
+
<p>
|
|
1605
|
+
Could not find <code>{endpointMethod.toUpperCase()} {endpointPath}</code> in <code>{inlineDocument ? inlineDocumentId : schemaSource}</code>.
|
|
1606
|
+
Showing <code>{fallbackTargetLabel}</code> instead.
|
|
1607
|
+
</p>
|
|
1608
|
+
</div>
|
|
1609
|
+
) : null}
|
|
1610
|
+
<APIPage
|
|
1611
|
+
document={document}
|
|
1612
|
+
operations={operations}
|
|
1613
|
+
webhooks={webhooks}
|
|
1614
|
+
showTitle={showTitle}
|
|
1615
|
+
showDescription={showDescription}
|
|
1616
|
+
/>
|
|
1617
|
+
</section>
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
interface VeluOpenAPISchemaProps {
|
|
1622
|
+
schemaSource?: string;
|
|
1623
|
+
schema: string;
|
|
1624
|
+
className?: string;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
export async function VeluOpenAPISchema({
|
|
1628
|
+
schemaSource = DEFAULT_SCHEMA_SOURCE,
|
|
1629
|
+
schema,
|
|
1630
|
+
className,
|
|
1631
|
+
}: VeluOpenAPISchemaProps) {
|
|
1632
|
+
const schemaName = String(schema ?? '').trim();
|
|
1633
|
+
if (!schemaName) return null;
|
|
1634
|
+
|
|
1635
|
+
const resolvedSource = resolveSchemaSource(schemaSource);
|
|
1636
|
+
const { document, server } = getRenderer(resolvedSource, undefined, 'full', {});
|
|
1637
|
+
const resolvedSchema = await server.getSchema(document);
|
|
1638
|
+
const schemas = resolvedSchema?.dereferenced?.components?.schemas;
|
|
1639
|
+
if (!schemas || typeof schemas !== 'object') {
|
|
1640
|
+
return (
|
|
1641
|
+
<section className={className}>
|
|
1642
|
+
<div className="velu-openapi-warning">
|
|
1643
|
+
<p>
|
|
1644
|
+
Could not find <code>components.schemas</code> in <code>{schemaSource}</code>.
|
|
1645
|
+
</p>
|
|
1646
|
+
</div>
|
|
1647
|
+
</section>
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const entries = Object.entries(schemas) as Array<[string, unknown]>;
|
|
1652
|
+
const exact = entries.find(([name]) => name === schemaName);
|
|
1653
|
+
const fallback = entries.find(([name]) => name.toLowerCase() === schemaName.toLowerCase());
|
|
1654
|
+
const selected = exact ?? fallback;
|
|
1655
|
+
if (!selected) {
|
|
1656
|
+
return (
|
|
1657
|
+
<section className={className}>
|
|
1658
|
+
<div className="velu-openapi-warning">
|
|
1659
|
+
<p>
|
|
1660
|
+
Could not find <code>components.schemas.{schemaName}</code> in <code>{schemaSource}</code>.
|
|
1661
|
+
</p>
|
|
1662
|
+
</div>
|
|
1663
|
+
</section>
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
const selectedName = selected[0];
|
|
1668
|
+
const selectedSchema = selected[1] as Record<string, unknown>;
|
|
1669
|
+
const description = typeof selectedSchema?.description === 'string' ? selectedSchema.description : undefined;
|
|
1670
|
+
|
|
1671
|
+
return (
|
|
1672
|
+
<section className={className}>
|
|
1673
|
+
<div className="velu-openapi-schema">
|
|
1674
|
+
<h2>{selectedName}</h2>
|
|
1675
|
+
{description ? <p>{description}</p> : null}
|
|
1676
|
+
<pre className="velu-openapi-schema-json">
|
|
1677
|
+
<code>{JSON.stringify(selectedSchema, null, 2)}</code>
|
|
1678
|
+
</pre>
|
|
1679
|
+
</div>
|
|
1680
|
+
</section>
|
|
1681
|
+
);
|
|
1682
|
+
}
|