fortify-schema
Version:
TypeScript interface-like schema validation system that's easier to use than Zod
280 lines (244 loc) âĸ 9.16 kB
text/typescript
/**
* TypeScript Transformer for Schema Generation
*
* This transformer replaces Make.fromType<T>() calls with actual schema strings
* at compile time, providing true TypeScript-to-schema conversion.
*/
import * as ts from 'typescript';
import { TypeAnalyzer } from './TypeAnalyzer';
/**
* Configuration for the schema transformer
*/
export interface TransformerConfig {
sourceFiles: string[];
compilerOptions?: ts.CompilerOptions;
debug?: boolean;
}
/**
* Create a TypeScript transformer that converts Make.fromType<T>() calls
* to actual schema strings at compile time
*/
export function createSchemaTransformer(config: TransformerConfig): ts.TransformerFactory<ts.SourceFile> {
const analyzer = new TypeAnalyzer(config.sourceFiles, config.compilerOptions);
return (context: ts.TransformationContext) => {
return (sourceFile: ts.SourceFile) => {
function visitor(node: ts.Node): ts.Node {
// Look for Make.fromType<T>() calls
if (ts.isCallExpression(node)) {
const result = transformMakeFromTypeCall(node, analyzer, config.debug);
if (result) {
if (config.debug) {
console.log(`đ Transformed: ${node.getText()} â ${result.getText()}`);
}
return result;
}
}
// Look for Make.fromInterface<T>() calls
if (ts.isCallExpression(node)) {
const result = transformMakeFromInterfaceCall(node, analyzer, config.debug);
if (result) {
if (config.debug) {
console.log(`đ Transformed: ${node.getText()} â ${result.getText()}`);
}
return result;
}
}
return ts.visitEachChild(node, visitor, context);
}
return ts.visitNode(sourceFile, visitor) as ts.SourceFile;
};
};
}
/**
* Transform Make.fromType<T>() calls to schema strings
*/
function transformMakeFromTypeCall(
node: ts.CallExpression,
analyzer: TypeAnalyzer,
debug?: boolean
): ts.StringLiteral | null {
// Check if this is a Make.fromType call
if (!isMakeFromTypeCall(node)) {
return null;
}
// Extract the type argument
const typeArgument = node.typeArguments?.[0];
if (!typeArgument) {
if (debug) {
console.warn('â ī¸ Make.fromType call without type argument');
}
return null;
}
// Check if there are runtime arguments (sample data)
if (node.arguments.length > 0) {
// Has sample data - let runtime handle it
if (debug) {
console.log('âšī¸ Make.fromType has sample data - skipping transformation');
}
return null;
}
try {
// Analyze the type and generate schema
const typeInfo = analyzer.analyzeTypeFromNode(typeArgument);
const schemaString = analyzer.typeInfoToSchemaString(typeInfo);
if (debug) {
console.log(`â
Generated schema: ${schemaString} for type: ${typeArgument.getText()}`);
}
// Return the schema string literal
return ts.factory.createStringLiteral(schemaString);
} catch (error) {
if (debug) {
console.error(`â Failed to transform Make.fromType:`, error);
}
return null;
}
}
/**
* Transform Make.fromInterface<T>() calls to schema objects
*/
function transformMakeFromInterfaceCall(
node: ts.CallExpression,
analyzer: TypeAnalyzer,
debug?: boolean
): ts.ObjectLiteralExpression | null {
// Check if this is a Make.fromInterface call
if (!isMakeFromInterfaceCall(node)) {
return null;
}
// Extract the type argument
const typeArgument = node.typeArguments?.[0];
if (!typeArgument) {
if (debug) {
console.warn('â ī¸ Make.fromInterface call without type argument');
}
return null;
}
// Check if there are runtime arguments (sample data)
if (node.arguments.length > 0) {
// Has sample data - let runtime handle it
if (debug) {
console.log('âšī¸ Make.fromInterface has sample data - skipping transformation');
}
return null;
}
try {
// Analyze the type and generate schema object
const typeInfo = analyzer.analyzeTypeFromNode(typeArgument);
if (typeInfo.kind === 'object' && typeInfo.properties) {
// Generate object literal with schema properties
const properties: ts.PropertyAssignment[] = [];
for (const [propName, propInfo] of Object.entries(typeInfo.properties)) {
const schemaString = analyzer.typeInfoToSchemaString(propInfo);
properties.push(
ts.factory.createPropertyAssignment(
ts.factory.createStringLiteral(propName),
ts.factory.createStringLiteral(schemaString)
)
);
}
if (debug) {
console.log(`â
Generated interface schema for: ${typeArgument.getText()}`);
}
return ts.factory.createObjectLiteralExpression(properties, true);
} else {
if (debug) {
console.warn(`â ī¸ Type is not an object interface: ${typeArgument.getText()}`);
}
return null;
}
} catch (error) {
if (debug) {
console.error(`â Failed to transform Make.fromInterface:`, error);
}
return null;
}
}
/**
* Check if a call expression is Make.fromType<T>()
*/
function isMakeFromTypeCall(node: ts.CallExpression): boolean {
// Check for Make.fromType pattern
if (ts.isPropertyAccessExpression(node.expression)) {
const { expression, name } = node.expression;
return ts.isIdentifier(expression) &&
expression.text === 'Make' &&
name.text === 'fromType';
}
return false;
}
/**
* Check if a call expression is Make.fromInterface<T>()
*/
function isMakeFromInterfaceCall(node: ts.CallExpression): boolean {
// Check for Make.fromInterface pattern
if (ts.isPropertyAccessExpression(node.expression)) {
const { expression, name } = node.expression;
return ts.isIdentifier(expression) &&
expression.text === 'Make' &&
name.text === 'fromInterface';
}
return false;
}
/**
* Utility function to transform TypeScript files with the schema transformer
*/
export function transformSourceFile(
sourceFile: ts.SourceFile,
config: TransformerConfig
): ts.SourceFile {
const transformer = createSchemaTransformer(config);
const transformationContext: ts.TransformationContext = {
getCompilerOptions: () => config.compilerOptions || {},
startLexicalEnvironment: () => {},
suspendLexicalEnvironment: () => {},
resumeLexicalEnvironment: () => {},
endLexicalEnvironment: () => [],
hoistFunctionDeclaration: () => {},
hoistVariableDeclaration: () => {},
requestEmitHelper: () => {},
readEmitHelpers: () => [],
enableSubstitution: () => {},
isSubstitutionEnabled: () => false,
onSubstituteNode: (hint, node) => node,
enableEmitNotification: () => {},
isEmitNotificationEnabled: () => false,
onEmitNode: () => {},
factory: ts.factory
};
const result = transformer(transformationContext)(sourceFile);
return result;
}
/**
* Transform multiple source files
*/
export function transformFiles(filePaths: string[], config: Partial<TransformerConfig> = {}): void {
const fullConfig: TransformerConfig = {
sourceFiles: filePaths,
debug: true,
...config
};
const program = ts.createProgram(filePaths, fullConfig.compilerOptions || {});
for (const sourceFile of program.getSourceFiles()) {
if (filePaths.includes(sourceFile.fileName)) {
console.log(`đ Transforming: ${sourceFile.fileName}`);
const transformed = transformSourceFile(sourceFile, fullConfig);
// Print the transformed result (in a real implementation, you'd write to file)
const printer = ts.createPrinter();
const result = printer.printFile(transformed);
console.log(`â
Transformed result:\n${result}`);
}
}
}
/**
* CLI utility for testing the transformer
*/
export function runTransformerCLI(): void {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage: node transformer.js <file1.ts> [file2.ts] ...');
process.exit(1);
}
console.log('đ Running Schema Transformer');
console.log('============================');
transformFiles(args, { debug: true });
}