coil-runtime 0.1.0

HTTP runtime and request handling for the Coil framework.
Documentation
use std::collections::BTreeMap;

use axum::body::Body;
use axum::http::{HeaderName, HeaderValue, StatusCode};
use axum::response::Response;

use crate::FileDeliveryMode;
use crate::live::LiveHtmlResponseGraph;

use super::LiveResponseAnnotations;
use super::headers::{body_response, file_delivery_mode_name, render_json_object};

#[derive(Debug, Clone)]
pub(crate) struct LiveResponseGraph {
    status: StatusCode,
    body: LiveResponseBodyGraph,
    annotations: LiveResponseAnnotations,
    cookies: Vec<String>,
}

#[derive(Debug, Clone)]
enum LiveResponseBodyGraph {
    Html(LiveHtmlResponseGraph),
    Json(BTreeMap<String, String>),
    Redirect {
        location: String,
    },
    File {
        logical_path: String,
        content_type: String,
        delivery_mode: FileDeliveryMode,
    },
}

#[derive(Debug, Clone)]
pub(crate) struct LiveResponseComposition {
    graph: LiveResponseGraph,
}

impl LiveResponseGraph {
    fn new(status: StatusCode, body: LiveResponseBodyGraph) -> Self {
        Self {
            status,
            body,
            annotations: LiveResponseAnnotations::default(),
            cookies: Vec::new(),
        }
    }

    fn into_response(self) -> Response<Body> {
        let mut response = match self.body {
            LiveResponseBodyGraph::Html(body) => {
                body_response(self.status, body.render(), Some("text/html; charset=utf-8"))
            }
            LiveResponseBodyGraph::Json(payload) => {
                let body = render_json_object(payload);
                body_response(self.status, body, Some("application/json"))
            }
            LiveResponseBodyGraph::Redirect { location } => {
                let mut response = Response::new(Body::empty());
                *response.status_mut() = self.status;
                response.headers_mut().insert(
                    HeaderName::from_static("location"),
                    HeaderValue::from_str(&location)
                        .expect("redirect location is a valid header value"),
                );
                response
            }
            LiveResponseBodyGraph::File {
                logical_path,
                content_type,
                delivery_mode,
            } => {
                let mut response = Response::new(Body::empty());
                *response.status_mut() = self.status;
                response.headers_mut().insert(
                    HeaderName::from_static("content-type"),
                    HeaderValue::from_str(&content_type)
                        .expect("file content type is a valid header value"),
                );
                response.headers_mut().insert(
                    HeaderName::from_static("x-coil-file-path"),
                    HeaderValue::from_str(&logical_path)
                        .expect("file logical path is a valid header value"),
                );
                response.headers_mut().insert(
                    HeaderName::from_static("x-coil-file-delivery"),
                    HeaderValue::from_static(file_delivery_mode_name(delivery_mode)),
                );
                response
            }
        };

        for header in self.annotations.rendered_headers() {
            response.headers_mut().insert(header.name, header.value);
        }
        for cookie in self.cookies {
            if let Ok(value) = HeaderValue::from_str(&cookie) {
                response
                    .headers_mut()
                    .append(HeaderName::from_static("set-cookie"), value);
            }
        }

        response
    }
}

impl LiveResponseComposition {
    pub(crate) fn html(status: StatusCode, body: LiveHtmlResponseGraph) -> Self {
        Self {
            graph: LiveResponseGraph::new(status, LiveResponseBodyGraph::Html(body)),
        }
    }

    pub(crate) fn json(status: StatusCode, body: BTreeMap<String, String>) -> Self {
        Self {
            graph: LiveResponseGraph::new(status, LiveResponseBodyGraph::Json(body)),
        }
    }

    pub(crate) fn redirect(status: StatusCode, location: impl Into<String>) -> Self {
        Self {
            graph: LiveResponseGraph::new(
                status,
                LiveResponseBodyGraph::Redirect {
                    location: location.into(),
                },
            ),
        }
    }

    pub(crate) fn file(
        status: StatusCode,
        logical_path: impl Into<String>,
        content_type: impl Into<String>,
        delivery_mode: FileDeliveryMode,
    ) -> Self {
        Self {
            graph: LiveResponseGraph::new(
                status,
                LiveResponseBodyGraph::File {
                    logical_path: logical_path.into(),
                    content_type: content_type.into(),
                    delivery_mode,
                },
            ),
        }
    }

    pub(crate) fn with_annotation(mut self, annotations: LiveResponseAnnotations) -> Self {
        self.graph.annotations = annotations;
        self
    }

    pub(crate) fn with_cookie(mut self, value: impl Into<String>) -> Self {
        self.graph.cookies.push(value.into());
        self
    }

    pub(crate) fn into_response(self) -> Response<Body> {
        self.graph.into_response()
    }
}