UNPKG

hey-api-builders

Version:

A custom plugin for @hey-api/openapi-ts that generates mock data builders with a lightweight custom runtime, Zod integration, or static mock generation.

288 lines (248 loc) 10.9 kB
import type { IR } from '@hey-api/openapi-ts'; import type { Schema } from 'json-schema-faker'; import type { GeneratedSchemaMeta, EnumSchemaObject, EnumItem, JsonValue, ExtendedSchema, NormalizedSchemaNode, } from './types'; export function irToSchema( ir: IR.SchemaObject, all: Record<string, IR.SchemaObject>, seen = new Set<IR.SchemaObject>() ): Schema { if (!ir || typeof ir !== 'object') return {}; if (seen.has(ir)) return {}; seen.add(ir); if (ir.$ref) { const name = ir.$ref.replace('#/components/schemas/', ''); const target = all[name]; return target ? irToSchema(target, all, seen) : {}; } if (isEnum(ir)) { const enumIr = ir as EnumSchemaObject; if (Array.isArray(enumIr.enum)) { const enumValues = enumIr.enum; const anyOfSchemas = enumValues.map((value: JsonValue) => ({ const: value })); return { anyOf: anyOfSchemas }; } if (enumIr.type === 'enum' && Array.isArray(enumIr.items)) { const enumValues = (enumIr.items as EnumItem[]) .filter((item: EnumItem) => item && typeof item === 'object' && 'const' in item) .map((item: EnumItem) => item.const); if (enumValues.length > 0) { const anyOfSchemas = enumValues.map((value: JsonValue) => ({ const: value })); return { anyOf: anyOfSchemas }; } } if (!enumIr.enum && Array.isArray(enumIr.items)) { const items = enumIr.items as EnumItem[]; if (items.length > 0 && items.every((it: EnumItem) => it && typeof it === 'object' && 'const' in it)) { const enumValues = items.map((it: EnumItem) => it.const); const anyOfSchemas = enumValues.map((value: JsonValue) => ({ const: value })); return { anyOf: anyOfSchemas }; } } } const out: Record<string, unknown> = {}; if (ir.type) out.type = ir.type; else if (ir.properties) out.type = 'object'; else if (ir.items) out.type = 'array'; const extendedIr = ir as EnumSchemaObject; if (extendedIr.nullable) { if (typeof out.type === 'string') out.type = [out.type, 'null']; else if (Array.isArray(out.type) && !out.type.includes('null')) out.type.push('null'); } if (ir.properties) { out.properties = {}; for (const [k, v] of Object.entries(ir.properties)) { (out.properties as Record<string, Schema>)[k] = irToSchema(v as IR.SchemaObject, all, seen); } } if (ir.required && ir.required.length) out.required = [...ir.required]; if (typeof ir.additionalProperties !== 'undefined') { if (typeof ir.additionalProperties === 'boolean') out.additionalProperties = ir.additionalProperties; else out.additionalProperties = irToSchema(ir.additionalProperties as IR.SchemaObject, all, seen); } if (ir.items) { if (Array.isArray(ir.items)) out.items = ir.items.map(i => irToSchema(i as IR.SchemaObject, all, seen)); else out.items = irToSchema(ir.items as IR.SchemaObject, all, seen); } const extendedIrWithComposition = ir as IR.SchemaObject & { allOf?: IR.SchemaObject[]; anyOf?: IR.SchemaObject[]; oneOf?: IR.SchemaObject[]; }; if (extendedIrWithComposition.allOf) out.allOf = extendedIrWithComposition.allOf.map((s: IR.SchemaObject) => irToSchema(s, all, seen)); if (extendedIrWithComposition.anyOf) out.anyOf = extendedIrWithComposition.anyOf.map((s: IR.SchemaObject) => irToSchema(s, all, seen)); if (extendedIrWithComposition.oneOf) out.oneOf = extendedIrWithComposition.oneOf.map((s: IR.SchemaObject) => irToSchema(s, all, seen)); const copy: (keyof IR.SchemaObject | string)[] = [ 'format','pattern','minimum','maximum','exclusiveMinimum','exclusiveMaximum','minLength','maxLength','minItems','maxItems','uniqueItems','minProperties','maxProperties','title','description','default','deprecated','readOnly','writeOnly','example' ]; for (const k of copy) { const val = (ir as Record<string, unknown>)[k]; if (typeof val !== 'undefined') { if (k === 'example') out.examples = [val]; else out[k] = val; } } return out as Schema; } function normalizeSchema(node: NormalizedSchemaNode | ExtendedSchema | Schema): NormalizedSchemaNode { if (!node || typeof node !== 'object') return node as NormalizedSchemaNode; const workingNode = { ...node } as NormalizedSchemaNode; if (workingNode.type === 'enum') { let enumValues: JsonValue[] = []; if (Array.isArray(workingNode.items)) { for (const item of workingNode.items) { if (item && typeof item === 'object' && 'const' in item) { enumValues.push((item as { const: JsonValue }).const); } } } if (enumValues.length > 0) { const primitiveTypes = new Set(enumValues.map(v => (v === null ? 'null' : typeof v))); let inferred: 'null' | 'boolean' | 'object' | 'array' | 'number' | 'string' | 'integer' = primitiveTypes.size === 1 ? [...primitiveTypes][0] as 'null' | 'boolean' | 'object' | 'array' | 'number' | 'string' : 'string'; if (inferred === 'number') { if (enumValues.every(v => typeof v === 'number' && Number.isInteger(v))) inferred = 'integer'; } if (inferred === 'object') inferred = 'string'; workingNode.type = inferred; workingNode.enum = enumValues; } else { workingNode.type = 'string'; } delete workingNode.items; delete workingNode.logicalOperator; } if (Array.isArray(workingNode.items)) { const hasEnumItems = workingNode.items.some((item: NormalizedSchemaNode) => item && typeof item === 'object' && item.type === 'enum' ); if (hasEnumItems) { workingNode.items = workingNode.items.map((item: NormalizedSchemaNode) => { if (item && typeof item === 'object' && item.type === 'enum') { return normalizeSchema(item); } return item; }); } } if ( workingNode.type === 'array' && Array.isArray(workingNode.items) && workingNode.items.length > 0 && workingNode.items.every( (item: NormalizedSchemaNode) => item && typeof item === 'object' && 'type' in item && (item.type === 'string' || item.type === 'null' || item.type === 'number' || item.type === 'integer' || item.type === 'boolean' || item.type === 'object') ) ) { workingNode.anyOf = workingNode.items; delete workingNode.type; delete workingNode.items; } if (workingNode.properties && typeof workingNode.properties === 'object') { for (const key of Object.keys(workingNode.properties)) { const prop = workingNode.properties[key]; if (prop !== undefined) { workingNode.properties[key] = normalizeSchema(prop); } } } if (workingNode.items) { if (Array.isArray(workingNode.items)) workingNode.items = workingNode.items.map(it => normalizeSchema(it)); else workingNode.items = normalizeSchema(workingNode.items); } for (const k of ['allOf','anyOf','oneOf'] as const) { const schemaArray = workingNode[k]; if (Array.isArray(schemaArray)) workingNode[k] = schemaArray.map((s: NormalizedSchemaNode) => normalizeSchema(s)); } return workingNode; } function sanitizeSchema(node: NormalizedSchemaNode): Schema { if (!node || typeof node !== 'object') return node as Schema; const workingNode = { ...node } as Record<string, unknown>; if (workingNode.type === 'enum') { delete workingNode.type; if (Array.isArray(workingNode.enum)) { workingNode.type = 'string'; } } if (workingNode.type === 'unknown') delete workingNode.type; if (!workingNode.type && Array.isArray(workingNode.enum)) workingNode.type = 'string'; delete workingNode.logicalOperator; if (workingNode.properties && typeof workingNode.properties === 'object') { const properties = workingNode.properties as Record<string, NormalizedSchemaNode>; for (const k of Object.keys(properties)) { const prop = properties[k]; if (prop !== undefined) { properties[k] = sanitizeSchema(prop) as NormalizedSchemaNode; } } } if (workingNode.additionalProperties && typeof workingNode.additionalProperties === 'object') { workingNode.additionalProperties = sanitizeSchema(workingNode.additionalProperties as NormalizedSchemaNode); } if (workingNode.items) { if (Array.isArray(workingNode.items)) { workingNode.items = (workingNode.items as NormalizedSchemaNode[]).map(sanitizeSchema); } else { workingNode.items = sanitizeSchema(workingNode.items as NormalizedSchemaNode); } } (['allOf','anyOf','oneOf'] as const).forEach(k => { const schemaArray = workingNode[k]; if (Array.isArray(schemaArray)) { workingNode[k] = (schemaArray as NormalizedSchemaNode[]).map(sanitizeSchema); } }); return workingNode as Schema; } export function isEnum(ir: IR.SchemaObject): boolean { if (!ir || typeof ir !== 'object') return false; const enumIr = ir as EnumSchemaObject; if (Array.isArray(enumIr.enum)) return true; if (enumIr.type === 'enum') return true; if (!enumIr.enum && Array.isArray(enumIr.items)) { const items = enumIr.items as EnumItem[]; if (items.length > 0 && items.every((it: EnumItem) => it && typeof it === 'object' && 'const' in it)) { return true; } } return false; } export function toPascal(str: string): string { return str .replace(/([_-]+|\s+)([a-zA-Z0-9])/g, (_, __, c) => c.toUpperCase()) .replace(/^[a-z]/, c => c.toUpperCase()); } export function safeTypeName(name: string): string { return name.replace(/[^a-zA-Z0-9_]/g, '_'); } export function collectSchemas(all: Record<string, IR.SchemaObject>): GeneratedSchemaMeta[] { const metas: GeneratedSchemaMeta[] = []; for (const [name, irSchema] of Object.entries(all)) { let typeName = name.replace(/Schema$/, ''); typeName = typeName.replace(/^UI([A-Z])/g, 'Ui$1'); typeName = typeName.replace(/^API([A-Z])/g, 'Api$1'); typeName = typeName.replace(/^HTTP([A-Z])/g, 'Http$1'); typeName = typeName.replace(/^URL([A-Z])/g, 'Url$1'); typeName = typeName.replace(/^ID([A-Z])/g, 'Id$1'); const jsf = sanitizeSchema(normalizeSchema(irToSchema(irSchema as IR.SchemaObject, all))); const schemaWithType = jsf as ExtendedSchema; const t = schemaWithType.type; const isObject = t === 'object' || (Array.isArray(t) && t.includes('object')); metas.push({ typeName, constName: `${safeTypeName(typeName)}Schema`, isEnum: isEnum(irSchema as IR.SchemaObject), schema: jsf, isObject }); } return metas; } export function generateWithMethods(schema: Schema, typeName: string): string { const schemaWithProps = schema as ExtendedSchema; if (schemaWithProps.type !== 'object' || !schemaWithProps.properties) return ''; return Object.keys(schemaWithProps.properties) .map(p => ` with${toPascal(p)}(value: types.${typeName}["${p}"]): this { this.overrides["${p}"] = value; return this; }`) .join('\n'); }