# Copyright (c) 2023-present Plane Software, Inc. and contributors # SPDX-License-Identifier: AGPL-3.0-only # See the LICENSE file for details. """ Common OpenAPI responses for drf-spectacular. This module provides reusable response definitions for common HTTP status codes and scenarios that occur across multiple API endpoints. """ from drf_spectacular.utils import OpenApiResponse, OpenApiExample, inline_serializer from rest_framework import serializers from .examples import get_sample_for_schema # Authentication & Authorization Responses UNAUTHORIZED_RESPONSE = OpenApiResponse( description="Authentication credentials were not provided or are invalid.", examples=[ OpenApiExample( name="Unauthorized", value={ "error": "Authentication credentials were not provided", "error_code": "AUTHENTICATION_REQUIRED", }, ) ], ) FORBIDDEN_RESPONSE = OpenApiResponse( description="Permission denied. User lacks required permissions.", examples=[ OpenApiExample( name="Forbidden", value={ "error": "You do not have permission to perform this action", "error_code": "PERMISSION_DENIED", }, ) ], ) # Resource Responses NOT_FOUND_RESPONSE = OpenApiResponse( description="The requested resource was not found.", examples=[ OpenApiExample( name="Not Found", value={"error": "Not found", "error_code": "RESOURCE_NOT_FOUND"}, ) ], ) VALIDATION_ERROR_RESPONSE = OpenApiResponse( description="Validation error occurred with the provided data.", examples=[ OpenApiExample( name="Validation Error", value={ "error": "Validation failed", "details": {"field_name": ["This field is required."]}, }, ) ], ) # Generic Success Responses DELETED_RESPONSE = OpenApiResponse( description="Resource deleted successfully", examples=[ OpenApiExample( name="Deleted Successfully", value={"message": "Resource deleted successfully"}, ) ], ) ARCHIVED_RESPONSE = OpenApiResponse( description="Resource archived successfully", examples=[ OpenApiExample( name="Archived Successfully", value={"message": "Resource archived successfully"}, ) ], ) UNARCHIVED_RESPONSE = OpenApiResponse( description="Resource unarchived successfully", examples=[ OpenApiExample( name="Unarchived Successfully", value={"message": "Resource unarchived successfully"}, ) ], ) # Specific Error Responses INVALID_REQUEST_RESPONSE = OpenApiResponse( description="Invalid request data provided", examples=[ OpenApiExample( name="Invalid Request", value={ "error": "Invalid request data", "details": "Specific validation errors", }, ) ], ) CONFLICT_RESPONSE = OpenApiResponse( description="Resource conflict - duplicate or constraint violation", examples=[ OpenApiExample( name="Resource Conflict", value={ "error": "Resource with the same identifier already exists", "id": "550e8400-e29b-41d4-a716-446655440000", }, ) ], ) ADMIN_ONLY_RESPONSE = OpenApiResponse( description="Only admin or creator can perform this action", examples=[ OpenApiExample( name="Admin Only", value={"error": "Only admin or creator can perform this action"}, ) ], ) CANNOT_DELETE_RESPONSE = OpenApiResponse( description="Resource cannot be deleted due to constraints", examples=[ OpenApiExample( name="Cannot Delete", value={"error": "Resource cannot be deleted", "reason": "Has dependencies"}, ) ], ) CANNOT_ARCHIVE_RESPONSE = OpenApiResponse( description="Resource cannot be archived in current state", examples=[ OpenApiExample( name="Cannot Archive", value={ "error": "Resource cannot be archived", "reason": "Not in valid state", }, ) ], ) REQUIRED_FIELDS_RESPONSE = OpenApiResponse( description="Required fields are missing", examples=[ OpenApiExample( name="Required Fields Missing", value={"error": "Required fields are missing", "fields": ["name", "type"]}, ) ], ) # Project-specific Responses PROJECT_NOT_FOUND_RESPONSE = OpenApiResponse( description="Project not found", examples=[ OpenApiExample( name="Project Not Found", value={"error": "Project not found"}, ) ], ) WORKSPACE_NOT_FOUND_RESPONSE = OpenApiResponse( description="Workspace not found", examples=[ OpenApiExample( name="Workspace Not Found", value={"error": "Workspace not found"}, ) ], ) PROJECT_NAME_TAKEN_RESPONSE = OpenApiResponse( description="Project name already taken", examples=[ OpenApiExample( name="Project Name Taken", value={"error": "Project name already taken"}, ) ], ) # Issue-specific Responses ISSUE_NOT_FOUND_RESPONSE = OpenApiResponse( description="Issue not found", examples=[ OpenApiExample( name="Issue Not Found", value={"error": "Issue not found"}, ) ], ) WORK_ITEM_NOT_FOUND_RESPONSE = OpenApiResponse( description="Work item not found", examples=[ OpenApiExample( name="Work Item Not Found", value={"error": "Work item not found"}, ) ], ) EXTERNAL_ID_EXISTS_RESPONSE = OpenApiResponse( description="Resource with same external ID already exists", examples=[ OpenApiExample( name="External ID Exists", value={ "error": "Resource with the same external id and external source already exists", # noqa: E501 "id": "550e8400-e29b-41d4-a716-446655440000", }, ) ], ) # Label-specific Responses LABEL_NOT_FOUND_RESPONSE = OpenApiResponse( description="Label not found", examples=[ OpenApiExample( name="Label Not Found", value={"error": "Label not found"}, ) ], ) LABEL_NAME_EXISTS_RESPONSE = OpenApiResponse( description="Label with the same name already exists", examples=[ OpenApiExample( name="Label Name Exists", value={"error": "Label with the same name already exists in the project"}, ) ], ) # Module-specific Responses MODULE_NOT_FOUND_RESPONSE = OpenApiResponse( description="Module not found", examples=[ OpenApiExample( name="Module Not Found", value={"error": "Module not found"}, ) ], ) MODULE_ISSUE_NOT_FOUND_RESPONSE = OpenApiResponse( description="Module issue not found", examples=[ OpenApiExample( name="Module Issue Not Found", value={"error": "Module issue not found"}, ) ], ) # Cycle-specific Responses CYCLE_CANNOT_ARCHIVE_RESPONSE = OpenApiResponse( description="Cycle cannot be archived", examples=[ OpenApiExample( name="Cycle Cannot Archive", value={"error": "Only completed cycles can be archived"}, ) ], ) # State-specific Responses STATE_NAME_EXISTS_RESPONSE = OpenApiResponse( description="State with the same name already exists", examples=[ OpenApiExample( name="State Name Exists", value={"error": "State with the same name already exists"}, ) ], ) STATE_CANNOT_DELETE_RESPONSE = OpenApiResponse( description="State cannot be deleted", examples=[ OpenApiExample( name="State Cannot Delete", value={ "error": "State cannot be deleted", "reason": "Default state or has issues", }, ) ], ) # Comment-specific Responses COMMENT_NOT_FOUND_RESPONSE = OpenApiResponse( description="Comment not found", examples=[ OpenApiExample( name="Comment Not Found", value={"error": "Comment not found"}, ) ], ) # Link-specific Responses LINK_NOT_FOUND_RESPONSE = OpenApiResponse( description="Link not found", examples=[ OpenApiExample( name="Link Not Found", value={"error": "Link not found"}, ) ], ) # Attachment-specific Responses ATTACHMENT_NOT_FOUND_RESPONSE = OpenApiResponse( description="Attachment not found", examples=[ OpenApiExample( name="Attachment Not Found", value={"error": "Attachment not found"}, ) ], ) # Search-specific Responses BAD_SEARCH_REQUEST_RESPONSE = OpenApiResponse( description="Bad request - invalid search parameters", examples=[ OpenApiExample( name="Bad Search Request", value={"error": "Invalid search parameters"}, ) ], ) # Pagination Response Templates def create_paginated_response( item_schema, schema_name, description="Paginated results", example_name="Paginated Response", ): """Create a paginated response with the specified item schema""" return OpenApiResponse( description=description, response=inline_serializer( name=schema_name, fields={ "grouped_by": serializers.CharField(allow_null=True), "sub_grouped_by": serializers.CharField(allow_null=True), "total_count": serializers.IntegerField(), "next_cursor": serializers.CharField(), "prev_cursor": serializers.CharField(), "next_page_results": serializers.BooleanField(), "prev_page_results": serializers.BooleanField(), "count": serializers.IntegerField(), "total_pages": serializers.IntegerField(), "total_results": serializers.IntegerField(), "extra_stats": serializers.CharField(allow_null=True), "results": serializers.ListField(child=item_schema()), }, ), examples=[ OpenApiExample( name=example_name, value={ "grouped_by": "state", "sub_grouped_by": "priority", "total_count": 150, "next_cursor": "20:1:0", "prev_cursor": "20:0:0", "next_page_results": True, "prev_page_results": False, "count": 20, "total_pages": 8, "total_results": 150, "extra_stats": None, "results": [get_sample_for_schema(schema_name)], }, summary=example_name, ) ], ) # Asset-specific Responses PRESIGNED_URL_SUCCESS_RESPONSE = OpenApiResponse(description="Presigned URL generated successfully") GENERIC_ASSET_UPLOAD_SUCCESS_RESPONSE = OpenApiResponse( description="Presigned URL generated successfully", examples=[ OpenApiExample( name="Generic Asset Upload Response", value={ "upload_data": { "url": "https://s3.amazonaws.com/bucket-name", "fields": { "key": "workspace-id/uuid-filename.pdf", "AWSAccessKeyId": "AKIA...", "policy": "eyJ...", "signature": "abc123...", }, }, "asset_id": "550e8400-e29b-41d4-a716-446655440000", "asset_url": "https://cdn.example.com/workspace-id/uuid-filename.pdf", }, ) ], ) GENERIC_ASSET_VALIDATION_ERROR_RESPONSE = OpenApiResponse( description="Validation error", examples=[ OpenApiExample( name="Missing required fields", value={"error": "Name and size are required fields.", "status": False}, ), OpenApiExample( name="Invalid file type", value={"error": "Invalid file type.", "status": False}, ), ], ) ASSET_CONFLICT_RESPONSE = OpenApiResponse( description="Asset with same external ID already exists", examples=[ OpenApiExample( name="Duplicate external asset", value={ "message": "Asset with same external id and source already exists", "asset_id": "550e8400-e29b-41d4-a716-446655440000", "asset_url": "https://cdn.example.com/existing-file.pdf", }, ) ], ) ASSET_DOWNLOAD_SUCCESS_RESPONSE = OpenApiResponse( description="Presigned download URL generated successfully", examples=[ OpenApiExample( name="Asset Download Response", value={ "asset_id": "550e8400-e29b-41d4-a716-446655440000", "asset_url": "https://s3.amazonaws.com/bucket/file.pdf?signed-url", "asset_name": "document.pdf", "asset_type": "application/pdf", }, ) ], ) ASSET_DOWNLOAD_ERROR_RESPONSE = OpenApiResponse( description="Bad request", examples=[ OpenApiExample(name="Asset not uploaded", value={"error": "Asset not yet uploaded"}), ], ) ASSET_UPDATED_RESPONSE = OpenApiResponse(description="Asset updated successfully") ASSET_DELETED_RESPONSE = OpenApiResponse(description="Asset deleted successfully") ASSET_NOT_FOUND_RESPONSE = OpenApiResponse( description="Asset not found", examples=[OpenApiExample(name="Asset not found", value={"error": "Asset not found"})], )