UNPKG

@ooopenlab/quiz-shared

Version:

Shared utilities and components for SuperQuiz modules

438 lines (381 loc) 12.6 kB
/** * Component Injection Validation Tools * * This file provides validation utilities for the Component Injection workflow * to ensure AI-generated components comply with the OOOPEN Lab standards. */ // Import only the types we need to avoid React dependency issues during compilation type ComponentType<P = {}> = (props: P) => any; import { InjectedComponentProps, InjectionResult, ComponentInjectionSchema, InjectionValidationOptions, InjectionValidationResult, ComponentInjectionErrorInfo, ComponentInjectionError } from './component-injection-types'; /** * Validate props for an injected component */ export function validateInjectedComponentProps( props: any, options: InjectionValidationOptions = {} ): InjectionValidationResult { const errors: string[] = []; const warnings: string[] = []; const startTime = Date.now(); // Check required props const requiredProps = ['onComplete', 'config']; for (const prop of requiredProps) { if (!(prop in props)) { errors.push(`Missing required prop: ${prop}`); } } // Validate onComplete function if (props.onComplete && typeof props.onComplete !== 'function') { errors.push('onComplete must be a function'); } // Validate config object if (props.config && typeof props.config !== 'object') { errors.push('config must be an object'); } // Validate optional props if (props.theme && typeof props.theme !== 'string') { warnings.push('theme should be a string'); } if (props.disabled && typeof props.disabled !== 'boolean') { warnings.push('disabled should be a boolean'); } if (props.debug && typeof props.debug !== 'boolean') { warnings.push('debug should be a boolean'); } // Strict validation if (options.strictProps) { const allowedProps = ['onComplete', 'config', 'theme', 'i18n', 'disabled', 'debug']; const extraProps = Object.keys(props).filter(key => !allowedProps.includes(key)); if (extraProps.length > 0) { warnings.push(`Extra props detected: ${extraProps.join(', ')}`); } } return { valid: errors.length === 0, errors, warnings, context: { componentName: 'Unknown', validationTime: Date.now() - startTime, rulesApplied: ['required-props', 'type-checking', ...(options.strictProps ? ['strict-props'] : [])] } }; } /** * Validate an injection result */ export function validateInjectionResult( result: any, schema?: ComponentInjectionSchema, options: InjectionValidationOptions = {} ): InjectionValidationResult { const errors: string[] = []; const warnings: string[] = []; const startTime = Date.now(); // Basic structure validation if (!result || typeof result !== 'object') { errors.push('Result must be an object'); return { valid: false, errors, warnings, context: { componentName: 'Unknown', validationTime: Date.now() - startTime, rulesApplied: ['basic-structure'] } }; } // Required fields const requiredFields = ['type', 'data', 'timestamp']; for (const field of requiredFields) { if (!(field in result)) { errors.push(`Missing required field: ${field}`); } } // Type validation if (result.type && typeof result.type !== 'string') { errors.push('type must be a string'); } if (result.timestamp && typeof result.timestamp !== 'number') { errors.push('timestamp must be a number'); } if (result.timestamp && result.timestamp > Date.now() + 1000) { warnings.push('timestamp seems to be in the future'); } // Schema validation if (schema && options.enforceSchema) { const schemaValidation = validateAgainstSchema(result.data, schema); errors.push(...schemaValidation.errors); warnings.push(...schemaValidation.warnings); } // Metadata validation if (result.metadata) { if (typeof result.metadata !== 'object') { errors.push('metadata must be an object'); } else { // Validate common metadata fields if (result.metadata.duration && typeof result.metadata.duration !== 'number') { warnings.push('metadata.duration should be a number'); } if (result.metadata.interactions && typeof result.metadata.interactions !== 'number') { warnings.push('metadata.interactions should be a number'); } } } return { valid: errors.length === 0, errors, warnings, context: { componentName: result.type || 'Unknown', validationTime: Date.now() - startTime, rulesApplied: [ 'basic-structure', 'required-fields', 'type-checking', ...(schema && options.enforceSchema ? ['schema-validation'] : []) ] } }; } /** * Validate data against a schema */ function validateAgainstSchema( data: any, schema: ComponentInjectionSchema ): { errors: string[], warnings: string[] } { const errors: string[] = []; const warnings: string[] = []; // Check required fields for (const field of schema.required) { if (!(field in data)) { errors.push(`Missing required field: ${field}`); } } // Check field types for (const [field, expectedType] of Object.entries(schema.fieldTypes)) { if (field in data) { const actualType = Array.isArray(data[field]) ? 'array' : typeof data[field]; if (actualType !== expectedType) { errors.push(`Field ${field} expected ${expectedType}, got ${actualType}`); } } } return { errors, warnings }; } /** * Create an error info object */ export function createInjectionError( type: ComponentInjectionError, message: string, componentName?: string, context?: Record<string, any> ): ComponentInjectionErrorInfo { return { type, message, componentName, timestamp: Date.now(), context }; } /** * Validate a React component for injection compatibility */ export function validateComponentForInjection( Component: ComponentType<any>, componentName: string ): InjectionValidationResult { const errors: string[] = []; const warnings: string[] = []; const startTime = Date.now(); // Basic component validation if (!Component) { errors.push('Component is null or undefined'); return { valid: false, errors, warnings, context: { componentName, validationTime: Date.now() - startTime, rulesApplied: ['component-existence'] } }; } if (typeof Component !== 'function') { errors.push('Component must be a function or class'); } // Check if component has display name or name if (!(Component as any).displayName && !(Component as any).name) { warnings.push('Component should have a displayName or name for debugging'); } // Check for React component patterns const componentString = Component.toString(); // Look for common React patterns const hasReactImport = componentString.includes('React') || componentString.includes('react'); const hasJSXReturn = componentString.includes('return') && ( componentString.includes('<') || componentString.includes('createElement') ); if (!hasReactImport && !hasJSXReturn) { warnings.push('Component may not be a valid React component'); } return { valid: errors.length === 0, errors, warnings, context: { componentName, validationTime: Date.now() - startTime, rulesApplied: ['component-existence', 'type-checking', 'react-patterns'] } }; } /** * Runtime validation wrapper for injected components */ export function withInjectionValidation<P extends InjectedComponentProps>( WrappedComponent: ComponentType<P>, validationOptions: InjectionValidationOptions = {} ): ComponentType<P> { const WithValidation: ComponentType<P> = (props) => { // Validate props at runtime const validation = validateInjectedComponentProps(props, validationOptions); if (!validation.valid) { console.error('Component injection validation failed:', validation.errors); if (validationOptions.strictProps) { throw new Error(`Injection validation failed: ${validation.errors.join(', ')}`); } } if (validation.warnings.length > 0) { console.warn('Component injection warnings:', validation.warnings); } // Wrap onComplete to validate results const originalOnComplete = props.onComplete; const validatedOnComplete = (result: InjectionResult) => { if (validationOptions.validateResults) { const resultValidation = validateInjectionResult(result, undefined, validationOptions); if (!resultValidation.valid) { console.error('Injection result validation failed:', resultValidation.errors); if (validationOptions.strictProps) { throw new Error(`Result validation failed: ${resultValidation.errors.join(', ')}`); } } if (resultValidation.warnings.length > 0) { console.warn('Injection result warnings:', resultValidation.warnings); } } originalOnComplete(result); }; const validatedProps = { ...props, onComplete: validatedOnComplete }; // Create the component element without using React.createElement for now // This will need to be updated when React is available return WrappedComponent(validatedProps); }; (WithValidation as any).displayName = `WithInjectionValidation(${(WrappedComponent as any).displayName || (WrappedComponent as any).name || 'Component'})`; return WithValidation; } /** * Create a validation schema for common component types */ export function createStandardSchema(type: 'game' | 'survey' | 'interactive'): ComponentInjectionSchema { const baseSchema: ComponentInjectionSchema = { required: ['timestamp'], fieldTypes: { timestamp: 'number' }, defaults: {} }; switch (type) { case 'game': return { ...baseSchema, required: [...baseSchema.required, 'score', 'completionTime'], fieldTypes: { ...baseSchema.fieldTypes, score: 'number', completionTime: 'number', level: 'number', attempts: 'number' }, defaults: { level: 1, attempts: 1 } }; case 'survey': return { ...baseSchema, required: [...baseSchema.required, 'responses'], fieldTypes: { ...baseSchema.fieldTypes, responses: 'array', completionRate: 'number' }, defaults: { completionRate: 100 } }; case 'interactive': return { ...baseSchema, required: [...baseSchema.required, 'interactions'], fieldTypes: { ...baseSchema.fieldTypes, interactions: 'number', duration: 'number', engagementScore: 'number' }, defaults: { interactions: 0, engagementScore: 0 } }; default: return baseSchema; } } /** * Utility to check if a component follows injection patterns */ export function isInjectionCompatible(Component: ComponentType<any>): boolean { const validation = validateComponentForInjection(Component, Component.name || 'Unknown'); return validation.valid; } /** * Development helper to debug injection issues */ export function debugInjection( component: ComponentType<any>, props: InjectedComponentProps, result?: InjectionResult ): void { console.group('🔍 Component Injection Debug'); // Component validation const componentValidation = validateComponentForInjection(component, component.name || 'Unknown'); console.log('Component Validation:', componentValidation); // Props validation const propsValidation = validateInjectedComponentProps(props, { strictProps: true }); console.log('Props Validation:', propsValidation); // Result validation (if provided) if (result) { const resultValidation = validateInjectionResult(result, undefined, { validateResults: true }); console.log('Result Validation:', resultValidation); } console.groupEnd(); }