road-runner-common 0.6.0

Shared Rust utilities for exchange ecosystem backend services.
Documentation
#![cfg(feature = "openapi")]

pub use utoipa::ToSchema;

pub use crate::pagination::{PageRequest, Paged};
pub use crate::response::{ApiErrorDetail, ApiMeta, ApiResponse};

use utoipa::Modify;
use serde_json;

/// Global error response modifier for OpenAPI documentation.
///
/// Automatically injects standard error responses (400, 401, 403, 404, 500, 503)
/// to all API operations using the `ApiResponse<()>` schema from road-runner-common.
///
/// # Usage
///
/// Add this modifier to your OpenAPI document:
///
/// ```rust,ignore
/// #[derive(OpenApi)]
/// #[openapi(
///     paths(...),
///     modifiers(&road_runner_common::openapi::GlobalErrorResponses),
///     components(schemas(ApiResponse, ApiErrorDetail, ApiMeta))
/// )]
/// pub struct ApiDoc;
/// ```
///
/// This will automatically add the following responses to all operations:
/// - 400 Bad Request (validation errors)
/// - 401 Unauthorized (authentication required)
/// - 403 Forbidden (insufficient permissions)
/// - 404 Not Found (resource not found)
/// - 500 Internal Server Error (server errors)
/// - 503 Service Unavailable (service not ready)
pub struct GlobalErrorResponses;

impl Modify for GlobalErrorResponses {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        use utoipa::openapi::{
            content::ContentBuilder,
            response::ResponseBuilder,
            schema::{ArrayBuilder, ObjectBuilder, Schema, SchemaType},
            Ref,
        };

        // Ensure components exists
        let components = openapi
            .components
            .get_or_insert_with(utoipa::openapi::Components::new);

        // Create ApiErrorResponse schema if it doesn't exist
        if !components.schemas.contains_key("ApiErrorResponse") {
            let errors_array = ArrayBuilder::new()
                .items(Ref::from_schema_name("ApiErrorDetail"))
                .build();

            let error_response_schema = ObjectBuilder::new()
                .property(
                    "success",
                    ObjectBuilder::new()
                        .schema_type(SchemaType::Boolean)
                        .example(Some(serde_json::json!(false)))
                        .build(),
                )
                .property("errors", errors_array)
                .required("success")
                .required("errors")
                .property("meta", Ref::from_schema_name("ApiMeta"))
                .required("meta")
                .build();

            components
                .schemas
                .insert("ApiErrorResponse".to_string(), Schema::from(error_response_schema).into());
        }

        // Wrap 200 responses with ApiResponse structure
        for (_path, item) in openapi.paths.paths.iter_mut() {
            for (_ty, operation) in item.operations.iter_mut() {
                if let Some(response) = operation.responses.responses.get_mut("200") {
                    use utoipa::openapi::RefOr;
                    
                    // Get the current schema from the 200 response
                    if let RefOr::T(response_obj) = response {
                        if let Some(content) = response_obj.content.get_mut("application/json") {
                            // Store the original data schema and example
                            let original_schema = content.schema.clone();
                            let original_example = content.example.clone();
                            
                            // Create ApiResponse wrapper schema
                            let api_response_schema = ObjectBuilder::new()
                                .property(
                                    "success",
                                    ObjectBuilder::new()
                                        .schema_type(SchemaType::Boolean)
                                        .example(Some(serde_json::json!(true)))
                                        .build(),
                                )
                                .property("data", original_schema)
                                .property(
                                    "errors",
                                    ArrayBuilder::new()
                                        .items(Ref::from_schema_name("ApiErrorDetail"))
                                        .build(),
                                )
                                .property("meta", Ref::from_schema_name("ApiMeta"))
                                .required("success")
                                .required("meta")
                                .build();
                            
                            // Replace the schema with ApiResponse wrapper
                            content.schema = Schema::from(api_response_schema).into();
                            
                            // Wrap the example in ApiResponse structure if it exists
                            if let Some(data_example) = original_example {
                                let wrapped_example = serde_json::json!({
                                    "success": true,
                                    "data": data_example,
                                    "errors": [],
                                    "meta": {
                                        "request_id": "550e8400-e29b-41d4-a716-446655440000",
                                        "timestamp": "2024-01-20T15:30:45.123Z"
                                    }
                                });
                                content.example = Some(wrapped_example);
                            } else {
                                // If no example exists, create a generic one
                                // Note: This won't have the actual data structure, but at least shows the ApiResponse format
                                let generic_example = serde_json::json!({
                                    "success": true,
                                    "data": null,
                                    "errors": [],
                                    "meta": {
                                        "request_id": "550e8400-e29b-41d4-a716-446655440000",
                                        "timestamp": "2024-01-20T15:30:45.123Z"
                                    }
                                });
                                content.example = Some(generic_example);
                            }
                        }
                    }
                }
            }
        }

        // Inject error responses to all operations
        for (_path, item) in openapi.paths.paths.iter_mut() {
            for (_ty, operation) in item.operations.iter_mut() {
                // 400 Bad Request
                if !operation.responses.responses.contains_key("400") {
                    let bad_request = ResponseBuilder::new()
                        .description("Bad request")
                        .content(
                            "application/json",
                            ContentBuilder::new()
                                .schema(Ref::from_schema_name("ApiErrorResponse"))
                                .example(Some(serde_json::json!({
                                    "success": false,
                                    "errors": [{
                                        "code": "VALIDATION_ERROR",
                                        "message": "Invalid request parameters",
                                        "field": "instrumentId"
                                    }],
                                    "meta": {
                                        "request_id": "550e8400-e29b-41d4-a716-446655440000",
                                        "timestamp": "2024-01-20T15:30:45.123Z"
                                    }
                                })))
                                .build(),
                        )
                        .build();
                    operation
                        .responses
                        .responses
                        .insert("400".to_string(), bad_request.into());
                }

                // 401 Unauthorized
                if !operation.responses.responses.contains_key("401") {
                    let unauthorized = ResponseBuilder::new()
                        .description("Unauthorized")
                        .content(
                            "application/json",
                            ContentBuilder::new()
                                .schema(Ref::from_schema_name("ApiErrorResponse"))
                                .example(Some(serde_json::json!({
                                    "success": false,
                                    "errors": [{
                                        "code": "UNAUTHORIZED",
                                        "message": "Authentication required"
                                    }],
                                    "meta": {
                                        "request_id": "550e8400-e29b-41d4-a716-446655440000",
                                        "timestamp": "2024-01-20T15:30:45.123Z"
                                    }
                                })))
                                .build(),
                        )
                        .build();
                    operation
                        .responses
                        .responses
                        .insert("401".to_string(), unauthorized.into());
                }

                // 403 Forbidden
                if !operation.responses.responses.contains_key("403") {
                    let forbidden = ResponseBuilder::new()
                        .description("Forbidden")
                        .content(
                            "application/json",
                            ContentBuilder::new()
                                .schema(Ref::from_schema_name("ApiErrorResponse"))
                                .example(Some(serde_json::json!({
                                    "success": false,
                                    "errors": [{
                                        "code": "FORBIDDEN",
                                        "message": "Insufficient permissions"
                                    }],
                                    "meta": {
                                        "request_id": "550e8400-e29b-41d4-a716-446655440000",
                                        "timestamp": "2024-01-20T15:30:45.123Z"
                                    }
                                })))
                                .build(),
                        )
                        .build();
                    operation
                        .responses
                        .responses
                        .insert("403".to_string(), forbidden.into());
                }

                // 404 Not Found
                if !operation.responses.responses.contains_key("404") {
                    let not_found = ResponseBuilder::new()
                        .description("Not found")
                        .content(
                            "application/json",
                            ContentBuilder::new()
                                .schema(Ref::from_schema_name("ApiErrorResponse"))
                                .example(Some(serde_json::json!({
                                    "success": false,
                                    "errors": [{
                                        "code": "NOT_FOUND",
                                        "message": "Resource not found"
                                    }],
                                    "meta": {
                                        "request_id": "550e8400-e29b-41d4-a716-446655440000",
                                        "timestamp": "2024-01-20T15:30:45.123Z"
                                    }
                                })))
                                .build(),
                        )
                        .build();
                    operation
                        .responses
                        .responses
                        .insert("404".to_string(), not_found.into());
                }

                // 500 Internal Server Error
                if !operation.responses.responses.contains_key("500") {
                    let internal = ResponseBuilder::new()
                        .description("Internal server error")
                        .content(
                            "application/json",
                            ContentBuilder::new()
                                .schema(Ref::from_schema_name("ApiErrorResponse"))
                                .example(Some(serde_json::json!({
                                    "success": false,
                                    "errors": [{
                                        "code": "INTERNAL_ERROR",
                                        "message": "Internal server error"
                                    }],
                                    "meta": {
                                        "request_id": "550e8400-e29b-41d4-a716-446655440000",
                                        "timestamp": "2024-01-20T15:30:45.123Z"
                                    }
                                })))
                                .build(),
                        )
                        .build();
                    operation
                        .responses
                        .responses
                        .insert("500".to_string(), internal.into());
                }

                // 503 Service Unavailable
                if !operation.responses.responses.contains_key("503") {
                    let unavailable = ResponseBuilder::new()
                        .description("Service unavailable")
                        .content(
                            "application/json",
                            ContentBuilder::new()
                                .schema(Ref::from_schema_name("ApiErrorResponse"))
                                .example(Some(serde_json::json!({
                                    "success": false,
                                    "errors": [{
                                        "code": "EXTERNAL_ERROR",
                                        "message": "Upstream service error"
                                    }],
                                    "meta": {
                                        "request_id": "550e8400-e29b-41d4-a716-446655440000",
                                        "timestamp": "2024-01-20T15:30:45.123Z"
                                    }
                                })))
                                .build(),
                        )
                        .build();
                    operation
                        .responses
                        .responses
                        .insert("503".to_string(), unavailable.into());
                }
            }
        }
    }
}