kegani 0.1.1

A developer-friendly, ergonomic, production-ready Rust web framework
Documentation
//! Response types and builders for Kegani
//!
//! Provides `Json<T>`, `Status`, and `Response` for building HTTP responses.

use actix_web::{HttpResponse, Responder, http::StatusCode, http::header};
use serde::Serialize;
use std::collections::HashMap;

/// JSON response wrapper with optional status code
pub struct Json<T> {
    data: T,
    status: StatusCode,
}

impl<T> Json<T> {
    /// Create a new JSON response with OK status
    pub fn new(data: T) -> Self {
        Self {
            data,
            status: StatusCode::OK,
        }
    }

    /// Create a JSON response with custom status
    pub fn with_status(data: T, status: StatusCode) -> Self {
        Self { data, status }
    }

    /// Create a JSON response for CREATED (201)
    pub fn created(data: T) -> Self {
        Self {
            data,
            status: StatusCode::CREATED,
        }
    }

    /// Create a JSON response for ACCEPTED (202)
    pub fn accepted(data: T) -> Self {
        Self {
            data,
            status: StatusCode::ACCEPTED,
        }
    }
}

impl<T: Serialize> Responder for Json<T> {
    type Body = actix_web::body::BoxBody;

    fn respond_to(self, _: &actix_web::HttpRequest) -> HttpResponse<Self::Body> {
        HttpResponse::build(self.status)
            .content_type("application/json")
            .json(&self.data)
    }
}

impl<T: Serialize> From<T> for Json<T> {
    fn from(data: T) -> Self {
        Json::new(data)
    }
}

/// Empty JSON response for NO_CONTENT
pub struct EmptyJson;

impl Responder for EmptyJson {
    type Body = actix_web::body::BoxBody;

    fn respond_to(self, _: &actix_web::HttpRequest) -> HttpResponse<Self::Body> {
        HttpResponse::NoContent()
            .content_type("application/json")
            .finish()
    }
}

/// HTTP status code helper with named constructors
#[derive(Debug, Clone, Copy)]
pub struct Status(StatusCode);

impl Status {
    /// Create a status with the given code
    pub fn new(code: u16) -> Self {
        Status(StatusCode::from_u16(code).unwrap_or(StatusCode::OK))
    }

    /// Get the status code
    pub fn code(&self) -> StatusCode {
        self.0
    }

    // --- 2xx Success ---
    pub const fn ok() -> Self {
        Status(StatusCode::OK)
    }

    pub const fn created() -> Self {
        Status(StatusCode::CREATED)
    }

    pub const fn accepted() -> Self {
        Status(StatusCode::ACCEPTED)
    }

    pub const fn no_content() -> Self {
        Status(StatusCode::NO_CONTENT)
    }

    // --- 3xx Redirection ---
    pub const fn moved_permanently() -> Self {
        Status(StatusCode::MOVED_PERMANENTLY)
    }

    pub const fn found() -> Self {
        Status(StatusCode::FOUND)
    }

    pub const fn not_modified() -> Self {
        Status(StatusCode::NOT_MODIFIED)
    }

    // --- 4xx Client Error ---
    pub const fn bad_request() -> Self {
        Status(StatusCode::BAD_REQUEST)
    }

    pub const fn unauthorized() -> Self {
        Status(StatusCode::UNAUTHORIZED)
    }

    pub const fn forbidden() -> Self {
        Status(StatusCode::FORBIDDEN)
    }

    pub const fn not_found() -> Self {
        Status(StatusCode::NOT_FOUND)
    }

    pub const fn conflict() -> Self {
        Status(StatusCode::CONFLICT)
    }

    pub const fn unprocessable_entity() -> Self {
        Status(StatusCode::UNPROCESSABLE_ENTITY)
    }

    pub const fn too_many_requests() -> Self {
        Status(StatusCode::TOO_MANY_REQUESTS)
    }

    // --- 5xx Server Error ---
    pub const fn internal_server_error() -> Self {
        Status(StatusCode::INTERNAL_SERVER_ERROR)
    }

    pub const fn not_implemented() -> Self {
        Status(StatusCode::NOT_IMPLEMENTED)
    }

    pub const fn service_unavailable() -> Self {
        Status(StatusCode::SERVICE_UNAVAILABLE)
    }
}

impl Responder for Status {
    type Body = actix_web::body::BoxBody;

    fn respond_to(self, _: &actix_web::HttpRequest) -> HttpResponse<Self::Body> {
        HttpResponse::build(self.0).finish()
    }
}

impl From<StatusCode> for Status {
    fn from(code: StatusCode) -> Self {
        Status(code)
    }
}

/// Response builder for more control
pub struct Response {
    status_code: StatusCode,
    headers: HashMap<String, String>,
    body: Option<String>,
}

impl Response {
    /// Create a new response builder
    pub fn new() -> Self {
        Self {
            status_code: StatusCode::OK,
            headers: HashMap::new(),
            body: None,
        }
    }

    /// Set the status code
    pub fn status(mut self, status: StatusCode) -> Self {
        self.status_code = status;
        self
    }

    /// Add a header
    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.headers.insert(key.into(), value.into());
        self
    }

    /// Set content type
    pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
        self.headers.insert("Content-Type".to_string(), content_type.into());
        self
    }

    /// Set plain text body
    pub fn body<T: AsRef<str>>(mut self, body: T) -> Self {
        self.body = Some(body.as_ref().to_string());
        self
    }

    /// Set JSON body
    pub fn json<T: Serialize>(mut self, body: &T) -> Result<Self, serde_json::Error> {
        match serde_json::to_string(body) {
            Ok(json) => {
                self.body = Some(json);
                if !self.headers.contains_key("Content-Type") {
                    self.headers.insert("Content-Type".to_string(), "application/json".to_string());
                }
                Ok(self)
            }
            Err(e) => Err(e),
        }
    }
}

impl Default for Response {
    fn default() -> Self {
        Self::new()
    }
}

impl Responder for Response {
    type Body = actix_web::body::BoxBody;

    fn respond_to(self, _: &actix_web::HttpRequest) -> HttpResponse<Self::Body> {
        let mut response = HttpResponse::build(self.status_code);

        for (key, value) in &self.headers {
            if let Ok(header_name) = header::HeaderName::from_bytes(key.as_bytes()) {
                response.insert_header((header_name, value.as_str()));
            }
        }

        if let Some(body) = self.body {
            response.body(body)
        } else {
            response.finish()
        }
    }
}