""" Shared Contract Serializers for ThrillWiki API This module contains standardized serializers that enforce consistent formats across all API responses, ensuring they match frontend TypeScript interfaces exactly. These serializers prevent contract violations by providing a single source of truth for common data structures used throughout the API. """ from rest_framework import serializers from typing import Dict, Any, List class FilterOptionSerializer(serializers.Serializer): """ Standard filter option format - matches frontend TypeScript exactly. Frontend TypeScript interface: interface FilterOption { value: string; label: string; count?: number; selected?: boolean; } """ value = serializers.CharField( help_text="The actual value used for filtering" ) label = serializers.CharField( help_text="Human-readable display label" ) count = serializers.IntegerField( required=False, allow_null=True, help_text="Number of items matching this filter option" ) selected = serializers.BooleanField( default=False, help_text="Whether this option is currently selected" ) class FilterRangeSerializer(serializers.Serializer): """ Standard range filter format. Frontend TypeScript interface: interface FilterRange { min: number; max: number; step: number; unit?: string; } """ min = serializers.FloatField( allow_null=True, help_text="Minimum value for the range" ) max = serializers.FloatField( allow_null=True, help_text="Maximum value for the range" ) step = serializers.FloatField( default=1.0, help_text="Step size for range inputs" ) unit = serializers.CharField( required=False, allow_null=True, help_text="Unit of measurement (e.g., 'feet', 'mph', 'stars')" ) class BooleanFilterSerializer(serializers.Serializer): """ Standard boolean filter format. Frontend TypeScript interface: interface BooleanFilter { key: string; label: string; description: string; } """ key = serializers.CharField( help_text="The filter parameter key" ) label = serializers.CharField( help_text="Human-readable label for the filter" ) description = serializers.CharField( help_text="Description of what this filter does" ) class OrderingOptionSerializer(serializers.Serializer): """ Standard ordering option format. Frontend TypeScript interface: interface OrderingOption { value: string; label: string; } """ value = serializers.CharField( help_text="The ordering parameter value" ) label = serializers.CharField( help_text="Human-readable label for the ordering option" ) class StandardizedFilterMetadataSerializer(serializers.Serializer): """ Matches frontend TypeScript interface exactly. This serializer ensures all filter metadata responses follow the same structure that the frontend expects, preventing runtime type errors. """ categorical = serializers.DictField( child=FilterOptionSerializer(many=True), help_text="Categorical filter options with value/label/count structure" ) ranges = serializers.DictField( child=FilterRangeSerializer(), help_text="Range filter metadata with min/max/step/unit" ) total_count = serializers.IntegerField( help_text="Total number of items in the filtered dataset" ) ordering_options = FilterOptionSerializer( many=True, required=False, help_text="Available ordering options" ) boolean_filters = BooleanFilterSerializer( many=True, required=False, help_text="Available boolean filter options" ) class PaginationMetadataSerializer(serializers.Serializer): """ Standard pagination metadata format. Frontend TypeScript interface: interface PaginationMetadata { count: number; next: string | null; previous: string | null; page_size: number; current_page: number; total_pages: number; } """ count = serializers.IntegerField( help_text="Total number of items across all pages" ) next = serializers.URLField( allow_null=True, required=False, help_text="URL for the next page of results" ) previous = serializers.URLField( allow_null=True, required=False, help_text="URL for the previous page of results" ) page_size = serializers.IntegerField( help_text="Number of items per page" ) current_page = serializers.IntegerField( help_text="Current page number (1-indexed)" ) total_pages = serializers.IntegerField( help_text="Total number of pages" ) class ApiResponseSerializer(serializers.Serializer): """ Standard API response wrapper. Frontend TypeScript interface: interface ApiResponse { success: boolean; data?: T; message?: string; errors?: ValidationError; } """ success = serializers.BooleanField( help_text="Whether the request was successful" ) response_data = serializers.JSONField( required=False, help_text="Response data (structure varies by endpoint)", source='data' ) message = serializers.CharField( required=False, help_text="Human-readable message about the operation" ) response_errors = serializers.DictField( required=False, help_text="Validation errors (field -> error messages)", source='errors' ) class ErrorResponseSerializer(serializers.Serializer): """ Standard error response format. Frontend TypeScript interface: interface ApiError { status: "error"; error: { code: string; message: string; details?: any; request_user?: string; }; data: null; } """ status = serializers.CharField( default="error", help_text="Response status indicator" ) error = serializers.DictField( help_text="Error details" ) response_data = serializers.JSONField( default=None, allow_null=True, help_text="Always null for error responses", source='data' ) class LocationSerializer(serializers.Serializer): """ Standard location format. Frontend TypeScript interface: interface Location { city: string; state?: string; country: string; address?: string; latitude?: number; longitude?: number; } """ city = serializers.CharField( help_text="City name" ) state = serializers.CharField( required=False, allow_null=True, help_text="State/province name" ) country = serializers.CharField( help_text="Country name" ) address = serializers.CharField( required=False, allow_null=True, help_text="Street address" ) latitude = serializers.FloatField( required=False, allow_null=True, help_text="Latitude coordinate" ) longitude = serializers.FloatField( required=False, allow_null=True, help_text="Longitude coordinate" ) # Alias for backward compatibility LocationOutputSerializer = LocationSerializer class CompanyOutputSerializer(serializers.Serializer): """ Standard company output format. Frontend TypeScript interface: interface Company { id: number; name: string; slug: string; roles?: string[]; } """ id = serializers.IntegerField( help_text="Company ID" ) name = serializers.CharField( help_text="Company name" ) slug = serializers.SlugField( help_text="URL-friendly identifier" ) roles = serializers.ListField( child=serializers.CharField(), required=False, help_text="Company roles (manufacturer, operator, etc.)" ) class ModelChoices: """ Utility class to provide model choices for serializers using Rich Choice Objects. This prevents circular imports while providing access to model choices from the registry. NO FALLBACKS - All choices must be properly defined in Rich Choice Objects. """ @staticmethod def get_park_status_choices(): """Get park status choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("statuses", "parks") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_ride_status_choices(): """Get ride status choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("statuses", "rides") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_company_role_choices(): """Get company role choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices # Get rides domain company roles (MANUFACTURER, DESIGNER) rides_choices = get_choices("company_roles", "rides") # Get parks domain company roles (OPERATOR, PROPERTY_OWNER) parks_choices = get_choices("company_roles", "parks") all_choices = list(rides_choices) + list(parks_choices) return [(choice.value, choice.label) for choice in all_choices] @staticmethod def get_ride_category_choices(): """Get ride category choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("categories", "rides") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_ride_post_closing_choices(): """Get ride post-closing status choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("post_closing_statuses", "rides") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_coaster_track_choices(): """Get coaster track material choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("track_materials", "rides") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_coaster_type_choices(): """Get coaster type choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("coaster_types", "rides") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_launch_choices(): """Get launch system choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("launch_systems", "rides") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_photo_type_choices(): """Get photo type choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("photo_types", "rides") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_spec_category_choices(): """Get technical specification category choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("spec_categories", "rides") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_technical_spec_category_choices(): """Get technical specification category choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("spec_categories", "rides") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_target_market_choices(): """Get target market choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("target_markets", "rides") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_entity_type_choices(): """Get entity type choices for search functionality.""" from apps.core.choices.registry import get_choices choices = get_choices("entity_types", "core") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_health_status_choices(): """Get health check status choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("health_statuses", "core") return [(choice.value, choice.label) for choice in choices] @staticmethod def get_simple_health_status_choices(): """Get simple health check status choices from Rich Choice registry.""" from apps.core.choices.registry import get_choices choices = get_choices("simple_health_statuses", "core") return [(choice.value, choice.label) for choice in choices] class EntityReferenceSerializer(serializers.Serializer): """ Standard entity reference format. Frontend TypeScript interface: interface Entity { id: number; name: string; slug: string; } """ id = serializers.IntegerField( help_text="Unique identifier" ) name = serializers.CharField( help_text="Display name" ) slug = serializers.SlugField( help_text="URL-friendly identifier" ) class ImageVariantsSerializer(serializers.Serializer): """ Standard image variants format. Frontend TypeScript interface: interface ImageVariants { thumbnail: string; medium: string; large: string; avatar?: string; } """ thumbnail = serializers.URLField( help_text="Thumbnail size image URL" ) medium = serializers.URLField( help_text="Medium size image URL" ) large = serializers.URLField( help_text="Large size image URL" ) avatar = serializers.URLField( required=False, help_text="Avatar size image URL (for user avatars)" ) class PhotoSerializer(serializers.Serializer): """ Standard photo format. Frontend TypeScript interface: interface Photo { id: number; image_variants: ImageVariants; alt_text?: string; image_url?: string; caption?: string; photo_type?: string; uploaded_by?: UserInfo; uploaded_at?: string; } """ id = serializers.IntegerField( help_text="Photo ID" ) image_variants = ImageVariantsSerializer( help_text="Available image size variants" ) alt_text = serializers.CharField( required=False, allow_null=True, help_text="Alternative text for accessibility" ) image_url = serializers.URLField( required=False, help_text="Primary image URL (for compatibility)" ) caption = serializers.CharField( required=False, allow_null=True, help_text="Photo caption" ) photo_type = serializers.CharField( required=False, allow_null=True, help_text="Type/category of photo" ) uploaded_by = EntityReferenceSerializer( required=False, help_text="User who uploaded the photo" ) uploaded_at = serializers.DateTimeField( required=False, help_text="When the photo was uploaded" ) class UserInfoSerializer(serializers.Serializer): """ Standard user info format. Frontend TypeScript interface: interface UserInfo { id: number; username: string; display_name: string; avatar_url?: string; } """ id = serializers.IntegerField( help_text="User ID" ) username = serializers.CharField( help_text="Username" ) display_name = serializers.CharField( help_text="Display name" ) avatar_url = serializers.URLField( required=False, allow_null=True, help_text="User avatar URL" ) def validate_filter_metadata_contract(data: Dict[str, Any]) -> Dict[str, Any]: """ Validate that filter metadata follows the expected contract. This function can be used in views to ensure filter metadata matches the frontend TypeScript interface before returning it. Args: data: Filter metadata dictionary Returns: Validated and potentially transformed data Raises: serializers.ValidationError: If data doesn't match contract """ serializer = StandardizedFilterMetadataSerializer(data=data) serializer.is_valid(raise_exception=True) # Return validated_data directly - it's already a dict return serializer.validated_data def ensure_filter_option_format(options: List[Any]) -> List[Dict[str, Any]]: """ Ensure a list of filter options follows the expected format. This utility function converts various input formats to the standard FilterOption format expected by the frontend. Args: options: List of options in various formats Returns: List of options in standard format """ standardized = [] for option in options: if isinstance(option, dict): # Already in correct format or close to it standardized_option = { 'value': str(option.get('value', option.get('id', ''))), 'label': option.get('label', option.get('name', str(option.get('value', '')))), 'count': option.get('count'), 'selected': option.get('selected', False) } elif hasattr(option, 'value') and hasattr(option, 'label'): # RichChoice object format standardized_option = { 'value': str(option.value), 'label': str(option.label), 'count': None, 'selected': False } else: # Simple value - use as both value and label standardized_option = { 'value': str(option), 'label': str(option), 'count': None, 'selected': False } standardized.append(standardized_option) return standardized def ensure_range_format(range_data: Dict[str, Any]) -> Dict[str, Any]: """ Ensure range data follows the expected format. Args: range_data: Range data dictionary Returns: Range data in standard format """ return { 'min': range_data.get('min'), 'max': range_data.get('max'), 'step': range_data.get('step', 1.0), 'unit': range_data.get('unit') }