@djangocfg/ui-tools 2.1.289 → 2.1.291
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/README.md +14 -3
- package/dist/{DocsLayout-YDR7DSMM.cjs → DocsLayout-IKH7BLSU.cjs} +1537 -682
- package/dist/DocsLayout-IKH7BLSU.cjs.map +1 -0
- package/dist/{DocsLayout-TKJQ5W5E.mjs → DocsLayout-JPXFUKAR.mjs} +1429 -574
- package/dist/DocsLayout-JPXFUKAR.mjs.map +1 -0
- package/dist/{PrettyCode.client-5GABIN2I.cjs → PrettyCode.client-RPDIE5CH.cjs} +104 -3
- package/dist/PrettyCode.client-RPDIE5CH.cjs.map +1 -0
- package/dist/{PrettyCode.client-IZTXXYHG.mjs → PrettyCode.client-SPMTQEG4.mjs} +106 -5
- package/dist/PrettyCode.client-SPMTQEG4.mjs.map +1 -0
- package/dist/{chunk-IULI4XII.cjs → chunk-5Q4UMSWB.cjs} +355 -9
- package/dist/chunk-5Q4UMSWB.cjs.map +1 -0
- package/dist/{chunk-VZGQC3NG.mjs → chunk-EFWOJPA6.mjs} +349 -9
- package/dist/chunk-EFWOJPA6.mjs.map +1 -0
- package/dist/index.cjs +18 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.mjs +13 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +20 -15
- package/src/components/markdown/MarkdownMessage.tsx +46 -0
- package/src/tools/MarkdownEditor/MarkdownEditor.tsx +42 -1
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +87 -178
- package/src/tools/OpenapiViewer/README.md +114 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +20 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +6 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx +36 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/index.tsx +56 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts +77 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +146 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MethodBadge.tsx +6 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/PathDisplay.tsx +26 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +87 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamGroup.tsx +30 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamRow.tsx +36 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/index.tsx +22 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/RequestBody/index.tsx +33 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +76 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseRow.tsx +80 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +32 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/index.tsx +21 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +106 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +127 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/index.tsx +31 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/types.ts +28 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +87 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/defaults.ts +27 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +45 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/context.tsx +56 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/hooks/useSectionHash.ts +63 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +96 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +133 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/selectors.ts +40 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/types.ts +17 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +8 -2
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +48 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/CategoryBlock.tsx +33 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/EndpointRow.tsx +73 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +43 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SchemaSection.tsx +27 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SearchInput.tsx +45 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SidebarBody.tsx +50 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +64 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/buildVM.ts +126 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +112 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/types.ts +42 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/useDebouncedValue.ts +14 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +10 -7
- package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +9 -6
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +55 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PreviewView.tsx +115 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/RawView.tsx +24 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/StatusBar.tsx +63 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/ViewTabs.tsx +45 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/detectContent.ts +97 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/index.tsx +93 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/types.ts +26 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/useResponseView.ts +62 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +41 -71
- package/src/tools/OpenapiViewer/types.ts +10 -0
- package/src/tools/OpenapiViewer/utils/codeSamples.ts +287 -0
- package/src/tools/OpenapiViewer/utils/index.ts +3 -0
- package/src/tools/OpenapiViewer/utils/operationToHar.ts +119 -0
- package/src/tools/OpenapiViewer/utils/sampler.ts +72 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +88 -1
- package/src/tools/PrettyCode/PrettyCode.story.tsx +114 -361
- package/src/tools/PrettyCode/index.tsx +13 -0
- package/src/tools/PrettyCode/lazy.tsx +5 -0
- package/src/tools/PrettyCode/registerPrismLanguages.ts +111 -0
- package/dist/DocsLayout-TKJQ5W5E.mjs.map +0 -1
- package/dist/DocsLayout-YDR7DSMM.cjs.map +0 -1
- package/dist/PrettyCode.client-5GABIN2I.cjs.map +0 -1
- package/dist/PrettyCode.client-IZTXXYHG.mjs.map +0 -1
- package/dist/chunk-IULI4XII.cjs.map +0 -1
- package/dist/chunk-VZGQC3NG.mjs.map +0 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +0 -273
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +0 -439
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel.tsx +0 -127
|
@@ -4,69 +4,11 @@ import consola from 'consola';
|
|
|
4
4
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
5
|
|
|
6
6
|
import { ApiEndpoint, LoadedSchemaEntry, OpenApiInfo, OpenApiSchema, SchemaSource, UseOpenApiSchemaReturn } from '../types';
|
|
7
|
+
import { sampleSchemaJson } from '../utils/sampler';
|
|
7
8
|
import { dereferenceSchema } from '../utils/schemaExport';
|
|
8
9
|
import { joinUrl, resolveBaseUrl } from '../utils/url';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
type JsonSchemaNode = Record<string, unknown> & {
|
|
13
|
-
type?: string;
|
|
14
|
-
properties?: Record<string, JsonSchemaNode>;
|
|
15
|
-
required?: string[];
|
|
16
|
-
items?: JsonSchemaNode;
|
|
17
|
-
enum?: unknown[];
|
|
18
|
-
example?: unknown;
|
|
19
|
-
default?: unknown;
|
|
20
|
-
format?: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/** Walk a JSON Schema and build a realistic-looking example value. */
|
|
24
|
-
function exampleFromSchema(schema: JsonSchemaNode | undefined, depth = 0): unknown {
|
|
25
|
-
if (!schema || depth > 8) return null;
|
|
26
|
-
|
|
27
|
-
// Respect schema-provided examples first — no need to invent a value
|
|
28
|
-
// when the spec author already did the work.
|
|
29
|
-
if (schema.example !== undefined) return schema.example;
|
|
30
|
-
if (schema.default !== undefined) return schema.default;
|
|
31
|
-
if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
|
|
32
|
-
|
|
33
|
-
switch (schema.type) {
|
|
34
|
-
case 'object': {
|
|
35
|
-
const out: Record<string, unknown> = {};
|
|
36
|
-
const props = schema.properties ?? {};
|
|
37
|
-
for (const [k, v] of Object.entries(props)) {
|
|
38
|
-
out[k] = exampleFromSchema(v, depth + 1);
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
|
-
case 'array':
|
|
43
|
-
return [exampleFromSchema(schema.items, depth + 1)];
|
|
44
|
-
case 'integer':
|
|
45
|
-
case 'number':
|
|
46
|
-
return 0;
|
|
47
|
-
case 'boolean':
|
|
48
|
-
return false;
|
|
49
|
-
case 'string':
|
|
50
|
-
if (schema.format === 'date-time') return new Date().toISOString();
|
|
51
|
-
if (schema.format === 'date') return new Date().toISOString().slice(0, 10);
|
|
52
|
-
if (schema.format === 'email') return 'user@example.com';
|
|
53
|
-
if (schema.format === 'uri' || schema.format === 'url') return 'https://example.com';
|
|
54
|
-
if (schema.format === 'uuid') return '00000000-0000-0000-0000-000000000000';
|
|
55
|
-
return '';
|
|
56
|
-
default:
|
|
57
|
-
// No type (or composed schema like allOf/oneOf we don't unpack) —
|
|
58
|
-
// fall back to an empty object rather than ``null`` so the resulting
|
|
59
|
-
// JSON is still valid-looking.
|
|
60
|
-
if (schema.properties) {
|
|
61
|
-
const out: Record<string, unknown> = {};
|
|
62
|
-
for (const [k, v] of Object.entries(schema.properties)) {
|
|
63
|
-
out[k] = exampleFromSchema(v, depth + 1);
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
11
|
+
type JsonSchemaNode = Record<string, unknown>;
|
|
70
12
|
|
|
71
13
|
// HTTP methods to extract from OpenAPI schema
|
|
72
14
|
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const;
|
|
@@ -76,7 +18,17 @@ const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const;
|
|
|
76
18
|
// the front of each path here. ``schemaId`` is tagged onto every
|
|
77
19
|
// endpoint so downstream consumers (sections-mode sidebar, anchors, URL
|
|
78
20
|
// sync) can correlate endpoints back to their source schema.
|
|
79
|
-
|
|
21
|
+
//
|
|
22
|
+
// ``specRoot`` is the raw, un-dereferenced schema — passed to
|
|
23
|
+
// ``openapi-sampler`` so it can resolve any ``$ref`` nodes our own
|
|
24
|
+
// ``dereferenceSchema`` left behind (depth-limited, external, or
|
|
25
|
+
// circular). Without it the sampler throws on deep ref chains.
|
|
26
|
+
const extractEndpoints = (
|
|
27
|
+
schema: OpenApiSchema,
|
|
28
|
+
baseUrl: string,
|
|
29
|
+
schemaId?: string,
|
|
30
|
+
specRoot?: OpenApiSchema,
|
|
31
|
+
): ApiEndpoint[] => {
|
|
80
32
|
const endpoints: ApiEndpoint[] = [];
|
|
81
33
|
|
|
82
34
|
if (!schema.paths) return [];
|
|
@@ -109,17 +61,32 @@ const extractEndpoints = (schema: OpenApiSchema, baseUrl: string, schemaId?: str
|
|
|
109
61
|
});
|
|
110
62
|
}
|
|
111
63
|
|
|
112
|
-
// Collect responses
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
64
|
+
// Collect responses. We also extract the ``application/json``
|
|
65
|
+
// schema (preferring it, falling back to whatever media type is
|
|
66
|
+
// present) and generate a sampled example — the docs layout
|
|
67
|
+
// renders it as a collapsible "Example response" block under the
|
|
68
|
+
// status-code table. ``writeOnly`` fields are skipped so secrets
|
|
69
|
+
// declared ``writeOnly: true`` don't leak into response samples.
|
|
70
|
+
const responses: NonNullable<ApiEndpoint['responses']> = [];
|
|
117
71
|
|
|
118
72
|
if (op.responses) {
|
|
119
73
|
for (const [code, response] of Object.entries(op.responses)) {
|
|
74
|
+
const respContent = (response as any).content as Record<string, any> | undefined;
|
|
75
|
+
const contentKeys = respContent ? Object.keys(respContent) : [];
|
|
76
|
+
const chosenContentType = respContent?.['application/json']
|
|
77
|
+
? 'application/json'
|
|
78
|
+
: contentKeys[0];
|
|
79
|
+
const chosen = chosenContentType ? respContent?.[chosenContentType] : undefined;
|
|
80
|
+
const respSchema = chosen?.schema as JsonSchemaNode | undefined;
|
|
81
|
+
|
|
120
82
|
responses.push({
|
|
121
83
|
code,
|
|
122
84
|
description: (response as any).description || `Response ${code}`,
|
|
85
|
+
contentType: chosenContentType,
|
|
86
|
+
schema: respSchema,
|
|
87
|
+
example: respSchema
|
|
88
|
+
? sampleSchemaJson(respSchema, { skipWriteOnly: true }, specRoot)
|
|
89
|
+
: undefined,
|
|
123
90
|
});
|
|
124
91
|
}
|
|
125
92
|
}
|
|
@@ -127,17 +94,20 @@ const extractEndpoints = (schema: OpenApiSchema, baseUrl: string, schemaId?: str
|
|
|
127
94
|
// Extract request body info — keep the dereferenced schema so
|
|
128
95
|
// downstream UI can render a fields table and generate a starter
|
|
129
96
|
// example instead of showing an opaque ``object`` / ``array`` tag.
|
|
97
|
+
// ``readOnly`` fields are skipped: the body is what the client
|
|
98
|
+
// *sends*, so server-owned fields (id, created_at, …) must not
|
|
99
|
+
// pre-fill the editor.
|
|
130
100
|
let requestBody: ApiEndpoint['requestBody'];
|
|
131
101
|
if (op.requestBody) {
|
|
132
102
|
const content = op.requestBody.content;
|
|
133
103
|
const mediaType = content?.['application/json'] || content?.[Object.keys(content || {})[0]];
|
|
134
104
|
const rawSchema = mediaType?.schema as JsonSchemaNode | undefined;
|
|
135
105
|
requestBody = {
|
|
136
|
-
type: rawSchema?.type || 'object',
|
|
106
|
+
type: (rawSchema?.type as string | undefined) || 'object',
|
|
137
107
|
description: op.requestBody.description,
|
|
138
108
|
schema: rawSchema,
|
|
139
109
|
example: rawSchema
|
|
140
|
-
?
|
|
110
|
+
? sampleSchemaJson(rawSchema, { skipReadOnly: true }, specRoot)
|
|
141
111
|
: undefined,
|
|
142
112
|
};
|
|
143
113
|
}
|
|
@@ -254,9 +224,9 @@ export default function useOpenApiSchema({
|
|
|
254
224
|
const endpoints = useMemo(
|
|
255
225
|
() =>
|
|
256
226
|
dereferencedSchema
|
|
257
|
-
? extractEndpoints(dereferencedSchema, resolvedBaseUrl, currentSchemaId)
|
|
227
|
+
? extractEndpoints(dereferencedSchema, resolvedBaseUrl, currentSchemaId, currentOpenApiSchema ?? undefined)
|
|
258
228
|
: [],
|
|
259
|
-
[dereferencedSchema, resolvedBaseUrl, currentSchemaId]
|
|
229
|
+
[dereferencedSchema, resolvedBaseUrl, currentSchemaId, currentOpenApiSchema]
|
|
260
230
|
);
|
|
261
231
|
|
|
262
232
|
const categories = useMemo(() => getCategories(endpoints), [endpoints]);
|
|
@@ -381,7 +351,7 @@ export default function useOpenApiSchema({
|
|
|
381
351
|
servers: raw.servers,
|
|
382
352
|
}
|
|
383
353
|
: null;
|
|
384
|
-
const eps = deref ? extractEndpoints(deref, resolved, src.id) : [];
|
|
354
|
+
const eps = deref ? extractEndpoints(deref, resolved, src.id, raw ?? undefined) : [];
|
|
385
355
|
const state = loadStates.get(src.id) ?? { loading: !raw, error: null };
|
|
386
356
|
return {
|
|
387
357
|
source: src,
|
|
@@ -47,6 +47,16 @@ export interface ApiEndpoint {
|
|
|
47
47
|
responses?: Array<{
|
|
48
48
|
code: string;
|
|
49
49
|
description: string;
|
|
50
|
+
/** Media type the schema is sourced from (e.g. ``application/json``).
|
|
51
|
+
* ``undefined`` when the response advertises no body. */
|
|
52
|
+
contentType?: string;
|
|
53
|
+
/** Dereferenced JSON Schema for the response body. Used by the docs
|
|
54
|
+
* layout to render a sampled example under the status-code table. */
|
|
55
|
+
schema?: Record<string, unknown>;
|
|
56
|
+
/** Pre-generated example JSON (sampled via ``openapi-sampler``).
|
|
57
|
+
* ``undefined`` when the response has no schema or sampling failed
|
|
58
|
+
* — the UI conditionally hides the "Example" block in that case. */
|
|
59
|
+
example?: string;
|
|
50
60
|
}>;
|
|
51
61
|
}
|
|
52
62
|
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code sample generation.
|
|
3
|
+
*
|
|
4
|
+
* In-house, browser-safe generator for a small catalogue of languages
|
|
5
|
+
* (curl / fetch / Node axios / Python requests / Go / PHP / Ruby /
|
|
6
|
+
* Java OkHttp). We used to wrap Kong's ``httpsnippet`` here but it is
|
|
7
|
+
* Node-only — the bundle referenced ``global`` / ``stream`` /
|
|
8
|
+
* ``string_decoder`` and crashed in Vite dev. Writing our own keeps the
|
|
9
|
+
* chunk small (a few kB vs ~1.5 MB) and avoids runtime polyfills.
|
|
10
|
+
*
|
|
11
|
+
* Adding more targets later is one function plus a row in the catalogue.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { HarRequest } from './operationToHar';
|
|
15
|
+
|
|
16
|
+
// Prism language ids used by ``prism-react-renderer``. ``bash``,
|
|
17
|
+
// ``ruby``, ``java`` and ``php`` aren't in the library's default
|
|
18
|
+
// bundle, but our ``registerPrismLanguages`` module pulls their
|
|
19
|
+
// grammars from ``prismjs`` at load time so they work as first-class
|
|
20
|
+
// languages here.
|
|
21
|
+
export const CODE_SAMPLE_TARGETS = [
|
|
22
|
+
{ id: 'curl', label: 'cURL', prism: 'bash' },
|
|
23
|
+
{ id: 'fetch', label: 'JavaScript', prism: 'javascript' },
|
|
24
|
+
{ id: 'axios', label: 'Node (axios)', prism: 'javascript' },
|
|
25
|
+
{ id: 'python', label: 'Python', prism: 'python' },
|
|
26
|
+
{ id: 'go', label: 'Go', prism: 'go' },
|
|
27
|
+
{ id: 'php', label: 'PHP', prism: 'php' },
|
|
28
|
+
{ id: 'ruby', label: 'Ruby', prism: 'ruby' },
|
|
29
|
+
{ id: 'java', label: 'Java', prism: 'java' },
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export type CodeSampleTargetId = typeof CODE_SAMPLE_TARGETS[number]['id'];
|
|
33
|
+
|
|
34
|
+
// ─── Body literal helpers ─────────────────────────────────────────────────────
|
|
35
|
+
//
|
|
36
|
+
// Pre-formatted JSON bodies (``JSON.stringify(value, null, 2)``) carry
|
|
37
|
+
// real newlines that we want the snippet to preserve — a one-line
|
|
38
|
+
// escaped string like ``"{\n \"id\": 10,\n …}"`` reads as a wall of
|
|
39
|
+
// escape sequences. Each helper below returns a multi-line string
|
|
40
|
+
// literal in the target language's native syntax so the snippet reads
|
|
41
|
+
// like hand-written code.
|
|
42
|
+
|
|
43
|
+
/** Go raw string literal — uses backticks. Falls back to a double-
|
|
44
|
+
* quoted escaped literal when the body itself contains a backtick. */
|
|
45
|
+
function goRawString(s: string): string {
|
|
46
|
+
return s.includes('`') ? JSON.stringify(s) : `\`${s}\``;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Python triple-quoted string — uses ``"""``. Falls back to escaped
|
|
50
|
+
* form when the body already contains that sequence. */
|
|
51
|
+
function pythonTripleQuote(s: string): string {
|
|
52
|
+
return s.includes('"""') ? JSON.stringify(s) : `"""${s}"""`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Ruby squiggly heredoc — ``<<~JSON`` style strips common leading
|
|
56
|
+
* whitespace. Safe unless the body literally contains ``JSON`` on a
|
|
57
|
+
* line by itself, which is vanishingly rare. */
|
|
58
|
+
function rubyHeredoc(s: string): string {
|
|
59
|
+
return `<<~JSON\n${s}\nJSON`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** PHP heredoc — ``<<<JSON`` style. Same caveat as Ruby's. Trailing
|
|
63
|
+
* ``JSON;`` must sit at column 0 (enforced by this template). */
|
|
64
|
+
function phpHeredoc(s: string): string {
|
|
65
|
+
return `<<<JSON\n${s}\nJSON`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Java 15+ text block — ``"""``. Falls back to an escaped literal
|
|
69
|
+
* when the body contains that sequence. */
|
|
70
|
+
function javaTextBlock(s: string): string {
|
|
71
|
+
if (s.includes('"""')) return JSON.stringify(s);
|
|
72
|
+
// Text blocks need the opening ``"""`` on its own line — the
|
|
73
|
+
// parser strips the newline that immediately follows the delimiter.
|
|
74
|
+
return `"""\n${s}\n"""`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fullUrl(har: HarRequest): string {
|
|
78
|
+
if (!har.queryString.length) return har.url;
|
|
79
|
+
const qs = har.queryString
|
|
80
|
+
.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
|
|
81
|
+
.join('&');
|
|
82
|
+
const sep = har.url.includes('?') ? '&' : '?';
|
|
83
|
+
return `${har.url}${sep}${qs}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function shellEscape(value: string): string {
|
|
87
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderCurl(har: HarRequest): string {
|
|
91
|
+
const lines: string[] = [`curl -X ${har.method} ${shellEscape(fullUrl(har))}`];
|
|
92
|
+
for (const h of har.headers) {
|
|
93
|
+
lines.push(` -H ${shellEscape(`${h.name}: ${h.value}`)}`);
|
|
94
|
+
}
|
|
95
|
+
if (har.postData?.text) {
|
|
96
|
+
lines.push(` -d ${shellEscape(har.postData.text)}`);
|
|
97
|
+
}
|
|
98
|
+
return lines.join(' \\\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function jsHeadersLiteral(har: HarRequest, indent: string): string {
|
|
102
|
+
if (!har.headers.length) return '{}';
|
|
103
|
+
const entries = har.headers
|
|
104
|
+
.map((h) => `${indent} ${JSON.stringify(h.name)}: ${JSON.stringify(h.value)}`)
|
|
105
|
+
.join(',\n');
|
|
106
|
+
return `{\n${entries},\n${indent}}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** JS template literal — backtick string that preserves newlines and
|
|
110
|
+
* escapes backticks / ``${`` sequences. Makes multi-line JSON body
|
|
111
|
+
* readable inside the fetch() options. */
|
|
112
|
+
function jsTemplateLiteral(s: string): string {
|
|
113
|
+
if (/[`$]/.test(s)) return JSON.stringify(s);
|
|
114
|
+
return `\`${s}\``;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderFetch(har: HarRequest): string {
|
|
118
|
+
const url = fullUrl(har);
|
|
119
|
+
const options: string[] = [` method: ${JSON.stringify(har.method)}`];
|
|
120
|
+
if (har.headers.length) {
|
|
121
|
+
options.push(` headers: ${jsHeadersLiteral(har, ' ')}`);
|
|
122
|
+
}
|
|
123
|
+
if (har.postData?.text) {
|
|
124
|
+
options.push(` body: ${jsTemplateLiteral(har.postData.text)}`);
|
|
125
|
+
}
|
|
126
|
+
return `const response = await fetch(${JSON.stringify(url)}, {\n${options.join(',\n')},\n});\nconst data = await response.json();`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderAxios(har: HarRequest): string {
|
|
130
|
+
const url = fullUrl(har);
|
|
131
|
+
const config: string[] = [
|
|
132
|
+
` method: ${JSON.stringify(har.method.toLowerCase())}`,
|
|
133
|
+
` url: ${JSON.stringify(url)}`,
|
|
134
|
+
];
|
|
135
|
+
if (har.headers.length) {
|
|
136
|
+
config.push(` headers: ${jsHeadersLiteral(har, ' ')}`);
|
|
137
|
+
}
|
|
138
|
+
if (har.postData?.text) {
|
|
139
|
+
// Axios auto-serializes objects; we pass the raw JSON string as data.
|
|
140
|
+
config.push(` data: ${har.postData.text}`);
|
|
141
|
+
}
|
|
142
|
+
return `import axios from 'axios';\n\nconst { data } = await axios({\n${config.join(',\n')},\n});`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderPython(har: HarRequest): string {
|
|
146
|
+
const lines: string[] = [`import requests`, ``];
|
|
147
|
+
lines.push(`url = ${JSON.stringify(fullUrl(har))}`);
|
|
148
|
+
if (har.headers.length) {
|
|
149
|
+
const headerEntries = har.headers
|
|
150
|
+
.map((h) => ` ${JSON.stringify(h.name)}: ${JSON.stringify(h.value)}`)
|
|
151
|
+
.join(',\n');
|
|
152
|
+
lines.push(`headers = {\n${headerEntries},\n}`);
|
|
153
|
+
}
|
|
154
|
+
if (har.postData?.text) {
|
|
155
|
+
// ``json=payload`` on ``requests`` expects a Python object, not
|
|
156
|
+
// a JSON string — so we leave the body as a raw dict literal
|
|
157
|
+
// (which Python parses identically to the JSON source for the
|
|
158
|
+
// shapes we generate). No wrapping helper needed here.
|
|
159
|
+
lines.push(`payload = ${har.postData.text}`);
|
|
160
|
+
}
|
|
161
|
+
const args = [`url`];
|
|
162
|
+
if (har.headers.length) args.push(`headers=headers`);
|
|
163
|
+
if (har.postData?.text) args.push(`json=payload`);
|
|
164
|
+
lines.push(``, `response = requests.${har.method.toLowerCase()}(${args.join(', ')})`);
|
|
165
|
+
lines.push(`data = response.json()`);
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderGo(har: HarRequest): string {
|
|
170
|
+
const url = fullUrl(har);
|
|
171
|
+
const lines: string[] = [
|
|
172
|
+
`package main`,
|
|
173
|
+
``,
|
|
174
|
+
`import (`,
|
|
175
|
+
` "fmt"`,
|
|
176
|
+
` "io"`,
|
|
177
|
+
];
|
|
178
|
+
if (har.postData?.text) lines.push(` "strings"`);
|
|
179
|
+
lines.push(` "net/http"`);
|
|
180
|
+
lines.push(`)`, ``, `func main() {`);
|
|
181
|
+
if (har.postData?.text) {
|
|
182
|
+
lines.push(` payload := strings.NewReader(${goRawString(har.postData.text)})`);
|
|
183
|
+
lines.push(` req, _ := http.NewRequest(${JSON.stringify(har.method)}, ${JSON.stringify(url)}, payload)`);
|
|
184
|
+
} else {
|
|
185
|
+
lines.push(` req, _ := http.NewRequest(${JSON.stringify(har.method)}, ${JSON.stringify(url)}, nil)`);
|
|
186
|
+
}
|
|
187
|
+
for (const h of har.headers) {
|
|
188
|
+
lines.push(` req.Header.Add(${JSON.stringify(h.name)}, ${JSON.stringify(h.value)})`);
|
|
189
|
+
}
|
|
190
|
+
lines.push(
|
|
191
|
+
``,
|
|
192
|
+
` res, _ := http.DefaultClient.Do(req)`,
|
|
193
|
+
` defer res.Body.Close()`,
|
|
194
|
+
` body, _ := io.ReadAll(res.Body)`,
|
|
195
|
+
` fmt.Println(string(body))`,
|
|
196
|
+
`}`,
|
|
197
|
+
);
|
|
198
|
+
return lines.join('\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderPhp(har: HarRequest): string {
|
|
202
|
+
const lines: string[] = [`<?php`, ``, `$curl = curl_init();`, ``, `curl_setopt_array($curl, [`];
|
|
203
|
+
lines.push(` CURLOPT_URL => ${JSON.stringify(fullUrl(har))},`);
|
|
204
|
+
lines.push(` CURLOPT_RETURNTRANSFER => true,`);
|
|
205
|
+
lines.push(` CURLOPT_CUSTOMREQUEST => ${JSON.stringify(har.method)},`);
|
|
206
|
+
if (har.postData?.text) {
|
|
207
|
+
lines.push(` CURLOPT_POSTFIELDS => ${phpHeredoc(har.postData.text)},`);
|
|
208
|
+
}
|
|
209
|
+
if (har.headers.length) {
|
|
210
|
+
const headerList = har.headers
|
|
211
|
+
.map((h) => ` ${JSON.stringify(`${h.name}: ${h.value}`)}`)
|
|
212
|
+
.join(',\n');
|
|
213
|
+
lines.push(` CURLOPT_HTTPHEADER => [\n${headerList},\n ],`);
|
|
214
|
+
}
|
|
215
|
+
lines.push(`]);`, ``, `$response = curl_exec($curl);`, `curl_close($curl);`, `echo $response;`);
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function renderRuby(har: HarRequest): string {
|
|
220
|
+
const lines: string[] = [
|
|
221
|
+
`require 'net/http'`,
|
|
222
|
+
`require 'uri'`,
|
|
223
|
+
`require 'json'`,
|
|
224
|
+
``,
|
|
225
|
+
`uri = URI(${JSON.stringify(fullUrl(har))})`,
|
|
226
|
+
`http = Net::HTTP.new(uri.host, uri.port)`,
|
|
227
|
+
`http.use_ssl = uri.scheme == 'https'`,
|
|
228
|
+
``,
|
|
229
|
+
];
|
|
230
|
+
const methodClass = har.method.charAt(0) + har.method.slice(1).toLowerCase();
|
|
231
|
+
lines.push(`request = Net::HTTP::${methodClass}.new(uri)`);
|
|
232
|
+
for (const h of har.headers) {
|
|
233
|
+
lines.push(`request[${JSON.stringify(h.name)}] = ${JSON.stringify(h.value)}`);
|
|
234
|
+
}
|
|
235
|
+
if (har.postData?.text) {
|
|
236
|
+
lines.push(`request.body = ${rubyHeredoc(har.postData.text)}`);
|
|
237
|
+
}
|
|
238
|
+
lines.push(``, `response = http.request(request)`, `puts response.body`);
|
|
239
|
+
return lines.join('\n');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function renderJava(har: HarRequest): string {
|
|
243
|
+
const lines: string[] = [
|
|
244
|
+
`OkHttpClient client = new OkHttpClient();`,
|
|
245
|
+
``,
|
|
246
|
+
];
|
|
247
|
+
if (har.postData?.text) {
|
|
248
|
+
lines.push(
|
|
249
|
+
`MediaType mediaType = MediaType.parse("application/json");`,
|
|
250
|
+
`RequestBody body = RequestBody.create(mediaType, ${javaTextBlock(har.postData.text)});`,
|
|
251
|
+
``,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
lines.push(`Request request = new Request.Builder()`);
|
|
255
|
+
lines.push(` .url(${JSON.stringify(fullUrl(har))})`);
|
|
256
|
+
if (har.postData?.text) {
|
|
257
|
+
lines.push(` .method(${JSON.stringify(har.method)}, body)`);
|
|
258
|
+
} else {
|
|
259
|
+
lines.push(` .method(${JSON.stringify(har.method)}, null)`);
|
|
260
|
+
}
|
|
261
|
+
for (const h of har.headers) {
|
|
262
|
+
lines.push(` .addHeader(${JSON.stringify(h.name)}, ${JSON.stringify(h.value)})`);
|
|
263
|
+
}
|
|
264
|
+
lines.push(` .build();`, ``, `Response response = client.newCall(request).execute();`);
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const RENDERERS: Record<CodeSampleTargetId, (har: HarRequest) => string> = {
|
|
269
|
+
curl: renderCurl,
|
|
270
|
+
fetch: renderFetch,
|
|
271
|
+
axios: renderAxios,
|
|
272
|
+
python: renderPython,
|
|
273
|
+
go: renderGo,
|
|
274
|
+
php: renderPhp,
|
|
275
|
+
ruby: renderRuby,
|
|
276
|
+
java: renderJava,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export function renderSnippet(har: HarRequest, targetId: CodeSampleTargetId): string | null {
|
|
280
|
+
const renderer = RENDERERS[targetId];
|
|
281
|
+
if (!renderer) return null;
|
|
282
|
+
try {
|
|
283
|
+
return renderer(har);
|
|
284
|
+
} catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export * from './apiKeyManager';
|
|
8
|
+
export * from './codeSamples';
|
|
9
|
+
export * from './operationToHar';
|
|
8
10
|
export * from './versionManager';
|
|
9
11
|
export * from './formatters';
|
|
12
|
+
export * from './sampler';
|
|
10
13
|
export * from './schemaExport';
|
|
11
14
|
export * from './url';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI endpoint → HAR request.
|
|
3
|
+
*
|
|
4
|
+
* Our in-house code-sample renderers consume a HAR 1.2-shaped request.
|
|
5
|
+
* This module shapes an ``ApiEndpoint`` + user-provided parameter
|
|
6
|
+
* values into that form — path substitution, query-string assembly,
|
|
7
|
+
* header inference, body formatting.
|
|
8
|
+
*
|
|
9
|
+
* We keep the HAR intermediate (rather than a bespoke type) so the
|
|
10
|
+
* renderers match well-known HAR semantics and stay easy to extend.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ApiEndpoint } from '../types';
|
|
14
|
+
|
|
15
|
+
/** HAR 1.2 request subset that our renderers consume. We don't ship
|
|
16
|
+
* the full HAR type surface — just the fields that snippet generators
|
|
17
|
+
* actually read. */
|
|
18
|
+
export interface HarRequest {
|
|
19
|
+
method: string;
|
|
20
|
+
url: string;
|
|
21
|
+
httpVersion: string;
|
|
22
|
+
headers: Array<{ name: string; value: string }>;
|
|
23
|
+
queryString: Array<{ name: string; value: string }>;
|
|
24
|
+
cookies: Array<{ name: string; value: string }>;
|
|
25
|
+
headersSize: number;
|
|
26
|
+
bodySize: number;
|
|
27
|
+
postData?: {
|
|
28
|
+
mimeType: string;
|
|
29
|
+
text: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BuildHarInput {
|
|
34
|
+
endpoint: ApiEndpoint;
|
|
35
|
+
/** Raw body string — whatever the user typed in the playground or
|
|
36
|
+
* the pre-sampled example. Passed through verbatim; the caller
|
|
37
|
+
* decides the content shape. */
|
|
38
|
+
body?: string;
|
|
39
|
+
/** Path-param + query-param values, keyed by name. We look up each
|
|
40
|
+
* parameter on ``endpoint`` to decide whether it slots into the
|
|
41
|
+
* path or the query string. */
|
|
42
|
+
parameters?: Record<string, string>;
|
|
43
|
+
/** Extra headers to merge in (e.g. ``X-API-Key``, ``Authorization``).
|
|
44
|
+
* Later entries with the same name override earlier ones. */
|
|
45
|
+
headers?: Record<string, string>;
|
|
46
|
+
/** Override the request URL's base. When absent we use
|
|
47
|
+
* ``endpoint.path`` as-is (extractor already prepended base URL). */
|
|
48
|
+
baseUrl?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Split a template path into path vs query. Substitutes ``{id}``-style
|
|
52
|
+
* placeholders using the provided parameter map; unused entries flow
|
|
53
|
+
* into the query string. */
|
|
54
|
+
function buildUrl(
|
|
55
|
+
endpoint: ApiEndpoint,
|
|
56
|
+
parameters: Record<string, string>,
|
|
57
|
+
baseUrl?: string,
|
|
58
|
+
): { url: string; queryString: HarRequest['queryString'] } {
|
|
59
|
+
const pathParamNames = new Set(
|
|
60
|
+
(endpoint.parameters ?? [])
|
|
61
|
+
.filter((p) => endpoint.path.includes(`{${p.name}}`))
|
|
62
|
+
.map((p) => p.name),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
let path = endpoint.path;
|
|
66
|
+
for (const name of pathParamNames) {
|
|
67
|
+
const value = parameters[name] ?? `{${name}}`;
|
|
68
|
+
path = path.replaceAll(`{${name}}`, encodeURIComponent(value));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const queryString: HarRequest['queryString'] = [];
|
|
72
|
+
for (const param of endpoint.parameters ?? []) {
|
|
73
|
+
if (pathParamNames.has(param.name)) continue;
|
|
74
|
+
const value = parameters[param.name];
|
|
75
|
+
if (value === undefined || value === '') continue;
|
|
76
|
+
queryString.push({ name: param.name, value });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const url = baseUrl ? `${baseUrl.replace(/\/+$/, '')}${path.startsWith('/') ? path : `/${path}`}` : path;
|
|
80
|
+
return { url, queryString };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Build a HAR 1.2 request from an API endpoint + current form values. */
|
|
84
|
+
export function buildHarRequest(input: BuildHarInput): HarRequest {
|
|
85
|
+
const { endpoint, body, parameters = {}, headers = {}, baseUrl } = input;
|
|
86
|
+
const { url, queryString } = buildUrl(endpoint, parameters, baseUrl);
|
|
87
|
+
|
|
88
|
+
const hasBody = Boolean(body && body.trim().length > 0);
|
|
89
|
+
const bodyMime = hasBody
|
|
90
|
+
? 'application/json'
|
|
91
|
+
: undefined;
|
|
92
|
+
|
|
93
|
+
// Merge headers. Caller wins, but we default Content-Type for bodies
|
|
94
|
+
// and Accept for JSON endpoints — snippet consumers expect these.
|
|
95
|
+
const mergedHeaders: Record<string, string> = {};
|
|
96
|
+
if (hasBody && bodyMime) mergedHeaders['Content-Type'] = bodyMime;
|
|
97
|
+
mergedHeaders['Accept'] = 'application/json';
|
|
98
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
99
|
+
if (v === undefined || v === '') continue;
|
|
100
|
+
mergedHeaders[k] = v;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const har: HarRequest = {
|
|
104
|
+
method: endpoint.method.toUpperCase(),
|
|
105
|
+
url,
|
|
106
|
+
httpVersion: 'HTTP/1.1',
|
|
107
|
+
headers: Object.entries(mergedHeaders).map(([name, value]) => ({ name, value })),
|
|
108
|
+
queryString,
|
|
109
|
+
cookies: [],
|
|
110
|
+
headersSize: -1,
|
|
111
|
+
bodySize: hasBody ? new TextEncoder().encode(body!).length : 0,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (hasBody && bodyMime) {
|
|
115
|
+
har.postData = { mimeType: bodyMime, text: body! };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return har;
|
|
119
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema → example JSON.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over ``openapi-sampler`` (by Redocly — same library Redoc
|
|
5
|
+
* uses internally). Picks deterministic, realistic values from a
|
|
6
|
+
* dereferenced schema: honours ``const`` / ``examples`` / ``enum`` /
|
|
7
|
+
* ``default``, unpacks ``allOf`` / ``oneOf`` / ``anyOf``, respects
|
|
8
|
+
* string ``format`` (email/uuid/date-time/…).
|
|
9
|
+
*
|
|
10
|
+
* Replaces the previous hand-rolled ``exampleFromSchema`` — stricter on
|
|
11
|
+
* spec semantics and cheaper to maintain.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import consola from 'consola';
|
|
15
|
+
import { sample as openapiSample } from 'openapi-sampler';
|
|
16
|
+
|
|
17
|
+
type JsonSchemaLike = Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
export interface SampleOptions {
|
|
20
|
+
/** Skip properties marked ``readOnly`` — useful when the body will
|
|
21
|
+
* be sent in a request (request body editor). Default ``false``. */
|
|
22
|
+
skipReadOnly?: boolean;
|
|
23
|
+
/** Skip properties marked ``writeOnly`` — useful when rendering a
|
|
24
|
+
* response body (passwords etc. must not leak). Default ``false``. */
|
|
25
|
+
skipWriteOnly?: boolean;
|
|
26
|
+
/** Skip non-required properties. Rarely useful for a viewer — we
|
|
27
|
+
* want to show the full shape. Default ``false``. */
|
|
28
|
+
skipNonRequired?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Sample a JSON schema into a plain value. Returns ``null`` on failure
|
|
32
|
+
* (malformed schema, circular refs the sampler can't unwind) so the
|
|
33
|
+
* caller can show "no example" instead of crashing the page.
|
|
34
|
+
*
|
|
35
|
+
* ``spec`` must be the root OpenAPI document whenever ``schema`` may
|
|
36
|
+
* contain ``$ref`` nodes — ``openapi-sampler`` walks the tree and
|
|
37
|
+
* resolves refs against it. Our own ``dereferenceSchema`` only inlines
|
|
38
|
+
* refs up to a depth limit; anything deeper comes through here as a
|
|
39
|
+
* live ``$ref`` and the sampler throws if ``spec`` is missing. */
|
|
40
|
+
export function sampleSchema(
|
|
41
|
+
schema: JsonSchemaLike | undefined,
|
|
42
|
+
options: SampleOptions = {},
|
|
43
|
+
spec?: unknown,
|
|
44
|
+
): unknown {
|
|
45
|
+
if (!schema) return null;
|
|
46
|
+
try {
|
|
47
|
+
return openapiSample(schema as never, options, spec as never);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// Sampler failures used to be silent, which meant "no example"
|
|
50
|
+
// in the UI looked like a missing spec entry. Log so we can see
|
|
51
|
+
// the real cause in dev (circular refs, missing type, etc.).
|
|
52
|
+
consola.warn('[OpenapiViewer] sampleSchema failed:', err, { schema });
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Same as ``sampleSchema`` but returns a pre-stringified JSON payload
|
|
58
|
+
* ready to drop into a textarea / code block. Returns ``undefined``
|
|
59
|
+
* when sampling fails so the UI can conditionally hide the example. */
|
|
60
|
+
export function sampleSchemaJson(
|
|
61
|
+
schema: JsonSchemaLike | undefined,
|
|
62
|
+
options: SampleOptions = {},
|
|
63
|
+
spec?: unknown,
|
|
64
|
+
): string | undefined {
|
|
65
|
+
const value = sampleSchema(schema, options, spec);
|
|
66
|
+
if (value === null || value === undefined) return undefined;
|
|
67
|
+
try {
|
|
68
|
+
return JSON.stringify(value, null, 2);
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|