""" Schema extensions and customizations for drf-spectacular. This module provides custom extensions to improve OpenAPI schema generation for the ThrillWiki API, including better documentation and examples. """ from drf_spectacular.openapi import AutoSchema # Custom examples for common serializers PARK_EXAMPLE = { "id": 1, "name": "Cedar Point", "slug": "cedar-point", "description": "The Roller Coaster Capital of the World", "status": "OPERATING", "opening_date": "1870-07-04", "closing_date": None, "location": { "latitude": 41.4793, "longitude": -82.6833, "city": "Sandusky", "state": "Ohio", "country": "United States", "formatted_address": "Sandusky, OH, United States", }, "operator": { "id": 1, "name": "Cedar Fair", "slug": "cedar-fair", "roles": ["OPERATOR", "PROPERTY_OWNER"], }, "property_owner": { "id": 1, "name": "Cedar Fair", "slug": "cedar-fair", "roles": ["OPERATOR", "PROPERTY_OWNER"], }, "area_count": 15, "ride_count": 70, "operating_rides_count": 68, "roller_coaster_count": 17, } RIDE_EXAMPLE = { "id": 1, "name": "Steel Vengeance", "slug": "steel-vengeance", "description": "A hybrid wooden/steel roller coaster", "category": "ROLLER_COASTER", "status": "OPERATING", "opening_date": "2018-05-05", "closing_date": None, "park": {"id": 1, "name": "Cedar Point", "slug": "cedar-point"}, "manufacturer": { "id": 1, "name": "Rocky Mountain Construction", "slug": "rmc", "roles": ["MANUFACTURER"], }, "designer": { "id": 1, "name": "Rocky Mountain Construction", "slug": "rmc", "roles": ["DESIGNER"], }, "height_feet": 205, "length_feet": 5740, "speed_mph": 74, "inversions": 4, "duration_seconds": 150, "capacity_per_hour": 1200, "minimum_height_inches": 48, "maximum_height_inches": None, } COMPANY_EXAMPLE = { "id": 1, "name": "Cedar Fair", "slug": "cedar-fair", "roles": ["OPERATOR", "PROPERTY_OWNER"], } LOCATION_EXAMPLE = { "latitude": 41.4793, "longitude": -82.6833, "city": "Sandusky", "state": "Ohio", "country": "United States", "formatted_address": "Sandusky, OH, United States", } HISTORY_EVENT_EXAMPLE = { "id": "12345678-1234-5678-9012-123456789012", "pgh_created_at": "2024-01-15T14:30:00Z", "pgh_label": "updated", "pgh_model": "parks.park", "pgh_obj_id": 1, "pgh_context": { "user_id": 42, "request_id": "req_abc123", "ip_address": "192.168.1.100", }, "changed_fields": ["name", "description"], "field_changes": { "name": {"old_value": "Cedar Point Amusement Park", "new_value": "Cedar Point"}, "description": { "old_value": "America's Roller Coast", "new_value": "The Roller Coaster Capital of the World", }, }, } PARK_HISTORY_EXAMPLE = { "park": PARK_EXAMPLE, "current_state": PARK_EXAMPLE, "summary": { "total_events": 25, "first_recorded": "2023-01-01T00:00:00Z", "last_modified": "2024-01-15T14:30:00Z", "significant_changes": [ { "date": "2024-01-15T14:30:00Z", "event_type": "updated", "description": "Name and description updated", }, { "date": "2023-06-01T10:00:00Z", "event_type": "updated", "description": "Operating status changed", }, ], }, "events": [HISTORY_EVENT_EXAMPLE], } UNIFIED_HISTORY_TIMELINE_EXAMPLE = { "summary": { "total_events": 1250, "events_returned": 100, "event_type_breakdown": {"created": 45, "updated": 180, "deleted": 5}, "model_type_breakdown": { "parks.park": 75, "rides.ride": 120, "companies.operator": 15, "companies.manufacturer": 25, "accounts.user": 30, }, "time_range": { "earliest": "2023-01-01T00:00:00Z", "latest": "2024-01-15T14:30:00Z", }, }, "events": [ { "id": "event_001", "pgh_created_at": "2024-01-15T14:30:00Z", "pgh_label": "updated", "pgh_model": "parks.park", "pgh_obj_id": 1, "entity_name": "Cedar Point", "entity_slug": "cedar-point", "change_significance": "minor", "change_summary": "Park description updated", }, { "id": "event_002", "pgh_created_at": "2024-01-15T12:00:00Z", "pgh_label": "created", "pgh_model": "rides.ride", "pgh_obj_id": 100, "entity_name": "New Roller Coaster", "entity_slug": "new-roller-coaster", "change_significance": "major", "change_summary": "New ride added to park", }, ], } # OpenAPI schema customizations def custom_preprocessing_hook(endpoints): """ Custom preprocessing hook to modify endpoints before schema generation. This can be used to filter out certain endpoints, modify their metadata, or add custom documentation. """ # Filter out any endpoints we don't want in the public API filtered = [] for path, path_regex, method, callback in endpoints: # Skip internal or debug endpoints if "/debug/" not in path and "/internal/" not in path: filtered.append((path, path_regex, method, callback)) return filtered def custom_postprocessing_hook(result, generator, request, public): """ Custom postprocessing hook to modify the generated schema. This can be used to add custom metadata, modify response schemas, or enhance the overall API documentation. """ # Add custom info to the schema if "info" in result: result["info"]["contact"] = { "name": "ThrillWiki API Support", "email": "api@thrillwiki.com", "url": "https://thrillwiki.com/support", } result["info"]["license"] = { "name": "MIT", "url": "https://opensource.org/licenses/MIT", } # Add custom tags with descriptions if "tags" not in result: result["tags"] = [] result["tags"].extend( [ { "name": "Parks", "description": "Operations related to theme parks, including CRUD operations and statistics", }, { "name": "Rides", "description": "Operations related to rides and attractions within theme parks", }, { "name": "History", "description": "Historical change tracking for all entities, providing complete audit trails and version history", "externalDocs": { "description": "Learn more about pghistory", "url": "https://django-pghistory.readthedocs.io/", }, }, { "name": "Statistics", "description": "Statistical endpoints providing aggregated data and insights", }, { "name": "Reviews", "description": "User reviews and ratings for parks and rides", }, { "name": "Authentication", "description": "User authentication and account management endpoints", }, { "name": "Health", "description": "System health checks and monitoring endpoints", }, { "name": "Recent Changes", "description": "Endpoints for accessing recently changed entities by type and change category", }, ] ) # Add custom servers if not present if "servers" not in result: result["servers"] = [ { "url": "https://api.thrillwiki.com/v1", "description": "Production server", }, { "url": "https://staging-api.thrillwiki.com/v1", "description": "Staging server", }, { "url": "http://localhost:8000/api/v1", "description": "Development server", }, ] return result # Custom AutoSchema class for enhanced documentation class ThrillWikiAutoSchema(AutoSchema): """ Custom AutoSchema class that provides enhanced documentation for ThrillWiki API endpoints. """ def get_operation_id(self): """Generate meaningful operation IDs.""" if hasattr(self.view, "basename"): basename = self.view.basename else: basename = getattr(self.view, "__class__", self.view).__name__.lower() if basename.endswith("viewset"): basename = basename[:-7] # Remove 'viewset' suffix action = self.method_mapping.get(self.method.lower(), self.method.lower()) return f"{basename}_{action}" def get_tags(self): """Generate tags based on the viewset.""" if hasattr(self.view, "basename"): return [self.view.basename.title()] return super().get_tags() def get_summary(self): """Generate summary from docstring or method name.""" summary = super().get_summary() if summary: return summary # Generate from method and model action = self.method_mapping.get(self.method.lower(), self.method.lower()) model_name = getattr(self.view, "basename", "resource") action_map = { "list": f"List {model_name}", "create": f"Create {model_name}", "retrieve": f"Get {model_name} details", "update": f"Update {model_name}", "partial_update": f"Partially update {model_name}", "destroy": f"Delete {model_name}", } return action_map.get(action, f"{action.title()} {model_name}")