UNPKG

@gati-framework/cli

Version:

CLI tool for Gati framework - create, develop, build and deploy cloud-native applications

412 lines 21.5 kB
/** * @module cli/codegen/validator-generator * @description Generate optimized validator functions from GType schemas */ /** * Generate validator function from GType schema */ export class ValidatorGenerator { generate(schema, options = {}) { const opts = { includeComments: true, includeImports: true, functionName: 'validate', ...options, }; const lines = []; // Imports if (opts.includeImports) { lines.push("import type { ValidationResult, ValidationError } from '@gati-framework/runtime/gtype/errors';"); lines.push(''); } // Function header if (opts.includeComments) { lines.push('/**'); lines.push(' * Auto-generated validator function'); if (schema.description) { lines.push(` * ${schema.description}`); } lines.push(' */'); } lines.push(`export function ${opts.functionName}(value: unknown): ValidationResult {`); lines.push(' const errors: ValidationError[] = [];'); lines.push(''); // Generate validation logic lines.push(this.generateValidation(schema, 'value', [])); lines.push(''); lines.push(' return errors.length === 0'); lines.push(' ? { valid: true, errors: [] }'); lines.push(' : { valid: false, errors };'); lines.push('}'); return { code: lines.join('\n'), functionName: opts.functionName, }; } generateValidation(schema, varName, path) { const lines = []; // Handle optional if (schema.optional) { lines.push(` if (${varName} === undefined) {`); lines.push(' // Optional field, skip validation'); lines.push(' } else {'); lines.push(this.generateTypeValidation(schema, varName, path, ' ')); lines.push(' }'); return lines.join('\n'); } // Handle nullable if (schema.nullable) { lines.push(` if (${varName} === null) {`); lines.push(' // Nullable field, skip validation'); lines.push(' } else {'); lines.push(this.generateTypeValidation(schema, varName, path, ' ')); lines.push(' }'); return lines.join('\n'); } return this.generateTypeValidation(schema, varName, path, ''); } generateTypeValidation(schema, varName, path, indent) { switch (schema.kind) { case 'primitive': return this.generatePrimitiveValidation(schema, varName, path, indent); case 'literal': return this.generateLiteralValidation(schema, varName, path, indent); case 'object': return this.generateObjectValidation(schema, varName, path, indent); case 'array': return this.generateArrayValidation(schema, varName, path, indent); case 'tuple': return this.generateTupleValidation(schema, varName, path, indent); case 'union': return this.generateUnionValidation(schema, varName, path, indent); case 'intersection': return this.generateIntersectionValidation(schema, varName, path, indent); case 'enum': return this.generateEnumValidation(schema, varName, path, indent); default: return `${indent} // Unknown schema kind`; } } generatePrimitiveValidation(schema, varName, path, indent) { const lines = []; const pathStr = this.formatPath(path); lines.push(`${indent} if (typeof ${varName} !== '${schema.primitiveType}') {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: '${schema.primitiveType}',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: \`Expected ${schema.primitiveType}, got \${typeof ${varName}}\`,`); lines.push(`${indent} });`); lines.push(`${indent} }`); // Custom validators if (schema.validators && schema.validators.length > 0) { lines.push(`${indent} else {`); for (const validator of schema.validators) { lines.push(this.generateCustomValidator(validator, varName, path, indent + ' ')); } lines.push(`${indent} }`); } return lines.join('\n'); } generateLiteralValidation(schema, varName, path, indent) { const lines = []; const pathStr = this.formatPath(path); const literalValue = JSON.stringify(schema.value); lines.push(`${indent} if (${varName} !== ${literalValue}) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'literal ${literalValue}',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: \`Expected ${literalValue}, got \${${varName}}\`,`); lines.push(`${indent} });`); lines.push(`${indent} }`); return lines.join('\n'); } generateObjectValidation(schema, varName, path, indent) { const lines = []; const pathStr = this.formatPath(path); lines.push(`${indent} if (typeof ${varName} !== 'object' || ${varName} === null || Array.isArray(${varName})) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'object',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: 'Expected object',`); lines.push(`${indent} });`); lines.push(`${indent} } else {`); lines.push(`${indent} const obj = ${varName} as Record<string, unknown>;`); // Validate required properties if (schema.required && schema.required.length > 0) { for (const key of schema.required) { const propPath = [...path, key]; const propPathStr = this.formatPath(propPath); lines.push(`${indent} if (!(${JSON.stringify(key)} in obj)) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${propPathStr},`); lines.push(`${indent} expected: 'defined value',`); lines.push(`${indent} actual: undefined,`); lines.push(`${indent} message: 'Required property "${key}" is missing',`); lines.push(`${indent} });`); lines.push(`${indent} }`); } } // Validate each property for (const [key, propSchema] of Object.entries(schema.properties)) { const propPath = [...path, key]; lines.push(`${indent} if (${JSON.stringify(key)} in obj) {`); lines.push(this.generateValidation(propSchema, `obj[${JSON.stringify(key)}]`, propPath).split('\n').map(l => `${indent} ${l}`).join('\n')); lines.push(`${indent} }`); } // Additional properties check if (schema.additionalProperties === false) { const allowedKeys = Object.keys(schema.properties); lines.push(`${indent} const allowedKeys = new Set(${JSON.stringify(allowedKeys)});`); lines.push(`${indent} for (const key of Object.keys(obj)) {`); lines.push(`${indent} if (!allowedKeys.has(key)) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: [...${pathStr}, key],`); lines.push(`${indent} expected: 'no additional properties',`); lines.push(`${indent} actual: obj[key],`); lines.push(`${indent} message: \`Additional property "\${key}" is not allowed\`,`); lines.push(`${indent} });`); lines.push(`${indent} }`); lines.push(`${indent} }`); } lines.push(`${indent} }`); return lines.join('\n'); } generateArrayValidation(schema, varName, path, indent) { const lines = []; const pathStr = this.formatPath(path); lines.push(`${indent} if (!Array.isArray(${varName})) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'array',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: 'Expected array',`); lines.push(`${indent} });`); lines.push(`${indent} } else {`); // Length constraints if (schema.minItems !== undefined) { lines.push(`${indent} if (${varName}.length < ${schema.minItems}) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'array with at least ${schema.minItems} items',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: \`Array must have at least ${schema.minItems} items, got \${${varName}.length}\`,`); lines.push(`${indent} });`); lines.push(`${indent} }`); } if (schema.maxItems !== undefined) { lines.push(`${indent} if (${varName}.length > ${schema.maxItems}) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'array with at most ${schema.maxItems} items',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: \`Array must have at most ${schema.maxItems} items, got \${${varName}.length}\`,`); lines.push(`${indent} });`); lines.push(`${indent} }`); } // Validate items lines.push(`${indent} for (let i = 0; i < ${varName}.length; i++) {`); const itemPath = [...path, 'i']; lines.push(this.generateValidation(schema.items, `${varName}[i]`, itemPath).split('\n').map(l => `${indent} ${l}`).join('\n')); lines.push(`${indent} }`); lines.push(`${indent} }`); return lines.join('\n'); } generateTupleValidation(schema, varName, path, indent) { const lines = []; const pathStr = this.formatPath(path); lines.push(`${indent} if (!Array.isArray(${varName})) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'tuple',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: 'Expected tuple',`); lines.push(`${indent} });`); lines.push(`${indent} } else if (${varName}.length !== ${schema.items.length}) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'tuple with ${schema.items.length} items',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: \`Tuple must have exactly ${schema.items.length} items, got \${${varName}.length}\`,`); lines.push(`${indent} });`); lines.push(`${indent} } else {`); // Validate each item schema.items.forEach((itemSchema, index) => { const itemPath = [...path, String(index)]; lines.push(this.generateValidation(itemSchema, `${varName}[${index}]`, itemPath).split('\n').map(l => `${indent} ${l}`).join('\n')); }); lines.push(`${indent} }`); return lines.join('\n'); } generateUnionValidation(schema, varName, path, indent) { const lines = []; const pathStr = this.formatPath(path); lines.push(`${indent} {`); lines.push(`${indent} let matched = false;`); lines.push(`${indent} const unionErrors: ValidationError[][] = [];`); schema.types.forEach((typeSchema, index) => { lines.push(`${indent} {`); lines.push(`${indent} const tempErrors: ValidationError[] = [];`); lines.push(`${indent} const originalLength = errors.length;`); // Temporarily collect errors const validation = this.generateTypeValidation(typeSchema, varName, path, indent + ' '); lines.push(validation.replace(/errors\.push/g, 'tempErrors.push')); lines.push(`${indent} if (tempErrors.length === 0) {`); lines.push(`${indent} matched = true;`); lines.push(`${indent} } else {`); lines.push(`${indent} unionErrors.push(tempErrors);`); lines.push(`${indent} }`); lines.push(`${indent} }`); if (index < schema.types.length - 1) { lines.push(`${indent} if (!matched) {`); } }); // Close if statements for (let i = 0; i < schema.types.length - 1; i++) { lines.push(`${indent} }`); } lines.push(`${indent} if (!matched) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'one of ${schema.types.length} types',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: 'Value does not match any of the union types',`); lines.push(`${indent} });`); lines.push(`${indent} }`); lines.push(`${indent} }`); return lines.join('\n'); } generateIntersectionValidation(schema, varName, path, indent) { const lines = []; // Validate against all types schema.types.forEach((typeSchema) => { lines.push(this.generateTypeValidation(typeSchema, varName, path, indent)); }); return lines.join('\n'); } generateEnumValidation(schema, varName, path, indent) { const lines = []; const pathStr = this.formatPath(path); const allowedValues = JSON.stringify(schema.values); lines.push(`${indent} if (!${allowedValues}.includes(${varName} as any)) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'one of [${schema.values.join(', ')}]',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: \`Expected one of [${schema.values.join(', ')}], got \${${varName}}\`,`); lines.push(`${indent} });`); lines.push(`${indent} }`); return lines.join('\n'); } generateCustomValidator(validator, varName, path, indent) { const lines = []; const pathStr = this.formatPath(path); switch (validator.type) { case 'min': lines.push(`${indent} if (typeof ${varName} === 'number' && ${varName} < ${validator.value}) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: '>= ${validator.value}',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: ${JSON.stringify(validator.message || `Value must be at least ${validator.value}`)},`); lines.push(`${indent} });`); lines.push(`${indent} }`); break; case 'max': lines.push(`${indent} if (typeof ${varName} === 'number' && ${varName} > ${validator.value}) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: '<= ${validator.value}',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: ${JSON.stringify(validator.message || `Value must be at most ${validator.value}`)},`); lines.push(`${indent} });`); lines.push(`${indent} }`); break; case 'minLength': lines.push(`${indent} if (typeof ${varName} === 'string' && ${varName}.length < ${validator.value}) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'string with length >= ${validator.value}',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: ${JSON.stringify(validator.message || `String must be at least ${validator.value} characters`)},`); lines.push(`${indent} });`); lines.push(`${indent} }`); break; case 'maxLength': lines.push(`${indent} if (typeof ${varName} === 'string' && ${varName}.length > ${validator.value}) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'string with length <= ${validator.value}',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: ${JSON.stringify(validator.message || `String must be at most ${validator.value} characters`)},`); lines.push(`${indent} });`); lines.push(`${indent} }`); break; case 'pattern': lines.push(`${indent} if (typeof ${varName} === 'string' && !/${validator.value}/.test(${varName})) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'string matching ${validator.value}',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: ${JSON.stringify(validator.message || `String must match pattern ${validator.value}`)},`); lines.push(`${indent} });`); lines.push(`${indent} }`); break; case 'email': lines.push(`${indent} if (typeof ${varName} === 'string' && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(${varName})) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'valid email',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: ${JSON.stringify(validator.message || 'Must be a valid email address')},`); lines.push(`${indent} });`); lines.push(`${indent} }`); break; case 'url': lines.push(`${indent} if (typeof ${varName} === 'string') {`); lines.push(`${indent} try {`); lines.push(`${indent} new URL(${varName});`); lines.push(`${indent} } catch {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'valid URL',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: ${JSON.stringify(validator.message || 'Must be a valid URL')},`); lines.push(`${indent} });`); lines.push(`${indent} }`); lines.push(`${indent} }`); break; case 'uuid': lines.push(`${indent} if (typeof ${varName} === 'string' && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${varName})) {`); lines.push(`${indent} errors.push({`); lines.push(`${indent} path: ${pathStr},`); lines.push(`${indent} expected: 'valid UUID',`); lines.push(`${indent} actual: ${varName},`); lines.push(`${indent} message: ${JSON.stringify(validator.message || 'Must be a valid UUID')},`); lines.push(`${indent} });`); lines.push(`${indent} }`); break; } return lines.join('\n'); } formatPath(path) { if (path.length === 0) return '[]'; // Handle numeric indices (array items) const formatted = path.map(segment => { if (segment === 'i') return 'i'; // Loop variable return JSON.stringify(segment); }); return `[${formatted.join(', ')}]`; } } /** * Create validator generator instance */ export function createValidatorGenerator() { return new ValidatorGenerator(); } //# sourceMappingURL=validator-generator.js.map