cq-test / app /services /schema_validator.py
NANI-Nithin's picture
Phase 1: establish the baseline
eaf7eca
Raw
History Blame Contribute Delete
5.52 kB
"""Schema validation utilities for the game pipeline."""
import json
import jsonschema
from pathlib import Path
from typing import Any, Tuple
def load_schema(schema_name: str = "game_schema.json") -> dict:
"""Load a JSON schema from the schemas directory.
Args:
schema_name: Name of the schema file (default: game_schema.json)
Returns:
Loaded schema as dict
"""
schema_path = Path("app/schemas") / schema_name
with open(schema_path, 'r', encoding='utf-8') as f:
return json.load(f)
def validate_game_schema(game: dict) -> Tuple[bool, list[str]]:
"""Validate a game JSON against the game schema.
Args:
game: Game data to validate
Returns:
Tuple of (is_valid, list of error messages)
"""
schema = load_schema("game_schema.json")
errors = []
try:
jsonschema.validate(instance=game, schema=schema)
return True, []
except jsonschema.ValidationError as e:
errors.append(f"Validation error: {e.message}")
errors.append(f"Path: {'.'.join(str(p) for p in e.absolute_path)}")
return False, errors
except jsonschema.SchemaError as e:
errors.append(f"Schema error: {e.message}")
return False, errors
def validate_task_structure(task: dict) -> Tuple[bool, list[str]]:
"""Validate a single task object.
Args:
task: Task data to validate
Returns:
Tuple of (is_valid, list of error messages)
"""
required_fields = [
'task_id', 'title', 'description', 'location_hint',
'points', 'time_limit_minutes', 'proof_type', 'hint', 'safety_note'
]
errors = []
for field in required_fields:
if field not in task:
errors.append(f"Missing required field: {field}")
# Validate proof_type enum
if 'proof_type' in task:
valid_types = ['photo', 'observation', 'text']
if task['proof_type'] not in valid_types:
errors.append(f"Invalid proof_type: {task['proof_type']}. Must be one of {valid_types}")
# Validate points and time_limit are positive
if 'points' in task and task['points'] < 0:
errors.append(f"Task points must be non-negative, got {task['points']}")
if 'time_limit_minutes' in task and task['time_limit_minutes'] is not None:
if task['time_limit_minutes'] < 0:
errors.append(f"Task time_limit_minutes must be non-negative, got {task['time_limit_minutes']}")
return len(errors) == 0, errors
def validate_safety_structure(safety: dict) -> Tuple[bool, list[str]]:
"""Validate the safety object structure.
Args:
safety: Safety data to validate
Returns:
Tuple of (is_valid, list of error messages)
"""
required_fields = ['allowed_zone', 'forbidden_behaviors', 'adult_supervision', 'stop_conditions']
errors = []
for field in required_fields:
if field not in safety:
errors.append(f"Missing required safety field: {field}")
# Validate field types
if 'forbidden_behaviors' in safety and not isinstance(safety['forbidden_behaviors'], list):
errors.append("forbidden_behaviors must be an array")
if 'stop_conditions' in safety and not isinstance(safety['stop_conditions'], list):
errors.append("stop_conditions must be an array")
if 'adult_supervision' in safety and not isinstance(safety['adult_supervision'], bool):
errors.append("adult_supervision must be a boolean")
return len(errors) == 0, errors
def create_minimal_game_template() -> dict:
"""Create a minimal valid game template for testing.
Returns:
Minimal valid game JSON matching the schema
"""
return {
"game_id": "test-game-001",
"title": "Test Game",
"theme": "test",
"setup": {
"city": "Paris",
"area": "Test Area",
"meeting_point": "Central location",
"duration_minutes": 45,
"num_players": 4
},
"rules": [
"Rule 1",
"Rule 2"
],
"tasks": [
{
"task_id": "t1",
"title": "Task 1",
"description": "Find something",
"location_hint": "Look near the entrance",
"points": 20,
"time_limit_minutes": 10,
"proof_type": "photo",
"hint": "It's visible from the street",
"safety_note": "Stay on public paths"
}
],
"global_hints": [
"Explore systematically"
],
"score_rules": [
"1 point per second under time limit",
"No penalty for hints"
],
"tie_breaker": "Team with most tasks completed first",
"safety": {
"allowed_zone": "Public streets and parks in the designated area",
"forbidden_behaviors": [
"Entering private buildings",
"Crossing major roads unsafely"
],
"adult_supervision": False,
"stop_conditions": [
"Player injury",
"Weather emergency"
]
},
"story_seed": {
"tone": "playful",
"motifs": [
"discovery",
"teamwork"
],
"recap_style": "episode_recap"
}
}