radiant-docs 0.1.6 → 0.1.8

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 (78) hide show
  1. package/dist/index.js +32 -6
  2. package/package.json +3 -3
  3. package/template/astro.config.mjs +76 -3
  4. package/template/package-lock.json +924 -737
  5. package/template/package.json +7 -5
  6. package/template/scripts/generate-og-images.mjs +335 -0
  7. package/template/scripts/generate-og-metadata.mjs +173 -0
  8. package/template/scripts/rewrite-static-asset-host.mjs +408 -0
  9. package/template/scripts/stamp-image-versions.mjs +277 -0
  10. package/template/scripts/stamp-og-image-versions.mjs +199 -0
  11. package/template/scripts/stamp-pagefind-runtime-version.mjs +140 -0
  12. package/template/src/assets/fonts/geist-mono/cyrillic.woff2 +0 -0
  13. package/template/src/assets/fonts/geist-mono/latin-ext.woff2 +0 -0
  14. package/template/src/assets/fonts/geist-mono/latin.woff2 +0 -0
  15. package/template/src/assets/fonts/google-sans-flex/canadian-aboriginal.woff2 +0 -0
  16. package/template/src/assets/fonts/google-sans-flex/cherokee.woff2 +0 -0
  17. package/template/src/assets/fonts/google-sans-flex/latin-ext.woff2 +0 -0
  18. package/template/src/assets/fonts/google-sans-flex/latin.woff2 +0 -0
  19. package/template/src/assets/fonts/google-sans-flex/math.woff2 +0 -0
  20. package/template/src/assets/fonts/google-sans-flex/nushu.woff2 +0 -0
  21. package/template/src/assets/fonts/google-sans-flex/symbols.woff2 +0 -0
  22. package/template/src/assets/fonts/google-sans-flex/syriac.woff2 +0 -0
  23. package/template/src/assets/fonts/google-sans-flex/tifinagh.woff2 +0 -0
  24. package/template/src/assets/fonts/google-sans-flex/vietnamese.woff2 +0 -0
  25. package/template/src/components/Footer.astro +94 -0
  26. package/template/src/components/Header.astro +11 -66
  27. package/template/src/components/LogoLink.astro +103 -0
  28. package/template/src/components/MdxPage.astro +126 -11
  29. package/template/src/components/OpenApiPage.astro +1036 -69
  30. package/template/src/components/Search.astro +0 -2
  31. package/template/src/components/SidebarDropdown.astro +34 -14
  32. package/template/src/components/SidebarGroup.astro +3 -6
  33. package/template/src/components/SidebarLink.astro +22 -12
  34. package/template/src/components/SidebarMenu.astro +19 -16
  35. package/template/src/components/SidebarSegmented.astro +99 -0
  36. package/template/src/components/SidebarSubgroup.astro +12 -12
  37. package/template/src/components/ThemeSwitcher.astro +30 -7
  38. package/template/src/components/endpoint/PlaygroundBar.astro +32 -36
  39. package/template/src/components/endpoint/PlaygroundButton.astro +40 -4
  40. package/template/src/components/endpoint/PlaygroundField.astro +1068 -22
  41. package/template/src/components/endpoint/PlaygroundForm.astro +559 -61
  42. package/template/src/components/endpoint/RequestSnippets.astro +342 -193
  43. package/template/src/components/endpoint/ResponseDisplay.astro +161 -147
  44. package/template/src/components/endpoint/ResponseFieldTree.astro +134 -0
  45. package/template/src/components/endpoint/ResponseFields.astro +711 -68
  46. package/template/src/components/endpoint/ResponseSnippets.astro +299 -173
  47. package/template/src/components/sidebar/SidebarEndpointLink.astro +1 -1
  48. package/template/src/components/ui/CodeLanguageIcon.astro +19 -0
  49. package/template/src/components/ui/CodeTabEdge.astro +79 -0
  50. package/template/src/components/ui/Field.astro +103 -20
  51. package/template/src/components/ui/Icon.astro +32 -0
  52. package/template/src/components/ui/ListChevronsToggle.astro +31 -0
  53. package/template/src/components/ui/Tag.astro +1 -1
  54. package/template/src/components/user/{Accordian.astro → Accordion.astro} +6 -6
  55. package/template/src/components/user/Callout.astro +5 -9
  56. package/template/src/components/user/CodeBlock.astro +400 -0
  57. package/template/src/components/user/CodeGroup.astro +225 -0
  58. package/template/src/components/user/ComponentPreview.astro +1 -0
  59. package/template/src/components/user/ComponentPreviewBlock.astro +181 -0
  60. package/template/src/components/user/Image.astro +132 -0
  61. package/template/src/components/user/Steps.astro +1 -3
  62. package/template/src/components/user/Tabs.astro +2 -2
  63. package/template/src/content.config.ts +1 -0
  64. package/template/src/layouts/Layout.astro +109 -8
  65. package/template/src/lib/code/code-block.ts +546 -0
  66. package/template/src/lib/frontmatter-schema.ts +8 -7
  67. package/template/src/lib/mdx/remark-code-block-component.ts +342 -0
  68. package/template/src/lib/mdx/remark-demote-h1.ts +16 -0
  69. package/template/src/lib/pagefind.ts +19 -5
  70. package/template/src/lib/routes.ts +49 -31
  71. package/template/src/lib/utils.ts +20 -0
  72. package/template/src/lib/validation.ts +638 -200
  73. package/template/src/pages/[...slug].astro +18 -5
  74. package/template/src/styles/geist-mono.css +33 -0
  75. package/template/src/styles/global.css +89 -84
  76. package/template/src/styles/google-sans-flex.css +143 -0
  77. package/template/ec.config.mjs +0 -51
  78. /package/template/src/components/user/{AccordianGroup.astro → AccordionGroup.astro} +0 -0
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import type { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
3
- import Field from "../ui/Field.astro";
3
+ import ListChevronsToggle from "../ui/ListChevronsToggle.astro";
4
+ import ResponseFieldTree from "./ResponseFieldTree.astro";
4
5
 
5
6
  interface Props {
6
7
  responses: OpenAPIV3.ResponsesObject | OpenAPIV3_1.ResponsesObject;
@@ -8,37 +9,490 @@ interface Props {
8
9
 
9
10
  const { responses } = Astro.props;
10
11
 
12
+ interface ResponseFieldVariant {
13
+ label: string;
14
+ fields: ResponseField[];
15
+ }
16
+
11
17
  interface ResponseField {
12
18
  name: string;
13
19
  required: boolean;
14
20
  type: string;
15
21
  description: string;
16
22
  nested?: ResponseField[]; // For nested objects
17
- enum?: string[];
23
+ enum?: (string | number)[];
24
+ minLength?: number;
25
+ maxLength?: number;
26
+ minimum?: number;
27
+ maximum?: number;
28
+ exclusiveMinimum?: number;
29
+ exclusiveMaximum?: number;
30
+ variants?: ResponseFieldVariant[];
31
+ variantType?: "oneOf" | "anyOf";
18
32
  }
19
33
 
34
+ type ResponseVariant = ResponseFieldVariant;
35
+
20
36
  interface ResponseData {
21
37
  statusCode: string;
22
38
  description?: string;
23
39
  fields: ResponseField[];
40
+ variants?: ResponseVariant[];
41
+ variantType?: "oneOf" | "anyOf";
42
+ }
43
+
44
+ function getStatusCodeDotClass(statusCode: string): string {
45
+ const family = statusCode[0];
46
+ if (family === "2") return "bg-green-700/70";
47
+ if (family === "3") return "bg-blue-700/70";
48
+ if (family === "4") return "bg-yellow-500/80";
49
+ if (family === "5") return "bg-red-700/70";
50
+ return "bg-neutral-400";
51
+ }
52
+
53
+ function getSchemaVariants(schema: any): any[] {
54
+ if (!schema) return [];
55
+ if (Array.isArray(schema.oneOf)) return schema.oneOf;
56
+ if (Array.isArray(schema.anyOf)) return schema.anyOf;
57
+ return [];
58
+ }
59
+
60
+ function getSchemaEnumValues(schema: any): (string | number)[] | undefined {
61
+ if (!schema) return undefined;
62
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
63
+ const values = schema.enum.filter(
64
+ (value: unknown): value is string | number =>
65
+ typeof value === "string" || typeof value === "number",
66
+ );
67
+ return values.length > 0 ? values : undefined;
68
+ }
69
+
70
+ const variantSchemas: any[] = getSchemaVariants(schema);
71
+ if (variantSchemas.length === 0) return undefined;
72
+
73
+ const deduped = new Map<string, string | number>();
74
+ variantSchemas.forEach((variant) => {
75
+ const variantEnum = getSchemaEnumValues(variant);
76
+ variantEnum?.forEach((value) => {
77
+ deduped.set(`${typeof value}:${String(value)}`, value);
78
+ });
79
+ });
80
+
81
+ return deduped.size > 0 ? Array.from(deduped.values()) : undefined;
82
+ }
83
+
84
+ function parseFiniteNumber(value: unknown): number | undefined {
85
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
86
+ }
87
+
88
+ function parseNonNegativeInteger(value: unknown): number | undefined {
89
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
90
+ if (!Number.isInteger(value) || value < 0) return undefined;
91
+ return value;
92
+ }
93
+
94
+ function getDirectSchemaNumericConstraints(schema: any): {
95
+ minimum?: number;
96
+ maximum?: number;
97
+ exclusiveMinimum?: number;
98
+ exclusiveMaximum?: number;
99
+ } {
100
+ const minimum = parseFiniteNumber(schema?.minimum);
101
+ const maximum = parseFiniteNumber(schema?.maximum);
102
+ const exclusiveMinimumValue = parseFiniteNumber(schema?.exclusiveMinimum);
103
+ const exclusiveMaximumValue = parseFiniteNumber(schema?.exclusiveMaximum);
104
+
105
+ const exclusiveMinimum =
106
+ exclusiveMinimumValue !== undefined
107
+ ? exclusiveMinimumValue
108
+ : schema?.exclusiveMinimum === true
109
+ ? minimum
110
+ : undefined;
111
+ const exclusiveMaximum =
112
+ exclusiveMaximumValue !== undefined
113
+ ? exclusiveMaximumValue
114
+ : schema?.exclusiveMaximum === true
115
+ ? maximum
116
+ : undefined;
117
+
118
+ const constraints: {
119
+ minimum?: number;
120
+ maximum?: number;
121
+ exclusiveMinimum?: number;
122
+ exclusiveMaximum?: number;
123
+ } = {};
124
+
125
+ if (exclusiveMinimum !== undefined) {
126
+ constraints.exclusiveMinimum = exclusiveMinimum;
127
+ } else if (minimum !== undefined) {
128
+ constraints.minimum = minimum;
129
+ }
130
+
131
+ if (exclusiveMaximum !== undefined) {
132
+ constraints.exclusiveMaximum = exclusiveMaximum;
133
+ } else if (maximum !== undefined) {
134
+ constraints.maximum = maximum;
135
+ }
136
+
137
+ return constraints;
138
+ }
139
+
140
+ function mergeConjunctiveNumericConstraints(
141
+ base: {
142
+ minimum?: number;
143
+ maximum?: number;
144
+ exclusiveMinimum?: number;
145
+ exclusiveMaximum?: number;
146
+ },
147
+ next: {
148
+ minimum?: number;
149
+ maximum?: number;
150
+ exclusiveMinimum?: number;
151
+ exclusiveMaximum?: number;
152
+ },
153
+ ): {
154
+ minimum?: number;
155
+ maximum?: number;
156
+ exclusiveMinimum?: number;
157
+ exclusiveMaximum?: number;
158
+ } {
159
+ const toLowerBound = (constraint: typeof base) => {
160
+ if (constraint.exclusiveMinimum !== undefined) {
161
+ return { value: constraint.exclusiveMinimum, exclusive: true };
162
+ }
163
+ if (constraint.minimum !== undefined) {
164
+ return { value: constraint.minimum, exclusive: false };
165
+ }
166
+ return undefined;
167
+ };
168
+ const toUpperBound = (constraint: typeof base) => {
169
+ if (constraint.exclusiveMaximum !== undefined) {
170
+ return { value: constraint.exclusiveMaximum, exclusive: true };
171
+ }
172
+ if (constraint.maximum !== undefined) {
173
+ return { value: constraint.maximum, exclusive: false };
174
+ }
175
+ return undefined;
176
+ };
177
+ const pickStricterLower = (
178
+ first?: { value: number; exclusive: boolean },
179
+ second?: { value: number; exclusive: boolean },
180
+ ) => {
181
+ if (!first) return second;
182
+ if (!second) return first;
183
+ if (first.value > second.value) return first;
184
+ if (second.value > first.value) return second;
185
+ return { value: first.value, exclusive: first.exclusive || second.exclusive };
186
+ };
187
+ const pickStricterUpper = (
188
+ first?: { value: number; exclusive: boolean },
189
+ second?: { value: number; exclusive: boolean },
190
+ ) => {
191
+ if (!first) return second;
192
+ if (!second) return first;
193
+ if (first.value < second.value) return first;
194
+ if (second.value < first.value) return second;
195
+ return { value: first.value, exclusive: first.exclusive || second.exclusive };
196
+ };
197
+
198
+ const lower = pickStricterLower(toLowerBound(base), toLowerBound(next));
199
+ const upper = pickStricterUpper(toUpperBound(base), toUpperBound(next));
200
+
201
+ const merged: {
202
+ minimum?: number;
203
+ maximum?: number;
204
+ exclusiveMinimum?: number;
205
+ exclusiveMaximum?: number;
206
+ } = {};
207
+ if (lower) {
208
+ if (lower.exclusive) merged.exclusiveMinimum = lower.value;
209
+ else merged.minimum = lower.value;
210
+ }
211
+ if (upper) {
212
+ if (upper.exclusive) merged.exclusiveMaximum = upper.value;
213
+ else merged.maximum = upper.value;
214
+ }
215
+
216
+ return merged;
217
+ }
218
+
219
+ function getSchemaNumericConstraints(
220
+ schema: any,
221
+ seen = new Set<any>(),
222
+ ): {
223
+ minimum?: number;
224
+ maximum?: number;
225
+ exclusiveMinimum?: number;
226
+ exclusiveMaximum?: number;
227
+ } {
228
+ if (!schema || typeof schema !== "object") return {};
229
+ if (seen.has(schema)) return {};
230
+
231
+ const nextSeen = new Set(seen);
232
+ nextSeen.add(schema);
233
+
234
+ let constraints = getDirectSchemaNumericConstraints(schema);
235
+
236
+ if (Array.isArray(schema.allOf)) {
237
+ schema.allOf.forEach((part: any) => {
238
+ constraints = mergeConjunctiveNumericConstraints(
239
+ constraints,
240
+ getSchemaNumericConstraints(part, nextSeen),
241
+ );
242
+ });
243
+ }
244
+
245
+ const variants = getSchemaVariants(schema);
246
+ if (variants.length > 0) {
247
+ const variantConstraints = variants.map((variant) =>
248
+ getSchemaNumericConstraints(variant, nextSeen),
249
+ );
250
+ const shared: {
251
+ minimum?: number;
252
+ maximum?: number;
253
+ exclusiveMinimum?: number;
254
+ exclusiveMaximum?: number;
255
+ } = {};
256
+ const keys = [
257
+ "minimum",
258
+ "maximum",
259
+ "exclusiveMinimum",
260
+ "exclusiveMaximum",
261
+ ] as const;
262
+
263
+ keys.forEach((key) => {
264
+ const firstValue = variantConstraints[0]?.[key];
265
+ if (firstValue === undefined) return;
266
+ const allMatch = variantConstraints.every(
267
+ (variantConstraint) => variantConstraint[key] === firstValue,
268
+ );
269
+ if (allMatch) {
270
+ shared[key] = firstValue;
271
+ }
272
+ });
273
+
274
+ constraints = mergeConjunctiveNumericConstraints(constraints, shared);
275
+ }
276
+
277
+ return constraints;
278
+ }
279
+
280
+ function getDirectSchemaStringLengthConstraints(schema: any): {
281
+ minLength?: number;
282
+ maxLength?: number;
283
+ } {
284
+ const minLength = parseNonNegativeInteger(schema?.minLength);
285
+ const maxLength = parseNonNegativeInteger(schema?.maxLength);
286
+
287
+ const constraints: {
288
+ minLength?: number;
289
+ maxLength?: number;
290
+ } = {};
291
+ if (minLength !== undefined) constraints.minLength = minLength;
292
+ if (maxLength !== undefined) constraints.maxLength = maxLength;
293
+
294
+ return constraints;
295
+ }
296
+
297
+ function mergeConjunctiveStringLengthConstraints(
298
+ base: {
299
+ minLength?: number;
300
+ maxLength?: number;
301
+ },
302
+ next: {
303
+ minLength?: number;
304
+ maxLength?: number;
305
+ },
306
+ ): {
307
+ minLength?: number;
308
+ maxLength?: number;
309
+ } {
310
+ const merged: {
311
+ minLength?: number;
312
+ maxLength?: number;
313
+ } = {};
314
+
315
+ const minLengthCandidates = [base.minLength, next.minLength].filter(
316
+ (value): value is number => value !== undefined,
317
+ );
318
+ const maxLengthCandidates = [base.maxLength, next.maxLength].filter(
319
+ (value): value is number => value !== undefined,
320
+ );
321
+
322
+ if (minLengthCandidates.length > 0) {
323
+ merged.minLength = Math.max(...minLengthCandidates);
324
+ }
325
+ if (maxLengthCandidates.length > 0) {
326
+ merged.maxLength = Math.min(...maxLengthCandidates);
327
+ }
328
+
329
+ return merged;
330
+ }
331
+
332
+ function getSchemaStringLengthConstraints(
333
+ schema: any,
334
+ seen = new Set<any>(),
335
+ ): {
336
+ minLength?: number;
337
+ maxLength?: number;
338
+ } {
339
+ if (!schema || typeof schema !== "object") return {};
340
+ if (seen.has(schema)) return {};
341
+
342
+ const nextSeen = new Set(seen);
343
+ nextSeen.add(schema);
344
+
345
+ let constraints = getDirectSchemaStringLengthConstraints(schema);
346
+
347
+ if (Array.isArray(schema.allOf)) {
348
+ schema.allOf.forEach((part: any) => {
349
+ constraints = mergeConjunctiveStringLengthConstraints(
350
+ constraints,
351
+ getSchemaStringLengthConstraints(part, nextSeen),
352
+ );
353
+ });
354
+ }
355
+
356
+ const variants = getSchemaVariants(schema);
357
+ if (variants.length > 0) {
358
+ const variantConstraints = variants.map((variant) =>
359
+ getSchemaStringLengthConstraints(variant, nextSeen),
360
+ );
361
+ const shared: {
362
+ minLength?: number;
363
+ maxLength?: number;
364
+ } = {};
365
+
366
+ (["minLength", "maxLength"] as const).forEach((key) => {
367
+ const firstValue = variantConstraints[0]?.[key];
368
+ if (firstValue === undefined) return;
369
+ const allMatch = variantConstraints.every(
370
+ (variantConstraint) => variantConstraint[key] === firstValue,
371
+ );
372
+ if (allMatch) {
373
+ shared[key] = firstValue;
374
+ }
375
+ });
376
+
377
+ constraints = mergeConjunctiveStringLengthConstraints(constraints, shared);
378
+ }
379
+
380
+ return constraints;
381
+ }
382
+
383
+ function getSchemaTypeLabel(schema: any): string {
384
+ if (!schema) return "unknown";
385
+
386
+ const variantSchemas: any[] = getSchemaVariants(schema);
387
+ if (variantSchemas.length > 0) {
388
+ const labels = Array.from(
389
+ new Set(
390
+ variantSchemas
391
+ .map((variant) => getSchemaTypeLabel(variant))
392
+ .filter((label) => label.length > 0),
393
+ ),
394
+ );
395
+ if (labels.length === 0) return "unknown";
396
+ return labels.length === 1 ? labels[0] : labels.join(" | ");
397
+ }
398
+
399
+ if (schema.type === "array") {
400
+ const itemType = getSchemaTypeLabel(schema.items);
401
+ return itemType.includes(" | ") ? `(${itemType})[]` : `${itemType}[]`;
402
+ }
403
+
404
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
405
+ return `enum<${schema.type || "string"}>`;
406
+ }
407
+
408
+ if (schema.format && schema.type) {
409
+ return `${schema.type} (${schema.format})`;
410
+ }
411
+
412
+ if (schema.type) return schema.type;
413
+ if (schema.properties || schema.allOf || schema.oneOf || schema.anyOf) {
414
+ return "object";
415
+ }
416
+ return "unknown";
417
+ }
418
+
419
+ function getSchemaVariantType(schema: any): "oneOf" | "anyOf" | undefined {
420
+ if (Array.isArray(schema?.oneOf) && schema.oneOf.length > 0) return "oneOf";
421
+ if (Array.isArray(schema?.anyOf) && schema.anyOf.length > 0) return "anyOf";
422
+ return undefined;
423
+ }
424
+
425
+ function getAdditionalPropertiesSchema(schema: any): any {
426
+ if (!schema || typeof schema !== "object") return undefined;
427
+
428
+ let additionalProperties = schema.additionalProperties;
429
+ if (Array.isArray(schema.allOf)) {
430
+ schema.allOf.forEach((part: any) => {
431
+ const partAdditional = getAdditionalPropertiesSchema(part);
432
+ if (partAdditional !== undefined) {
433
+ additionalProperties = partAdditional;
434
+ }
435
+ });
436
+ }
437
+
438
+ return additionalProperties;
439
+ }
440
+
441
+ function extractVariantData(
442
+ schema: any,
443
+ parentName: string,
444
+ fallbackRequired: string[] = [],
445
+ ): {
446
+ variants?: ResponseFieldVariant[];
447
+ variantType?: "oneOf" | "anyOf";
448
+ } {
449
+ const variantType = getSchemaVariantType(schema);
450
+ const variantSchemas =
451
+ variantType === "oneOf"
452
+ ? schema?.oneOf || []
453
+ : variantType === "anyOf"
454
+ ? schema?.anyOf || []
455
+ : [];
456
+
457
+ if (!variantType || variantSchemas.length === 0) return {};
458
+
459
+ const variants = variantSchemas
460
+ .map((variantSchema: any, index: number) => ({
461
+ label: `Variant ${index + 1}`,
462
+ fields: extractFieldsFromSchema(
463
+ variantSchema,
464
+ parentName,
465
+ Array.isArray(variantSchema?.required)
466
+ ? variantSchema.required
467
+ : fallbackRequired,
468
+ ),
469
+ }))
470
+ .filter((variant) => variant.fields.length > 0);
471
+
472
+ if (variants.length === 0) return {};
473
+ return { variants, variantType };
24
474
  }
25
475
 
26
- // Helper function to extract fields from a schema recursively
27
476
  function extractFieldsFromSchema(
28
477
  schema: any,
29
478
  parentName: string = "",
30
- required: string[] = []
479
+ required: string[] = [],
31
480
  ): ResponseField[] {
32
481
  if (!schema) return [];
33
482
 
34
483
  const fields: ResponseField[] = [];
484
+ const baseSchema = { ...schema };
485
+ delete baseSchema.oneOf;
486
+ delete baseSchema.anyOf;
35
487
 
36
488
  // Handle allOf - merge properties and required arrays
37
- let properties = schema.properties || {};
38
- let schemaRequired = schema.required || [];
489
+ let properties = baseSchema.properties || {};
490
+ let schemaRequired = Array.isArray(baseSchema.required)
491
+ ? baseSchema.required
492
+ : [];
39
493
 
40
- if (schema.allOf) {
41
- schema.allOf.forEach((s: any) => {
494
+ if (Array.isArray(baseSchema.allOf)) {
495
+ baseSchema.allOf.forEach((s: any) => {
42
496
  if (s.properties) {
43
497
  properties = { ...properties, ...s.properties };
44
498
  }
@@ -50,22 +504,37 @@ function extractFieldsFromSchema(
50
504
 
51
505
  // Handle array types - extract fields from items
52
506
  if (schema.type === "array" && schema.items) {
507
+ const itemSchema = schema.items;
508
+ const baseItemSchema = { ...itemSchema };
509
+ delete baseItemSchema.oneOf;
510
+ delete baseItemSchema.anyOf;
511
+
53
512
  const itemFields = extractFieldsFromSchema(
54
- schema.items,
513
+ baseItemSchema,
55
514
  "",
56
- schema.items.required || []
515
+ Array.isArray(baseItemSchema.required) ? baseItemSchema.required : [],
57
516
  );
58
- if (itemFields.length > 0) {
517
+ const itemVariantData = extractVariantData(
518
+ itemSchema,
519
+ "",
520
+ Array.isArray(itemSchema?.required) ? itemSchema.required : [],
521
+ );
522
+ if (itemFields.length > 0 || itemVariantData.variants) {
59
523
  fields.push({
60
524
  name: parentName || "items",
61
525
  required: required.includes(parentName) || false,
62
- type: "array",
526
+ type: getSchemaTypeLabel(schema),
63
527
  description: schema.description || "",
64
- nested: itemFields,
528
+ ...getSchemaStringLengthConstraints(schema),
529
+ ...getSchemaNumericConstraints(schema),
530
+ nested: itemFields.length > 0 ? itemFields : undefined,
531
+ variants: itemVariantData.variants,
532
+ variantType: itemVariantData.variantType,
65
533
  });
66
534
  }
67
535
  return fields;
68
536
  }
537
+
69
538
  // Extract object properties
70
539
  if (properties && Object.keys(properties).length > 0) {
71
540
  Object.entries(properties)
@@ -79,66 +548,126 @@ function extractFieldsFromSchema(
79
548
  .forEach(([name, propSchema]: [string, any]) => {
80
549
  const fullName = parentName ? `${parentName}.${name}` : name;
81
550
  const isRequired = schemaRequired.includes(name);
551
+ const variantData = extractVariantData(
552
+ propSchema,
553
+ fullName,
554
+ Array.isArray(propSchema?.required) ? propSchema.required : [],
555
+ );
82
556
 
83
- // Determine type
84
- let type = propSchema.type || "object";
85
- if (propSchema.format) {
86
- type = `${type} (${propSchema.format})`;
87
- } else if (propSchema.enum) {
88
- type = `enum<${propSchema.type || "string"}>`;
89
- }
90
-
91
- // Handle nested objects
92
557
  if (
93
558
  propSchema.type === "object" ||
94
559
  propSchema.properties ||
95
- propSchema.allOf
560
+ propSchema.allOf ||
561
+ propSchema.oneOf ||
562
+ propSchema.anyOf
96
563
  ) {
564
+ const basePropSchema = { ...propSchema };
565
+ delete basePropSchema.oneOf;
566
+ delete basePropSchema.anyOf;
567
+
97
568
  const nestedFields = extractFieldsFromSchema(
98
- propSchema,
569
+ basePropSchema,
99
570
  fullName,
100
- propSchema.required || []
571
+ Array.isArray(basePropSchema.required) ? basePropSchema.required : [],
101
572
  );
102
573
  fields.push({
103
574
  name: fullName,
104
575
  required: isRequired,
105
- type: type,
576
+ type: getSchemaTypeLabel(propSchema),
106
577
  description: propSchema.description || "",
578
+ ...getSchemaStringLengthConstraints(propSchema),
579
+ ...getSchemaNumericConstraints(propSchema),
107
580
  nested: nestedFields.length > 0 ? nestedFields : undefined,
581
+ variants: variantData.variants,
582
+ variantType: variantData.variantType,
108
583
  enum: propSchema.enum,
109
584
  });
110
- }
111
- // Handle arrays
112
- else if (propSchema.type === "array" && propSchema.items) {
585
+ } else if (propSchema.type === "array" && propSchema.items) {
586
+ const itemSchema = propSchema.items;
587
+ const baseItemSchema = { ...itemSchema };
588
+ delete baseItemSchema.oneOf;
589
+ delete baseItemSchema.anyOf;
590
+
113
591
  const itemFields = extractFieldsFromSchema(
114
- propSchema.items,
592
+ baseItemSchema,
115
593
  `${fullName}[]`,
116
- propSchema.items.required || []
594
+ Array.isArray(baseItemSchema.required) ? baseItemSchema.required : [],
595
+ );
596
+ const itemVariantData = extractVariantData(
597
+ itemSchema,
598
+ `${fullName}[]`,
599
+ Array.isArray(itemSchema?.required) ? itemSchema.required : [],
117
600
  );
118
601
  fields.push({
119
602
  name: fullName,
120
603
  required: isRequired,
121
- type: propSchema.items?.enum
122
- ? `enum<${propSchema.items?.type || "object"}>[]`
123
- : `${propSchema.items.type || "object"}[]`,
604
+ type: getSchemaTypeLabel(propSchema),
124
605
  description: propSchema.description || "",
606
+ ...getSchemaStringLengthConstraints(propSchema),
607
+ ...getSchemaNumericConstraints(propSchema),
125
608
  nested: itemFields.length > 0 ? itemFields : undefined,
126
- enum: propSchema.items?.enum,
609
+ variants: itemVariantData.variants,
610
+ variantType: itemVariantData.variantType,
611
+ enum: itemSchema?.enum,
127
612
  });
128
- }
129
- // Handle primitive types
130
- else {
613
+ } else {
131
614
  fields.push({
132
615
  name: fullName,
133
616
  required: isRequired,
134
- type: type,
617
+ type: getSchemaTypeLabel(propSchema),
135
618
  description: propSchema.description || "",
619
+ ...getSchemaStringLengthConstraints(propSchema),
620
+ ...getSchemaNumericConstraints(propSchema),
136
621
  enum: propSchema.enum,
137
622
  });
138
623
  }
139
624
  });
140
625
  }
141
626
 
627
+ const additionalProperties = getAdditionalPropertiesSchema(baseSchema);
628
+ const supportsAdditionalProperties =
629
+ additionalProperties === true ||
630
+ (additionalProperties &&
631
+ typeof additionalProperties === "object" &&
632
+ !Array.isArray(additionalProperties));
633
+
634
+ if (supportsAdditionalProperties) {
635
+ const additionalSchema =
636
+ additionalProperties && typeof additionalProperties === "object"
637
+ ? additionalProperties
638
+ : null;
639
+ const mapFieldName = parentName
640
+ ? `${parentName}.[key: string]`
641
+ : "[key: string]";
642
+ const additionalContent = additionalSchema
643
+ ? extractFieldsFromSchema(
644
+ additionalSchema,
645
+ mapFieldName,
646
+ Array.isArray(additionalSchema?.required) ? additionalSchema.required : [],
647
+ )
648
+ : [];
649
+ const additionalVariantData = additionalSchema
650
+ ? extractVariantData(
651
+ additionalSchema,
652
+ mapFieldName,
653
+ Array.isArray(additionalSchema?.required) ? additionalSchema.required : [],
654
+ )
655
+ : {};
656
+
657
+ fields.push({
658
+ name: mapFieldName,
659
+ required: false,
660
+ type: additionalSchema ? getSchemaTypeLabel(additionalSchema) : "any",
661
+ description: additionalSchema?.description || "",
662
+ ...getSchemaStringLengthConstraints(additionalSchema),
663
+ ...getSchemaNumericConstraints(additionalSchema),
664
+ nested: additionalContent.length > 0 ? additionalContent : undefined,
665
+ variants: additionalVariantData.variants,
666
+ variantType: additionalVariantData.variantType,
667
+ enum: additionalSchema ? getSchemaEnumValues(additionalSchema) : undefined,
668
+ });
669
+ }
670
+
142
671
  return fields;
143
672
  }
144
673
 
@@ -167,14 +696,49 @@ Object.entries(responses)
167
696
  const mediaType = contentType ? response.content?.[contentType] : null;
168
697
  const schema = mediaType?.schema;
169
698
 
170
- const fields: ResponseField[] = schema
171
- ? extractFieldsFromSchema(schema, "", schema.required || [])
172
- : [];
699
+ let fields: ResponseField[] = [];
700
+ let variants: ResponseVariant[] | undefined;
701
+ let variantType: "oneOf" | "anyOf" | undefined;
702
+
703
+ if (schema) {
704
+ const hasOneOf = Array.isArray(schema.oneOf) && schema.oneOf.length > 0;
705
+ const hasAnyOf = Array.isArray(schema.anyOf) && schema.anyOf.length > 0;
706
+
707
+ if (hasOneOf || hasAnyOf) {
708
+ variantType = hasOneOf ? "oneOf" : "anyOf";
709
+ const variantSchemas: any[] = hasOneOf ? schema.oneOf : schema.anyOf;
710
+
711
+ const baseSchema = { ...schema };
712
+ delete baseSchema.oneOf;
713
+ delete baseSchema.anyOf;
714
+
715
+ fields = extractFieldsFromSchema(
716
+ baseSchema,
717
+ "",
718
+ baseSchema.required || [],
719
+ );
720
+
721
+ variants = variantSchemas
722
+ .map((variantSchema, index) => ({
723
+ label: `Variant ${index + 1}`,
724
+ fields: extractFieldsFromSchema(
725
+ variantSchema,
726
+ "",
727
+ variantSchema?.required || [],
728
+ ),
729
+ }))
730
+ .filter((variant) => variant.fields.length > 0);
731
+ } else {
732
+ fields = extractFieldsFromSchema(schema, "", schema.required || []);
733
+ }
734
+ }
173
735
 
174
736
  responseFields.push({
175
737
  statusCode,
176
738
  description: response.description,
177
739
  fields,
740
+ variants,
741
+ variantType,
178
742
  });
179
743
  });
180
744
  ---
@@ -184,40 +748,119 @@ Object.entries(responses)
184
748
  {
185
749
  responseFields.map((response) => (
186
750
  <div class="mt-6">
187
- <div class="flex items-center gap-2 mb-4">
188
- <h5 class="text-lg font-medium">{response.statusCode}</h5>
751
+ <div class="flex items-start gap-2 mb-4">
752
+ <div class="mt-1.5 flex items-center gap-1.5">
753
+ <span
754
+ class:list={[
755
+ "size-[7px] shrink-0 rounded-full",
756
+ getStatusCodeDotClass(response.statusCode),
757
+ ]}
758
+ />
759
+ <h5 class="text-lg font-medium leading-none">
760
+ {response.statusCode}
761
+ </h5>
762
+ </div>
189
763
  {response.description && (
190
- <span class="text-sm text-neutral-600">{response.description}</span>
764
+ <span class="mt-1 text-sm text-neutral-500">
765
+ {response.description}
766
+ </span>
191
767
  )}
192
768
  </div>
193
- {response.fields.length > 0 ? (
194
- <div class="ml-2 space-y-4">
195
- {response.fields.map((field) => (
196
- <div>
197
- <Field
198
- name={field.name}
199
- type={field.type}
200
- optional={!field.required}
201
- description={field.description}
202
- />
203
- {field.nested && field.nested.length > 0 && (
204
- <div class="ml-6 mt-2 space-y-2">
205
- {field.nested.map((nestedField) => (
206
- <Field
207
- name={nestedField.name}
208
- type={nestedField.type}
209
- optional={!nestedField.required}
210
- description={nestedField.description}
211
- />
212
- ))}
769
+ {response.fields.length > 0 && (
770
+ <>
771
+ {response.variants && response.variants.length > 0 && (
772
+ <p class="mb-2 text-xs text-neutral-500">Common fields</p>
773
+ )}
774
+ <div x-data="{ expanded: false }">
775
+ <div
776
+ class="w-full overflow-hidden rounded-xl border border-neutral-200 bg-white transition-colors duration-200"
777
+ x-bind:class="expanded ? 'border-neutral-300' : 'border-neutral-200'"
778
+ >
779
+ <button
780
+ type="button"
781
+ x-on:click="expanded = !expanded"
782
+ x-bind:aria-expanded="expanded"
783
+ class="group flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm font-medium text-neutral-700 hover:bg-neutral-50 cursor-pointer transition-colors duration-200"
784
+ >
785
+ <span class="inline-flex items-center gap-2">
786
+ <ListChevronsToggle class="size-4 shrink-0 text-neutral-500 group-hover:text-neutral-600 transition duration-200" />
787
+ <span>
788
+ {response.fields.length}{" "}
789
+ {response.fields.length === 1
790
+ ? "field"
791
+ : "fields"}
792
+ </span>
793
+ </span>
794
+ </button>
795
+ <div x-show="expanded" x-collapse x-cloak>
796
+ <div class="border-t border-neutral-100 p-3">
797
+ <ResponseFieldTree fields={response.fields} />
213
798
  </div>
214
- )}
799
+ </div>
215
800
  </div>
801
+ </div>
802
+ </>
803
+ )}
804
+ {response.variants && response.variants.length > 0 && (
805
+ <div class="mt-3 space-y-2">
806
+ <p class="text-xs text-neutral-500">
807
+ {response.variantType === "anyOf"
808
+ ? "One or more response variants may apply."
809
+ : "Returns one of these response variants."}
810
+ </p>
811
+ {response.variants.map((variant, index) => (
812
+ <>
813
+ <div class="rounded-xl border border-neutral-200 bg-white p-3">
814
+ <div class="mb-2 text-xs font-medium text-neutral-600">
815
+ {variant.label}
816
+ </div>
817
+ <div x-data="{ expanded: false }">
818
+ <div
819
+ class="w-full overflow-hidden rounded-lg border border-neutral-200 bg-white transition-colors duration-200"
820
+ x-bind:class="expanded ? 'border-neutral-300' : 'border-neutral-200'"
821
+ >
822
+ <button
823
+ type="button"
824
+ x-on:click="expanded = !expanded"
825
+ x-bind:aria-expanded="expanded"
826
+ class="group flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-xs font-medium text-neutral-700 hover:bg-neutral-50 cursor-pointer transition-colors duration-200"
827
+ >
828
+ <span class="inline-flex items-center gap-2">
829
+ <ListChevronsToggle class="size-4 shrink-0 text-neutral-400 group-hover:text-neutral-600 transition duration-200" />
830
+ <span>
831
+ {variant.fields.length}{" "}
832
+ {variant.fields.length === 1
833
+ ? "field"
834
+ : "fields"}
835
+ </span>
836
+ </span>
837
+ </button>
838
+ <div x-show="expanded" x-collapse x-cloak>
839
+ <div class="border-t border-neutral-100 px-3 py-3">
840
+ <ResponseFieldTree fields={variant.fields} />
841
+ </div>
842
+ </div>
843
+ </div>
844
+ </div>
845
+ </div>
846
+ {response.variantType === "oneOf" &&
847
+ index < response.variants.length - 1 && (
848
+ <div class="flex items-center gap-2 py-0">
849
+ <div class="h-px flex-1 bg-neutral-200" />
850
+ <span class="px-1 text-[10px] uppercase tracking-wide text-neutral-500">
851
+ OR
852
+ </span>
853
+ <div class="h-px flex-1 bg-neutral-200" />
854
+ </div>
855
+ )}
856
+ </>
216
857
  ))}
217
858
  </div>
218
- ) : (
219
- <p class="text-sm text-neutral-500 ml-2">No response body fields</p>
220
859
  )}
860
+ {response.fields.length === 0 &&
861
+ (!response.variants || response.variants.length === 0) ? (
862
+ <p class="text-sm text-neutral-500 ml-2">No response body fields</p>
863
+ ) : null}
221
864
  </div>
222
865
  ))
223
866
  }