b122de518c
File path = route. JSON/JSON5, dynamic .ts handlers + faker, templating, stateful CRUD (pagination/sort/persist), __variants, sequence, chaos, rate-limit, auth, validation, per-route + global proxy/record, splat routes, X-Scenario overlays, SSE. Visual docs (Scalar) at /__docs from a live OpenAPI 3 spec. OpenAPI import script. Zero-build via tsx. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
/**
|
||
* Generate mock route files from an OpenAPI 3 spec (json or yaml).
|
||
*
|
||
* npm run openapi -- path/to/openapi.yaml [outDir]
|
||
*
|
||
* For each path × method it writes a route file whose body is the response
|
||
* example (or a synthesized one from the schema). Non-200 success codes land
|
||
* in the filename (e.g. users.post.201.json).
|
||
*/
|
||
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
||
import { dirname, join } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import YAML from 'yaml'
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||
const specPath = process.argv[2]
|
||
const outDir = process.argv[3] ?? join(__dirname, '..', 'routes')
|
||
|
||
if (!specPath) {
|
||
console.error('usage: npm run openapi -- <spec.yaml|json> [outDir]')
|
||
process.exit(1)
|
||
}
|
||
|
||
const text = readFileSync(specPath, 'utf8')
|
||
const spec: any = specPath.endsWith('.json') ? JSON.parse(text) : YAML.parse(text)
|
||
|
||
function resolveRef(ref: string): any {
|
||
// #/components/schemas/Foo
|
||
const parts = ref.replace(/^#\//, '').split('/')
|
||
return parts.reduce((o, k) => o?.[k], spec)
|
||
}
|
||
|
||
function example(schema: any, depth = 0): any {
|
||
if (!schema || depth > 6) return null
|
||
if (schema.$ref) return example(resolveRef(schema.$ref), depth + 1)
|
||
if (schema.example !== undefined) return schema.example
|
||
if (schema.default !== undefined) return schema.default
|
||
if (schema.enum) return schema.enum[0]
|
||
if (schema.allOf) return Object.assign({}, ...schema.allOf.map((s: any) => example(s, depth + 1)))
|
||
if (schema.oneOf || schema.anyOf) return example((schema.oneOf ?? schema.anyOf)[0], depth + 1)
|
||
|
||
switch (schema.type) {
|
||
case 'object': {
|
||
const out: Record<string, unknown> = {}
|
||
for (const [k, v] of Object.entries(schema.properties ?? {})) out[k] = example(v, depth + 1)
|
||
return out
|
||
}
|
||
case 'array':
|
||
return [example(schema.items, depth + 1)]
|
||
case 'integer':
|
||
case 'number':
|
||
return schema.format === 'float' ? 1.5 : 1
|
||
case 'boolean':
|
||
return true
|
||
case 'string':
|
||
if (schema.format === 'date-time') return '2026-01-01T00:00:00Z'
|
||
if (schema.format === 'email') return 'user@example.com'
|
||
if (schema.format === 'uuid') return '00000000-0000-0000-0000-000000000000'
|
||
return 'string'
|
||
default:
|
||
return schema.properties ? example({ ...schema, type: 'object' }, depth) : null
|
||
}
|
||
}
|
||
|
||
function pickResponse(op: any): { status: number; body: unknown } | null {
|
||
const responses = op.responses ?? {}
|
||
const code = Object.keys(responses).find((c) => /^2\d\d$/.test(c)) ?? Object.keys(responses)[0]
|
||
if (!code) return null
|
||
const r = responses[code]
|
||
const json = r?.content?.['application/json']
|
||
let body: unknown = {}
|
||
if (json?.example !== undefined) body = json.example
|
||
else if (json?.examples) body = (Object.values(json.examples)[0] as any)?.value ?? {}
|
||
else if (json?.schema) body = example(json.schema)
|
||
return { status: Number(code) || 200, body }
|
||
}
|
||
|
||
function fileFor(path: string, method: string, status: number): string {
|
||
// /users/{id} -> users/[id]
|
||
const segs = path.split('/').filter(Boolean).map((s) => s.replace(/^\{(.+)\}$/, '[$1]'))
|
||
const base = segs.length ? segs.join('/') : 'index'
|
||
const parts = [base]
|
||
if (method !== 'get') parts.push(method)
|
||
if (status !== 200 && status !== 0) parts.push(String(status))
|
||
return join(outDir, parts.join('.') + '.json')
|
||
}
|
||
|
||
let count = 0
|
||
for (const [path, item] of Object.entries<any>(spec.paths ?? {})) {
|
||
for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
|
||
const op = item[method]
|
||
if (!op) continue
|
||
const picked = pickResponse(op)
|
||
if (!picked) continue
|
||
const file = fileFor(path, method, picked.status)
|
||
mkdirSync(dirname(file), { recursive: true })
|
||
writeFileSync(file, JSON.stringify(picked.body, null, 2) + '\n')
|
||
count++
|
||
console.log(`+ ${method.toUpperCase().padEnd(6)} ${path} -> ${file.replace(outDir + '/', '')}`)
|
||
}
|
||
}
|
||
console.log(`\n${count} route(s) generated into ${outDir}`)
|