Files
mockly-hono/scripts/from-openapi.ts
T
Peter Meier b122de518c feat: mockly-hono — file-based mock API server (Hono)
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>
2026-06-23 12:44:31 +02:00

103 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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}`)