fortify-schema
Version:
TypeScript interface-like schema validation system that's easier to use than Zod
219 lines (187 loc) • 6.44 kB
text/typescript
/**
* Smart Schema Inference - Revolutionary TypeScript type-to-schema conversion
*
* This module provides automatic schema generation from TypeScript types,
* making schema definition even more seamless.
*/
import { SchemaInterface } from "../mode/interfaces/Interface";
/**
* Smart inference utilities for automatic schema generation
*/
export const Smart = {
/**
* Infer schema from TypeScript interface using runtime reflection
*
* @example
* ```typescript
* interface User {
* id: number;
* email: string;
* name?: string;
* }
*
* // Use with sample data that matches your interface
* const UserSchema = Smart.fromType<User>({
* id: 1,
* email: "user@example.com",
* name: "John Doe"
* });
* // Generates: Interface({ id: "positive", email: "email", name: "string?" })
* ```
*/
fromType<T>(sampleData: T): SchemaInterface {
if (!sampleData || typeof sampleData !== 'object') {
throw new Error('Smart.fromType() requires sample data that matches your TypeScript interface');
}
return Smart.fromSample(sampleData);
},
/**
* Infer schema from sample data with intelligent type detection
*
* @example
* ```typescript
* const sampleUser = {
* id: 1,
* email: "user@example.com",
* name: "John Doe",
* tags: ["developer", "typescript"]
* };
*
* const UserSchema = Smart.fromSample(sampleUser);
* // Generates: Interface({ id: "positive", email: "email", name: "string", tags: "string[]" })
* ```
*/
fromSample(sample: any): SchemaInterface {
const schema: any = {};
for (const [key, value] of Object.entries(sample)) {
schema[key] = Smart.inferFieldType(value);
}
return schema;
},
/**
* Infer field type from value with smart detection
*/
inferFieldType(value: any): string {
if (value === null || value === undefined) {
return "any?";
}
if (typeof value === "string") {
// Smart email detection
if (Smart.isEmail(value)) return "email";
// Smart URL detection
if (Smart.isUrl(value)) return "url";
// Smart UUID detection
if (Smart.isUuid(value)) return "uuid";
// Smart phone detection
if (Smart.isPhone(value)) return "phone";
return "string";
}
if (typeof value === "number") {
// Smart positive number detection
if (value > 0 && Number.isInteger(value)) return "positive";
if (Number.isInteger(value)) return "int";
return "number";
}
if (typeof value === "boolean") {
return "boolean";
}
if (value instanceof Date) {
return "date";
}
if (Array.isArray(value)) {
if (value.length === 0) return "any[]";
// Detect array element type from first element
const elementType = Smart.inferFieldType(value[0]);
const baseType = elementType.replace("?", ""); // Remove optional marker
return `${baseType}[]`;
}
if (typeof value === "object") {
// Nested object - recursively infer
const nestedSchema: any = {};
for (const [nestedKey, nestedValue] of Object.entries(value)) {
nestedSchema[nestedKey] = Smart.inferFieldType(nestedValue);
}
return nestedSchema;
}
return "any";
},
/**
* Smart format detection utilities
*/
isEmail(str: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
},
isUrl(str: string): boolean {
try {
new URL(str);
return true;
} catch {
return false;
}
},
isUuid(str: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
},
isPhone(str: string): boolean {
return /^\+?[1-9]\d{1,14}$/.test(str.replace(/[\s\-\(\)]/g, ''));
},
/**
* Generate schema from JSON Schema (migration helper)
*
* @example
* ```typescript
* const jsonSchema = {
* type: "object",
* properties: {
* id: { type: "number" },
* email: { type: "string", format: "email" }
* }
* };
*
* const schema = Smart.fromJsonSchema(jsonSchema);
* ```
*/
fromJsonSchema(jsonSchema: any): SchemaInterface {
if (jsonSchema.type === "object" && jsonSchema.properties) {
const schema: any = {};
for (const [key, prop] of Object.entries(jsonSchema.properties as any)) {
schema[key] = Smart.convertJsonSchemaProperty(prop);
}
return schema;
}
throw new Error("Unsupported JSON Schema format");
},
convertJsonSchemaProperty(prop: any): string {
const isOptional = !prop.required;
const suffix = isOptional ? "?" : "";
switch (prop.type) {
case "string":
if (prop.format === "email") return `email${suffix}`;
if (prop.format === "uri") return `url${suffix}`;
if (prop.format === "uuid") return `uuid${suffix}`;
if (prop.minLength && prop.maxLength) {
return `string(${prop.minLength},${prop.maxLength})${suffix}`;
}
return `string${suffix}`;
case "number":
case "integer":
if (prop.minimum && prop.maximum) {
return `number(${prop.minimum},${prop.maximum})${suffix}`;
}
if (prop.minimum > 0) return `positive${suffix}`;
return prop.type === "integer" ? `int${suffix}` : `number${suffix}`;
case "boolean":
return `boolean${suffix}`;
case "array":
const itemType = Smart.convertJsonSchemaProperty(prop.items || { type: "any" });
const baseType = itemType.replace("?", "");
return `${baseType}[]${suffix}`;
default:
return `any${suffix}`;
}
}
};
/**
* Export for easy access
*/
export default Smart;