@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.
Files changed (98) hide show
  1. package/README.md +14 -3
  2. package/dist/{DocsLayout-YDR7DSMM.cjs → DocsLayout-IKH7BLSU.cjs} +1537 -682
  3. package/dist/DocsLayout-IKH7BLSU.cjs.map +1 -0
  4. package/dist/{DocsLayout-TKJQ5W5E.mjs → DocsLayout-JPXFUKAR.mjs} +1429 -574
  5. package/dist/DocsLayout-JPXFUKAR.mjs.map +1 -0
  6. package/dist/{PrettyCode.client-5GABIN2I.cjs → PrettyCode.client-RPDIE5CH.cjs} +104 -3
  7. package/dist/PrettyCode.client-RPDIE5CH.cjs.map +1 -0
  8. package/dist/{PrettyCode.client-IZTXXYHG.mjs → PrettyCode.client-SPMTQEG4.mjs} +106 -5
  9. package/dist/PrettyCode.client-SPMTQEG4.mjs.map +1 -0
  10. package/dist/{chunk-IULI4XII.cjs → chunk-5Q4UMSWB.cjs} +355 -9
  11. package/dist/chunk-5Q4UMSWB.cjs.map +1 -0
  12. package/dist/{chunk-VZGQC3NG.mjs → chunk-EFWOJPA6.mjs} +349 -9
  13. package/dist/chunk-EFWOJPA6.mjs.map +1 -0
  14. package/dist/index.cjs +18 -10
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +35 -1
  17. package/dist/index.d.ts +35 -1
  18. package/dist/index.mjs +13 -5
  19. package/dist/index.mjs.map +1 -1
  20. package/package.json +20 -15
  21. package/src/components/markdown/MarkdownMessage.tsx +46 -0
  22. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +42 -1
  23. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +87 -178
  24. package/src/tools/OpenapiViewer/README.md +114 -6
  25. package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +20 -6
  26. package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +6 -0
  27. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx +36 -0
  28. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/index.tsx +56 -0
  29. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts +77 -0
  30. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +146 -0
  31. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MethodBadge.tsx +6 -0
  32. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/PathDisplay.tsx +26 -0
  33. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +87 -0
  34. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamGroup.tsx +30 -0
  35. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamRow.tsx +36 -0
  36. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/index.tsx +22 -0
  37. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/RequestBody/index.tsx +33 -0
  38. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +76 -0
  39. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseRow.tsx +80 -0
  40. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +32 -0
  41. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/index.tsx +21 -0
  42. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +106 -0
  43. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +127 -0
  44. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/index.tsx +31 -0
  45. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/types.ts +28 -0
  46. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +87 -0
  47. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/defaults.ts +27 -0
  48. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +45 -0
  49. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/context.tsx +56 -0
  50. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/hooks/useSectionHash.ts +63 -0
  51. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +96 -0
  52. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +133 -0
  53. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/selectors.ts +40 -0
  54. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/types.ts +17 -0
  55. package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +8 -2
  56. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +48 -0
  57. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/CategoryBlock.tsx +33 -0
  58. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/EndpointRow.tsx +73 -0
  59. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +43 -0
  60. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SchemaSection.tsx +27 -0
  61. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SearchInput.tsx +45 -0
  62. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SidebarBody.tsx +50 -0
  63. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +64 -0
  64. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/buildVM.ts +126 -0
  65. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +112 -0
  66. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/types.ts +42 -0
  67. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/useDebouncedValue.ts +14 -0
  68. package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +10 -7
  69. package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +9 -6
  70. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +55 -0
  71. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PreviewView.tsx +115 -0
  72. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/RawView.tsx +24 -0
  73. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/StatusBar.tsx +63 -0
  74. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/ViewTabs.tsx +45 -0
  75. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/detectContent.ts +97 -0
  76. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/index.tsx +93 -0
  77. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/types.ts +26 -0
  78. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/useResponseView.ts +62 -0
  79. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +41 -71
  80. package/src/tools/OpenapiViewer/types.ts +10 -0
  81. package/src/tools/OpenapiViewer/utils/codeSamples.ts +287 -0
  82. package/src/tools/OpenapiViewer/utils/index.ts +3 -0
  83. package/src/tools/OpenapiViewer/utils/operationToHar.ts +119 -0
  84. package/src/tools/OpenapiViewer/utils/sampler.ts +72 -0
  85. package/src/tools/PrettyCode/PrettyCode.client.tsx +88 -1
  86. package/src/tools/PrettyCode/PrettyCode.story.tsx +114 -361
  87. package/src/tools/PrettyCode/index.tsx +13 -0
  88. package/src/tools/PrettyCode/lazy.tsx +5 -0
  89. package/src/tools/PrettyCode/registerPrismLanguages.ts +111 -0
  90. package/dist/DocsLayout-TKJQ5W5E.mjs.map +0 -1
  91. package/dist/DocsLayout-YDR7DSMM.cjs.map +0 -1
  92. package/dist/PrettyCode.client-5GABIN2I.cjs.map +0 -1
  93. package/dist/PrettyCode.client-IZTXXYHG.mjs.map +0 -1
  94. package/dist/chunk-IULI4XII.cjs.map +0 -1
  95. package/dist/chunk-VZGQC3NG.mjs.map +0 -1
  96. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +0 -273
  97. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +0 -439
  98. 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
- // ─── JSON Schema → example value ──────────────────────────────────────────────
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
- const extractEndpoints = (schema: OpenApiSchema, baseUrl: string, schemaId?: string): ApiEndpoint[] => {
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
- const responses: Array<{
114
- code: string;
115
- description: string;
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
- ? JSON.stringify(exampleFromSchema(rawSchema), null, 2)
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
+ }