/** * Structured renderers for OpenAPI parameters, request bodies, and responses. * Replaces raw JSON.stringify output with readable tables and schema trees. */ import { useI18n } from '../lib/i18n'; /* ===== Helpers ===== */ type SchemaObj = { type?: string; format?: string; description?: string; enum?: unknown[]; items?: SchemaObj; properties?: Record; required?: string[]; additionalProperties?: boolean | SchemaObj; oneOf?: SchemaObj[]; anyOf?: SchemaObj[]; allOf?: SchemaObj[]; default?: unknown; example?: unknown; nullable?: boolean; minimum?: number; maximum?: number; minLength?: number; maxLength?: number; pattern?: string; [key: string]: unknown; }; type Parameter = { name: string; in: string; required?: boolean; description?: string; schema?: SchemaObj; type?: string; format?: string; enum?: unknown[]; [key: string]: unknown; }; function resolveType(schema?: SchemaObj): string { if (!schema) return '—'; if (schema.type === 'array' && schema.items) { return `${resolveType(schema.items)}[]`; } if (schema.oneOf) return schema.oneOf.map(resolveType).join(' | '); if (schema.anyOf) return schema.anyOf.map(resolveType).join(' | '); return schema.type || '—'; } function TypeBadge({ type }: { type: string }) { const colorMap: Record = { string: 'text-[#30a46c] bg-[rgba(48,164,108,0.08)]', integer: 'text-[#3b82f6] bg-[rgba(59,130,246,0.08)]', number: 'text-[#3b82f6] bg-[rgba(59,130,246,0.08)]', boolean: 'text-[#e5a000] bg-[rgba(229,160,0,0.08)]', object: 'text-[#8b5cf6] bg-[rgba(139,92,246,0.08)]', array: 'text-[#e5484d] bg-[rgba(229,72,77,0.08)]', }; const base = type.replace('[]', ''); const cls = colorMap[base] || 'text-text-muted bg-bg-tertiary'; return ( {type} ); } function InBadge({ location }: { location: string }) { return ( {location} ); } /* ===== Parameters Table ===== */ export function ParametersView({ parameters }: { parameters: unknown }) { const { t } = useI18n(); if (!Array.isArray(parameters) || parameters.length === 0) return null; const params = parameters as Parameter[]; return (

{t('dashboard.schema.parameters')}

{params.map((p, i) => { const type = resolveType(p.schema) || p.type || '—'; const format = p.schema?.format || p.format; const enumVals = p.schema?.enum || p.enum; return ( ); })}
{t('dashboard.schema.name')} {t('dashboard.schema.in')} {t('dashboard.schema.type')} {t('dashboard.schema.required')} {t('dashboard.schema.descriptionCol')}
{p.name}
{format && ( ({format}) )}
{p.required ? ( {t('dashboard.schema.required')} ) : ( {t('dashboard.schema.optional')} )}
{p.description && {p.description}} {enumVals && enumVals.length > 0 && (
{t('dashboard.schema.enum')} {enumVals.map((v, j) => ( {String(v)} ))}
)} {p.schema?.default !== undefined && (
{t('dashboard.schema.default')} {JSON.stringify(p.schema.default)}
)}
); } /* ===== Schema Properties Tree ===== */ function SchemaProperties({ schema, depth = 0 }: { schema: SchemaObj; depth?: number }) { const { t } = useI18n(); const properties = schema.properties; const requiredSet = new Set(schema.required || []); if (!properties || Object.keys(properties).length === 0) { // Just show the type if no properties if (schema.type) { return (
{schema.description && {schema.description}}
); } return null; } return (
0 ? 'ml-4 border-l border-border-muted pl-3 mt-1' : ''}> {Object.entries(properties).map(([name, prop]) => { const type = resolveType(prop); const hasChildren = prop.type === 'object' && prop.properties; const isArray = prop.type === 'array' && prop.items?.properties; return (
{name} {prop.format && ( ({prop.format}) )} {requiredSet.has(name) && ( {t('dashboard.schema.required')} )} {prop.nullable && ( {t('dashboard.schema.nullable')} )} {prop.description && ( {prop.description} )}
{prop.enum && prop.enum.length > 0 && (
{t('dashboard.schema.enum')} {prop.enum.map((v, j) => ( {String(v)} ))}
)} {prop.default !== undefined && (
{t('dashboard.schema.default')} {JSON.stringify(prop.default)}
)} {hasChildren && } {isArray && prop.items && }
); })}
); } /* ===== Request Body ===== */ export function RequestBodyView({ requestBody }: { requestBody: unknown }) { const { t } = useI18n(); if (!requestBody || typeof requestBody !== 'object') return null; const body = requestBody as { required?: boolean; description?: string; content?: Record; schema?: SchemaObj; // Swagger 2.0 converted format }; // Swagger 2.0 format: { schema: {...} } if (body.schema && !body.content) { return (

{t('dashboard.schema.requestBody')} {body.required && {t('dashboard.schema.required')}}

); } // OpenAPI 3.x format: { content: { "application/json": { schema: {...} } } } if (!body.content) return null; const contentTypes = Object.entries(body.content); return (

{t('dashboard.schema.requestBody')} {body.required && {t('dashboard.schema.required')}}

{body.description && (

{body.description}

)} {contentTypes.map(([contentType, media]) => (
{contentType}
{media.schema ? ( media.schema.properties ? ( ) : (
{media.schema.description && {media.schema.description}}
) ) : ( {t('dashboard.schema.noSchema')} )}
))}
); } /* ===== Responses ===== */ function StatusBadge({ code }: { code: string }) { const n = parseInt(code, 10); let cls = 'text-text-muted bg-bg-tertiary'; if (n >= 200 && n < 300) cls = 'text-[#30a46c] bg-[rgba(48,164,108,0.08)]'; else if (n >= 300 && n < 400) cls = 'text-[#3b82f6] bg-[rgba(59,130,246,0.08)]'; else if (n >= 400 && n < 500) cls = 'text-[#e5a000] bg-[rgba(229,160,0,0.08)]'; else if (n >= 500) cls = 'text-[#e5484d] bg-[rgba(229,72,77,0.08)]'; return ( {code} ); } export function ResponsesView({ responses }: { responses: unknown }) { const { t } = useI18n(); if (!responses || typeof responses !== 'object') return null; const entries = Object.entries(responses as Record); if (entries.length === 0) return null; return (

{t('dashboard.schema.responses')}

{entries.map(([code, resp]) => { const response = resp as { description?: string; content?: Record; schema?: SchemaObj; // Swagger 2.0 }; // Find schema from content or direct schema (Swagger 2) let schema: SchemaObj | undefined; let contentType: string | undefined; if (response.content) { const firstEntry = Object.entries(response.content)[0]; if (firstEntry) { contentType = firstEntry[0]; schema = firstEntry[1].schema; } } else if (response.schema) { schema = response.schema; } return (
{response.description && ( {response.description} )} {contentType && ( {contentType} )}
{schema && (schema.properties || schema.items?.properties || schema.type) && (
{schema.properties ? ( ) : schema.type === 'array' && schema.items?.properties ? (
{t('dashboard.schema.ofObjects')}
) : (
{schema.description && {schema.description}}
)}
)}
); })}
); }