"""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" } }