@ooopenlab/quiz-shared
Version:
Shared utilities and components for SuperQuiz modules
438 lines (381 loc) • 12.6 kB
text/typescript
/**
* 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();
}