fortify-schema
Version:
TypeScript interface-like schema validation system that's easier to use than Zod
176 lines (152 loc) • 5.82 kB
text/typescript
import { OpenAPIOptions, OpenAPISpec } from "../../../../types/extension.type";
import { SchemaInterface } from "../../../../types/SchemaValidator.type";
/**
* OpenAPI generator
*/
export 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 {
const properties: any = {};
const required: string[] = [];
Object.entries(this.schema).forEach(([fieldName, fieldType]) => {
const openApiProperty = this.convertFieldToOpenAPIProperty(fieldName, fieldType);
properties[fieldName] = openApiProperty.schema;
if (openApiProperty.required) {
required.push(fieldName);
}
});
return {
type: "object",
properties,
required: required.length > 0 ? required : undefined
};
}
private convertFieldToOpenAPIProperty(_fieldName: string, fieldType: any): { schema: any; required: boolean } {
if (typeof fieldType === "string") {
const isOptional = fieldType.includes("?");
const cleanType = fieldType.replace("?", "");
// Handle string types with constraints
if (cleanType.startsWith("string")) {
const constraints = this.parseStringConstraints(cleanType);
return {
schema: {
type: "string",
...constraints
},
required: !isOptional
};
}
// Handle specific string formats
if (cleanType === "email") {
return {
schema: { type: "string", format: "email" },
required: !isOptional
};
}
if (cleanType === "url") {
return {
schema: { type: "string", format: "uri" },
required: !isOptional
};
}
if (cleanType === "uuid") {
return {
schema: { type: "string", format: "uuid" },
required: !isOptional
};
}
if (cleanType === "date") {
return {
schema: { type: "string", format: "date-time" },
required: !isOptional
};
}
// Handle number types
if (cleanType === "number" || cleanType === "positive" || cleanType === "int") {
const schema: any = { type: cleanType === "int" ? "integer" : "number" };
if (cleanType === "positive") {
schema.minimum = 0;
schema.exclusiveMinimum = true;
}
return { schema, required: !isOptional };
}
// Handle boolean
if (cleanType === "boolean") {
return {
schema: { type: "boolean" },
required: !isOptional
};
}
// Handle arrays
if (cleanType.includes("[]")) {
const itemType = cleanType.replace("[]", "");
const itemSchema = this.convertFieldToOpenAPIProperty("item", itemType).schema;
return {
schema: {
type: "array",
items: itemSchema
},
required: !isOptional
};
}
// Default to string
return {
schema: { type: "string" },
required: !isOptional
};
}
// Handle object types
if (typeof fieldType === "object" && !Array.isArray(fieldType)) {
const nestedProperties: any = {};
const nestedRequired: string[] = [];
Object.entries(fieldType).forEach(([nestedField, nestedType]) => {
const nestedProperty = this.convertFieldToOpenAPIProperty(nestedField, nestedType);
nestedProperties[nestedField] = nestedProperty.schema;
if (nestedProperty.required) {
nestedRequired.push(nestedField);
}
});
return {
schema: {
type: "object",
properties: nestedProperties,
required: nestedRequired.length > 0 ? nestedRequired : undefined
},
required: true
};
}
// Fallback
return {
schema: { type: "string" },
required: true
};
}
private parseStringConstraints(stringType: string): any {
const constraints: any = {};
// Parse string(min,max) format
const match = stringType.match(/string\((\d+)?,?(\d+)?\)/);
if (match) {
if (match[1]) constraints.minLength = parseInt(match[1]);
if (match[2]) constraints.maxLength = parseInt(match[2]);
}
return constraints;
}
}