fortify-schema
Version:
TypeScript interface-like schema validation system that's easier to use than Zod
202 lines (170 loc) • 7.23 kB
text/typescript
import { Documentation, DocumentationOptions, FieldInfo, OpenAPISpec, SchemaAnalysis } from "../../../../types/extension.type";
import { SchemaInterface } from "../../../../types/SchemaValidator.type";
import { SchemaAnalyzer } from "./SchemaAnalyzer";
/**
* Main documentation generator
*/
export 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" };
}
}