This commit is contained in:
245
G102-sequence/sdk/package/dist/esm/client/DataValidator.js
vendored
Normal file
245
G102-sequence/sdk/package/dist/esm/client/DataValidator.js
vendored
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
import { GAME_CODES } from '../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
|
||||
*/
|
||||
export function validateGameData(gameCode, payload) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
// Check game code
|
||||
if (!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
|
||||
*/
|
||||
export function getSchema(gameCode) {
|
||||
return SCHEMAS[gameCode] ?? null;
|
||||
}
|
||||
/**
|
||||
* Get schema documentation for a game code
|
||||
*/
|
||||
export function getSchemaDoc(gameCode) {
|
||||
const schema = SCHEMAS[gameCode];
|
||||
if (!schema)
|
||||
return `Unknown game code: ${gameCode}`;
|
||||
const gameInfo = 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
|
||||
// =============================================================================
|
||||
export class DataValidator {
|
||||
constructor(gameCode) {
|
||||
this.gameCode = gameCode;
|
||||
}
|
||||
validate(payload) {
|
||||
return validateGameData(this.gameCode, payload);
|
||||
}
|
||||
getSchema() {
|
||||
return getSchema(this.gameCode);
|
||||
}
|
||||
getSchemaDoc() {
|
||||
return getSchemaDoc(this.gameCode);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=DataValidator.js.map
|
||||
Reference in New Issue
Block a user