|
| 1 | +type JsonSchema = { |
| 2 | + type?: string | string[] |
| 3 | + format?: string |
| 4 | + enum?: (string | number | boolean)[] |
| 5 | + default?: unknown |
| 6 | + deprecated?: boolean |
| 7 | + readOnly?: boolean |
| 8 | + minimum?: number |
| 9 | + maximum?: number |
| 10 | + minLength?: number |
| 11 | + maxLength?: number |
| 12 | + pattern?: string |
| 13 | + minItems?: number |
| 14 | + maxItems?: number |
| 15 | + uniqueItems?: boolean |
| 16 | + additionalProperties?: boolean | JsonSchema |
| 17 | + properties?: Record<string, JsonSchema> |
| 18 | + required?: string[] |
| 19 | + items?: JsonSchema |
| 20 | + oneOf?: JsonSchema[] |
| 21 | + anyOf?: JsonSchema[] |
| 22 | + allOf?: JsonSchema[] |
| 23 | + title?: string |
| 24 | + description?: string |
| 25 | + examples?: unknown[] |
| 26 | + example?: unknown |
| 27 | +} |
| 28 | + |
| 29 | +const MAX_DEPTH = 4 |
| 30 | + |
| 31 | +function renderTypeConstraints(schema: JsonSchema): string { |
| 32 | + const parts: string[] = [] |
| 33 | + |
| 34 | + const t = schema.type |
| 35 | + if (t) { |
| 36 | + parts.push(Array.isArray(t) ? t.join(' or ') : t) |
| 37 | + } |
| 38 | + if (schema.format) parts.push(`format: ${schema.format}`) |
| 39 | + if (schema.enum) { |
| 40 | + const vals = schema.enum.map((v) => `\`${v}\``).join(', ') |
| 41 | + parts.push(`enum: ${vals}`) |
| 42 | + } |
| 43 | + if (schema.default !== undefined) parts.push(`default: \`${JSON.stringify(schema.default)}\``) |
| 44 | + if (schema.deprecated) parts.push('deprecated') |
| 45 | + if (schema.readOnly) parts.push('read-only') |
| 46 | + if (schema.minimum !== undefined) parts.push(`minimum: ${schema.minimum}`) |
| 47 | + if (schema.maximum !== undefined) parts.push(`maximum: ${schema.maximum}`) |
| 48 | + if (schema.minLength !== undefined) parts.push(`minLength: ${schema.minLength}`) |
| 49 | + if (schema.maxLength !== undefined) parts.push(`maxLength: ${schema.maxLength}`) |
| 50 | + if (schema.pattern) parts.push(`pattern: \`${schema.pattern}\``) |
| 51 | + if (schema.minItems !== undefined) parts.push(`minItems: ${schema.minItems}`) |
| 52 | + if (schema.maxItems !== undefined) parts.push(`maxItems: ${schema.maxItems}`) |
| 53 | + if (schema.uniqueItems) parts.push('unique items') |
| 54 | + if (schema.additionalProperties === true) { |
| 55 | + parts.push('additional properties allowed') |
| 56 | + } else if (typeof schema.additionalProperties === 'object') { |
| 57 | + parts.push(`additional properties: ${renderTypeConstraints(schema.additionalProperties)}`) |
| 58 | + } |
| 59 | + |
| 60 | + return parts.join(', ') || 'object' |
| 61 | +} |
| 62 | + |
| 63 | +function renderCompositionVariants( |
| 64 | + keyword: string, |
| 65 | + variants: JsonSchema[], |
| 66 | + indent: number, |
| 67 | + depth: number, |
| 68 | +): string { |
| 69 | + const prefix = ' '.repeat(indent) |
| 70 | + const label = keyword.replace('Of', ' of') |
| 71 | + const lines: string[] = [`${prefix}* ${label}:`] |
| 72 | + |
| 73 | + for (const variant of variants) { |
| 74 | + const name = variant.title || renderTypeConstraints(variant) |
| 75 | + lines.push(`${prefix} * **${name}**`) |
| 76 | + if (depth < MAX_DEPTH && variant.properties) { |
| 77 | + const nested = renderProperties(variant, indent + 2, depth + 1) |
| 78 | + if (nested) lines.push(nested) |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + return lines.join('\n') |
| 83 | +} |
| 84 | + |
| 85 | +function renderProperties( |
| 86 | + schema: JsonSchema, |
| 87 | + indent: number, |
| 88 | + depth: number, |
| 89 | + requiredFields?: string[], |
| 90 | +): string { |
| 91 | + const props = schema.properties || {} |
| 92 | + const req = new Set(requiredFields || schema.required || []) |
| 93 | + const prefix = ' '.repeat(indent) |
| 94 | + const lines: string[] = [] |
| 95 | + |
| 96 | + for (const [name, prop] of Object.entries(props)) { |
| 97 | + const reqStr = req.has(name) ? 'required, ' : '' |
| 98 | + |
| 99 | + // Check for composition keywords at property level |
| 100 | + const compositionKey = (['oneOf', 'anyOf', 'allOf'] as const).find((k) => prop[k]) |
| 101 | + if (compositionKey) { |
| 102 | + const label = compositionKey.replace('Of', ' of') |
| 103 | + lines.push(`${prefix}* \`${name}\`: ${reqStr}${label}:`) |
| 104 | + for (const variant of prop[compositionKey]!) { |
| 105 | + const vName = variant.title || renderTypeConstraints(variant) |
| 106 | + lines.push(`${prefix} * **${vName}**`) |
| 107 | + if (depth < MAX_DEPTH && variant.properties) { |
| 108 | + const nested = renderProperties(variant, indent + 2, depth + 1) |
| 109 | + if (nested) lines.push(nested) |
| 110 | + } |
| 111 | + } |
| 112 | + continue |
| 113 | + } |
| 114 | + |
| 115 | + const propType = Array.isArray(prop.type) ? prop.type[0] : prop.type |
| 116 | + |
| 117 | + if (propType === 'array' && prop.items) { |
| 118 | + const itemTitle = prop.items.title |
| 119 | + if (prop.items.properties && depth < MAX_DEPTH) { |
| 120 | + const label = itemTitle ? `array of \`${itemTitle}\`` : 'array of objects' |
| 121 | + lines.push(`${prefix}* \`${name}\`: ${reqStr}${label}:`) |
| 122 | + lines.push(renderProperties(prop.items, indent + 1, depth + 1)) |
| 123 | + } else { |
| 124 | + lines.push(`${prefix}* \`${name}\`: ${reqStr}array of ${renderTypeConstraints(prop.items)}`) |
| 125 | + } |
| 126 | + } else if (prop.properties && depth < MAX_DEPTH) { |
| 127 | + const label = prop.title ? `\`${prop.title}\`` : renderTypeConstraints(prop) |
| 128 | + lines.push(`${prefix}* \`${name}\`: ${reqStr}${label}:`) |
| 129 | + lines.push(renderProperties(prop, indent + 1, depth + 1)) |
| 130 | + } else { |
| 131 | + lines.push(`${prefix}* \`${name}\`: ${reqStr}${renderTypeConstraints(prop)}`) |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + return lines.filter(Boolean).join('\n') |
| 136 | +} |
| 137 | + |
| 138 | +/** |
| 139 | + * Converts a JSON Schema response object into a readable markdown bullet list. |
| 140 | + * Includes type, required, format, enum, default, constraints — but omits |
| 141 | + * examples and descriptions to keep the output compact. |
| 142 | + */ |
| 143 | +export function summarizeSchema(schema: JsonSchema): string { |
| 144 | + if (!schema || typeof schema !== 'object') return '' |
| 145 | + |
| 146 | + // Handle top-level composition |
| 147 | + for (const keyword of ['oneOf', 'anyOf', 'allOf'] as const) { |
| 148 | + if (schema[keyword]) { |
| 149 | + return renderCompositionVariants(keyword, schema[keyword]!, 0, 0) |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + // Handle top-level array |
| 154 | + if (schema.type === 'array' && schema.items) { |
| 155 | + const items = schema.items |
| 156 | + const constraints: string[] = [] |
| 157 | + if (schema.minItems !== undefined) constraints.push(`minItems: ${schema.minItems}`) |
| 158 | + if (schema.maxItems !== undefined) constraints.push(`maxItems: ${schema.maxItems}`) |
| 159 | + if (schema.uniqueItems) constraints.push('unique items') |
| 160 | + const constraintStr = constraints.length ? ` (${constraints.join(', ')})` : '' |
| 161 | + const itemTitle = items.title |
| 162 | + |
| 163 | + // Composition inside items |
| 164 | + const compositionKey = (['oneOf', 'anyOf', 'allOf'] as const).find((k) => items[k]) |
| 165 | + if (compositionKey) { |
| 166 | + const label = compositionKey.replace('Of', ' of') |
| 167 | + const titlePart = itemTitle ? `\`${itemTitle}\` ` : '' |
| 168 | + const lines = [`Array${constraintStr} of ${titlePart}objects: ${label}:`] |
| 169 | + for (const variant of items[compositionKey]!) { |
| 170 | + const name = variant.title || renderTypeConstraints(variant) |
| 171 | + lines.push(` * **${name}**`) |
| 172 | + if (variant.properties) { |
| 173 | + const nested = renderProperties(variant, 2, 1) |
| 174 | + if (nested) lines.push(nested) |
| 175 | + } |
| 176 | + } |
| 177 | + return lines.join('\n') |
| 178 | + } |
| 179 | + |
| 180 | + if (items.properties) { |
| 181 | + const label = itemTitle ? `\`${itemTitle}\`` : 'objects' |
| 182 | + return `Array${constraintStr} of ${label}:\n${renderProperties(items, 1, 1)}` |
| 183 | + } |
| 184 | + |
| 185 | + return `Array${constraintStr} of ${renderTypeConstraints(items)}` |
| 186 | + } |
| 187 | + |
| 188 | + // Handle top-level object |
| 189 | + if (schema.properties) { |
| 190 | + return renderProperties(schema, 0, 0) |
| 191 | + } |
| 192 | + |
| 193 | + return renderTypeConstraints(schema) |
| 194 | +} |
0 commit comments