"use strict"; /** * Data Validator * Verify data structure cho từng game code * * Usage: * ```typescript * import { validateGameData, DataValidator } from 'game-iframe-sdk/client'; * * const result = validateGameData('G001', receivedData); * if (!result.valid) { * console.error('Invalid data:', result.errors); * } * ``` */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DataValidator = void 0; exports.validateGameData = validateGameData; exports.getSchema = getSchema; exports.getSchemaDoc = getSchemaDoc; const GameDataHandler_1 = require("../kit/GameDataHandler"); // ============================================================================= // SCHEMAS FOR EACH GAME CODE // ============================================================================= const QUIZ_BASE_SCHEMA = { id: { type: 'string', required: true, description: 'Unique question ID' }, options: { type: 'array', required: true, arrayItemType: 'string', description: 'Answer options' }, answer: { type: 'number', required: true, description: 'Correct answer index (0-based)' }, }; const SCHEMAS = { // Quiz variants G001: { ...QUIZ_BASE_SCHEMA, question: { type: 'string', required: true, description: 'Text question' }, }, G002: { ...QUIZ_BASE_SCHEMA, question_audio: { type: 'string', required: true, description: 'Audio URL for question' }, }, G003: { ...QUIZ_BASE_SCHEMA, question: { type: 'string', required: true, description: 'Text question' }, // options are audio URLs }, G004: { ...QUIZ_BASE_SCHEMA, question_image: { type: 'string', required: true, description: 'Image URL for question' }, question: { type: 'string', required: false, description: 'Optional text hint' }, }, // G005: Quiz Text-Image (options are image URLs, client picks index) G005: { ...QUIZ_BASE_SCHEMA, question: { type: 'string', required: true, description: 'Text question' }, // options are image URLs, answer is index pointing to correct image }, // Sequence Word variants G110: { id: { type: 'string', required: true }, word: { type: 'string', required: true, description: 'The word to arrange' }, parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Letters/parts to arrange' }, answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct order' }, }, G111: { id: { type: 'string', required: true }, word: { type: 'string', required: true }, parts: { type: 'array', required: true, arrayItemType: 'string' }, answer: { type: 'array', required: true, arrayItemType: 'string' }, audio_url: { type: 'string', required: true, description: 'Audio hint URL' }, }, G112: { id: { type: 'string', required: true }, word: { type: 'string', required: true }, parts: { type: 'array', required: true, arrayItemType: 'string' }, answer: { type: 'array', required: true, arrayItemType: 'string' }, audio_url: { type: 'string', required: true }, }, G113: { id: { type: 'string', required: true }, word: { type: 'string', required: true }, parts: { type: 'array', required: true, arrayItemType: 'string' }, answer: { type: 'array', required: true, arrayItemType: 'string' }, audio_url: { type: 'string', required: true }, }, // Sequence Sentence variants G120: { id: { type: 'string', required: true }, sentence: { type: 'string', required: false, description: 'Full sentence (hint)' }, parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Words to arrange' }, answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct word order' }, }, G121: { id: { type: 'string', required: true }, sentence: { type: 'string', required: false }, parts: { type: 'array', required: true, arrayItemType: 'string' }, answer: { type: 'array', required: true, arrayItemType: 'string' }, audio_url: { type: 'string', required: true }, }, G122: { id: { type: 'string', required: true }, sentence: { type: 'string', required: false }, parts: { type: 'array', required: true, arrayItemType: 'string' }, answer: { type: 'array', required: true, arrayItemType: 'string' }, audio_url: { type: 'string', required: true }, }, G123: { id: { type: 'string', required: true }, sentence: { type: 'string', required: false }, parts: { type: 'array', required: true, arrayItemType: 'string' }, answer: { type: 'array', required: true, arrayItemType: 'string' }, audio_url: { type: 'string', required: true }, } }; // ============================================================================= // VALIDATOR // ============================================================================= /** * Validate a single item against schema */ function validateItem(item, schema, itemIndex) { const errors = []; if (!item || typeof item !== 'object') { errors.push(`Item [${itemIndex}]: Must be an object`); return errors; } for (const [field, fieldSchema] of Object.entries(schema)) { const value = item[field]; // Check required if (fieldSchema.required && (value === undefined || value === null)) { errors.push(`Item [${itemIndex}].${field}: Required field is missing`); continue; } // Skip validation if optional and not present if (!fieldSchema.required && (value === undefined || value === null)) { continue; } // Check type const actualType = Array.isArray(value) ? 'array' : typeof value; if (fieldSchema.type !== 'any' && actualType !== fieldSchema.type) { errors.push(`Item [${itemIndex}].${field}: Expected ${fieldSchema.type}, got ${actualType}`); continue; } // Check array items if (fieldSchema.type === 'array' && fieldSchema.arrayItemType && fieldSchema.arrayItemType !== 'any') { for (let i = 0; i < value.length; i++) { const itemType = typeof value[i]; if (itemType !== fieldSchema.arrayItemType) { errors.push(`Item [${itemIndex}].${field}[${i}]: Expected ${fieldSchema.arrayItemType}, got ${itemType}`); } } } } return errors; } /** * Validate game data payload */ function validateGameData(gameCode, payload) { const errors = []; const warnings = []; // Check game code if (!GameDataHandler_1.GAME_CODES[gameCode]) { errors.push(`Unknown game code: ${gameCode}`); return { valid: false, errors, warnings }; } // Check payload structure if (!payload || typeof payload !== 'object') { errors.push('Payload must be an object'); return { valid: false, errors, warnings }; } // Check data array const items = payload.data || payload.items || payload.questions; if (!items) { errors.push('Missing data array (expected "data", "items", or "questions")'); return { valid: false, errors, warnings }; } if (!Array.isArray(items)) { errors.push('"data" must be an array'); return { valid: false, errors, warnings }; } if (items.length === 0) { warnings.push('Data array is empty'); } // Validate each item const schema = SCHEMAS[gameCode]; for (let i = 0; i < items.length; i++) { const itemErrors = validateItem(items[i], schema, i); errors.push(...itemErrors); } // Check for duplicate IDs const ids = items.map((item) => item.id).filter(Boolean); const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index); if (duplicates.length > 0) { warnings.push(`Duplicate IDs found: ${[...new Set(duplicates)].join(', ')}`); } return { valid: errors.length === 0, errors, warnings, }; } /** * Get schema for a game code */ function getSchema(gameCode) { return SCHEMAS[gameCode] ?? null; } /** * Get schema documentation for a game code */ function getSchemaDoc(gameCode) { const schema = SCHEMAS[gameCode]; if (!schema) return `Unknown game code: ${gameCode}`; const gameInfo = GameDataHandler_1.GAME_CODES[gameCode]; const lines = [ `## ${gameCode}: ${gameInfo.name}`, `Category: ${gameInfo.category}`, '', '### Fields:', ]; for (const [field, fieldSchema] of Object.entries(schema)) { const required = fieldSchema.required ? '(required)' : '(optional)'; let type = fieldSchema.type; if (fieldSchema.arrayItemType) { type = `${fieldSchema.type}<${fieldSchema.arrayItemType}>`; } lines.push(`- **${field}**: ${type} ${required}`); if (fieldSchema.description) { lines.push(` - ${fieldSchema.description}`); } } return lines.join('\n'); } // ============================================================================= // DATA VALIDATOR CLASS // ============================================================================= class DataValidator { constructor(gameCode) { this.gameCode = gameCode; } validate(payload) { return validateGameData(this.gameCode, payload); } getSchema() { return getSchema(this.gameCode); } getSchemaDoc() { return getSchemaDoc(this.gameCode); } } exports.DataValidator = DataValidator; //# sourceMappingURL=DataValidator.js.map