UNPKG

fortify-schema

Version:

TypeScript interface-like schema validation system that's easier to use than Zod

573 lines (498 loc) 17.3 kB
/** * Auto Documentation - Revolutionary schema documentation generator * * This module automatically generates beautiful documentation from schemas, * including API docs, type definitions, and interactive examples. */ import { SchemaInterface } from "../mode/interfaces/Interface"; /** * Auto documentation utilities */ export const Docs = { /** * Generate comprehensive documentation from schema * * @example * ```typescript * const UserSchema = Interface({ * id: "uuid", * email: "email", * name: "string(2,50)", * age: "int(18,120)?", * role: Make.union("user", "admin", "moderator") * }); * * const documentation = Docs.generate(UserSchema, { * title: "User API", * description: "User management endpoints", * examples: true, * interactive: true * }); * * console.log(documentation.markdown); * console.log(documentation.html); * console.log(documentation.openapi); * ``` */ generate(schema: SchemaInterface, options: DocumentationOptions = {}): Documentation { return new DocumentationGenerator(schema, options).generate(); }, /** * Generate OpenAPI specification from schema * * @example * ```typescript * const openApiSpec = Docs.openapi(UserSchema, { * title: "User API", * version: "1.0.0", * servers: ["https://api.example.com"] * }); * ``` */ openapi(schema: SchemaInterface, options: OpenAPIOptions): OpenAPISpec { return new OpenAPIGenerator(schema, options).generate(); }, /** * Generate TypeScript type definitions * * @example * ```typescript * const typeDefinitions = Docs.typescript(UserSchema, { * exportName: "User", * namespace: "API" * }); * * // Generates: * // export interface User { * // id: string; * // email: string; * // name: string; * // age?: number; * // role: "user" | "admin" | "moderator"; * // } * ``` */ typescript(schema: SchemaInterface, options: TypeScriptOptions = {}): string { return new TypeScriptGenerator(schema, options).generate(); }, /** * Generate interactive documentation with live examples * * @example * ```typescript * const interactiveDocs = Docs.interactive(UserSchema, { * title: "User Schema Playground", * theme: "dark", * showExamples: true, * allowTesting: true * }); * * document.body.innerHTML = interactiveDocs.html; * ``` */ interactive(schema: SchemaInterface, options: InteractiveOptions = {}): InteractiveDocumentation { return new InteractiveDocumentationGenerator(schema, options).generate(); } }; /** * Main documentation generator */ class DocumentationGenerator { constructor( private schema: SchemaInterface, private options: DocumentationOptions ) {} generate(): Documentation { const analyzer = new SchemaAnalyzer(this.schema); const analysis = analyzer.analyze(); return { markdown: this.generateMarkdown(analysis), html: this.generateHTML(analysis), openapi: this.generateOpenAPI(analysis), json: this.generateJSON(analysis), examples: this.generateExamples(analysis) }; } private generateMarkdown(analysis: SchemaAnalysis): string { const { title = "Schema Documentation", description = "" } = this.options; let markdown = `# ${title}\n\n`; if (description) { markdown += `${description}\n\n`; } markdown += "## Schema Structure\n\n"; markdown += this.generateFieldTable(analysis.fields); if (this.options.examples) { markdown += "\n## Examples\n\n"; markdown += this.generateExampleMarkdown(analysis); } return markdown; } private generateHTML(analysis: SchemaAnalysis): string { const { title = "Schema Documentation" } = this.options; return ` <!DOCTYPE html> <html> <head> <title>${title}</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } .field { margin: 1rem 0; padding: 1rem; border: 1px solid #e1e5e9; border-radius: 6px; } .field-name { font-weight: bold; color: #0366d6; } .field-type { color: #6f42c1; font-family: monospace; } .field-description { color: #586069; margin-top: 0.5rem; } .example { background: #f6f8fa; padding: 1rem; border-radius: 6px; margin: 1rem 0; } pre { background: #f6f8fa; padding: 1rem; border-radius: 6px; overflow-x: auto; } </style> </head> <body> <h1>${title}</h1> ${this.generateFieldHTML(analysis.fields)} ${this.options.examples ? this.generateExampleHTML(analysis) : ''} </body> </html>`; } private generateOpenAPI(analysis: SchemaAnalysis): OpenAPISpec { return { openapi: "3.0.0", info: { title: this.options.title || "API Documentation", version: "1.0.0" }, components: { schemas: { [this.options.title || "Schema"]: this.convertToOpenAPISchema(analysis) } } }; } private generateJSON(analysis: SchemaAnalysis): any { return { schema: this.schema, analysis, metadata: { generatedAt: new Date().toISOString(), version: "1.0.0" } }; } private generateExamples(analysis: SchemaAnalysis): any[] { return [ this.generateValidExample(analysis), this.generateInvalidExample(analysis) ]; } private generateFieldTable(fields: FieldInfo[]): string { let table = "| Field | Type | Required | Description |\n"; table += "|-------|------|----------|-------------|\n"; fields.forEach(field => { table += `| ${field.name} | \`${field.type}\` | ${field.required ? 'Yes' : 'No'} | ${field.description || ''} |\n`; }); return table; } private generateFieldHTML(fields: FieldInfo[]): string { return fields.map(field => ` <div class="field"> <div class="field-name">${field.name}</div> <div class="field-type">${field.type}</div> <div class="field-description">${field.description || ''}</div> </div> `).join(''); } private generateExampleMarkdown(analysis: SchemaAnalysis): string { const example = this.generateValidExample(analysis); return `\`\`\`json\n${JSON.stringify(example, null, 2)}\n\`\`\``; } private generateExampleHTML(analysis: SchemaAnalysis): string { const example = this.generateValidExample(analysis); return `<div class="example"><pre>${JSON.stringify(example, null, 2)}</pre></div>`; } private generateValidExample(analysis: SchemaAnalysis): any { const example: any = {}; analysis.fields.forEach(field => { if (field.required || Math.random() > 0.3) { // Include some optional fields example[field.name] = this.generateExampleValue(field.type); } }); return example; } private generateInvalidExample(analysis: SchemaAnalysis): any { const example = this.generateValidExample(analysis); // Introduce some invalid data if (example.email) example.email = "invalid-email"; if (example.age) example.age = -5; return example; } private generateExampleValue(type: string): any { if (type === "email") return "user@example.com"; if (type === "uuid") return "550e8400-e29b-41d4-a716-446655440000"; if (type === "url") return "https://example.com"; if (type.startsWith("string")) return "Example string"; if (type.includes("number") || type === "positive" || type === "int") return 42; if (type === "boolean") return true; if (type === "date") return new Date().toISOString(); if (type.includes("[]")) return ["item1", "item2"]; return "example"; } private convertToOpenAPISchema(analysis: SchemaAnalysis): any { const properties: any = {}; const required: string[] = []; analysis.fields.forEach(field => { properties[field.name] = this.convertFieldToOpenAPI(field); if (field.required) { required.push(field.name); } }); return { type: "object", properties, required }; } private convertFieldToOpenAPI(field: FieldInfo): any { const type = field.type; if (type === "email") return { type: "string", format: "email" }; if (type === "uuid") return { type: "string", format: "uuid" }; if (type === "url") return { type: "string", format: "uri" }; if (type.startsWith("string")) return { type: "string" }; if (type.includes("number") || type === "positive" || type === "int") return { type: "number" }; if (type === "boolean") return { type: "boolean" }; if (type === "date") return { type: "string", format: "date-time" }; if (type.includes("[]")) return { type: "array", items: { type: "string" } }; return { type: "string" }; } } /** * Schema analyzer for extracting field information */ class SchemaAnalyzer { constructor(private schema: SchemaInterface) {} analyze(): SchemaAnalysis { const fields: FieldInfo[] = []; Object.entries(this.schema).forEach(([name, type]) => { fields.push(this.analyzeField(name, type)); }); return { fields, totalFields: fields.length, requiredFields: fields.filter(f => f.required).length, optionalFields: fields.filter(f => !f.required).length }; } private analyzeField(name: string, type: any): FieldInfo { const typeStr = typeof type === "string" ? type : JSON.stringify(type); const required = !typeStr.includes("?"); return { name, type: typeStr, required, description: this.generateFieldDescription(name, typeStr) }; } private generateFieldDescription(name: string, type: string): string { if (type === "email") return "Email address"; if (type === "uuid") return "Unique identifier"; if (type === "url") return "URL address"; if (type.includes("string")) return "Text value"; if (type.includes("number")) return "Numeric value"; if (type === "boolean") return "True/false value"; if (type === "date") return "Date and time"; if (type.includes("[]")) return "Array of values"; return `${name} field`; } } /** * TypeScript generator */ class TypeScriptGenerator { constructor( private schema: SchemaInterface, private options: TypeScriptOptions ) {} generate(): string { const { exportName = "Schema", namespace } = this.options; let output = ""; if (namespace) { output += `export namespace ${namespace} {\n`; } output += `export interface ${exportName} {\n`; Object.entries(this.schema).forEach(([name, type]) => { const tsType = this.convertToTypeScript(type); const optional = typeof type === "string" && type.includes("?") ? "?" : ""; output += ` ${name}${optional}: ${tsType};\n`; }); output += "}\n"; if (namespace) { output += "}\n"; } return output; } private convertToTypeScript(type: any): string { if (typeof type === "string") { const cleanType = type.replace("?", ""); if (cleanType === "email" || cleanType === "uuid" || cleanType === "url") return "string"; if (cleanType.startsWith("string")) return "string"; if (cleanType.includes("number") || cleanType === "positive" || cleanType === "int") return "number"; if (cleanType === "boolean") return "boolean"; if (cleanType === "date") return "Date"; if (cleanType.includes("[]")) return `${this.convertToTypeScript(cleanType.replace("[]", ""))}[]`; } return "any"; } } /** * Interactive documentation generator */ class InteractiveDocumentationGenerator { constructor( private schema: SchemaInterface, private options: InteractiveOptions ) {} generate(): InteractiveDocumentation { return { html: this.generateInteractiveHTML(), css: this.generateCSS(), javascript: this.generateJavaScript() }; } private generateInteractiveHTML(): string { return ` <div id="schema-playground"> <h1>${this.options.title || 'Schema Playground'}</h1> <div class="playground-container"> <div class="schema-panel"> <h2>Schema</h2> <pre id="schema-display"></pre> </div> <div class="input-panel"> <h2>Test Data</h2> <textarea id="test-input" placeholder="Enter JSON data to validate..."></textarea> <button id="validate-btn">Validate</button> </div> <div class="result-panel"> <h2>Result</h2> <div id="validation-result"></div> </div> </div> </div>`; } private generateCSS(): string { return ` .playground-container { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; } .schema-panel, .input-panel, .result-panel { border: 1px solid #e1e5e9; border-radius: 6px; padding: 1rem; } #test-input { width: 100%; height: 200px; font-family: monospace; } #validate-btn { background: #0366d6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; } .valid { color: #28a745; } .invalid { color: #dc3545; }`; } private generateJavaScript(): string { return ` document.getElementById('schema-display').textContent = ${JSON.stringify(this.schema, null, 2)}; document.getElementById('validate-btn').addEventListener('click', function() { const input = document.getElementById('test-input').value; const result = document.getElementById('validation-result'); try { const data = JSON.parse(input); // Validation logic would go here result.innerHTML = '<div class="valid">✓ Valid data</div>'; } catch (e) { result.innerHTML = '<div class="invalid">✗ Invalid JSON</div>'; } });`; } } /** * OpenAPI generator */ class OpenAPIGenerator { constructor( private schema: SchemaInterface, private options: OpenAPIOptions ) {} generate(): OpenAPISpec { return { openapi: "3.0.0", info: { title: this.options.title, version: this.options.version }, servers: this.options.servers?.map(url => ({ url })) || [], components: { schemas: { [this.options.title]: this.convertSchema() } } }; } private convertSchema(): any { // Implementation would convert schema to OpenAPI format return { type: "object", properties: {}, required: [] }; } } /** * Type definitions */ interface DocumentationOptions { title?: string; description?: string; examples?: boolean; interactive?: boolean; } interface OpenAPIOptions { title: string; version: string; servers?: string[]; } interface TypeScriptOptions { exportName?: string; namespace?: string; } interface InteractiveOptions { title?: string; theme?: "light" | "dark"; showExamples?: boolean; allowTesting?: boolean; } interface Documentation { markdown: string; html: string; openapi: OpenAPISpec; json: any; examples: any[]; } interface InteractiveDocumentation { html: string; css: string; javascript: string; } interface OpenAPISpec { openapi: string; info: { title: string; version: string; }; servers?: Array<{ url: string }>; components: { schemas: Record<string, any>; }; } interface SchemaAnalysis { fields: FieldInfo[]; totalFields: number; requiredFields: number; optionalFields: number; } interface FieldInfo { name: string; type: string; required: boolean; description: string; } /** * Export all utilities */ export { DocumentationGenerator, SchemaAnalyzer, TypeScriptGenerator }; export default Docs;