unpoly 0.2.1

Helper for the Unpoly API, initially for usage in Axum handlers
Documentation
use crate::headers;
use crate::LayerMode;
use crate::Unpoly;

use axum::{
    extract::FromRequestParts,
    http::{request::Parts, StatusCode},
};

impl<S> FromRequestParts<S> for Unpoly
where
    S: Send + Sync,
{
    type Rejection = (StatusCode, &'static str);

    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
        let request_version = parts
            .headers
            .get(headers::VERSION)
            .map(|v| v.to_str().map_or(None, |v| Some(v.to_string())))
            .unwrap_or(None);

        let request_context: Option<serde_json::Value> = parts
            .headers
            .get(headers::CONTEXT)
            .map(|v| v.to_str().ok())
            .unwrap_or(None)
            .map(|v| serde_json::from_str(v).unwrap_or_default());

        let request_fail_context: Option<serde_json::Value> = parts
            .headers
            .get(headers::FAIL_CONTEXT)
            .map(|v| v.to_str().ok())
            .unwrap_or(None)
            .map(|v| serde_json::from_str(v).unwrap_or_default());

        let request_mode = parts
            .headers
            .get(headers::MODE)
            .map(|v| {
                v.to_str().map_or(LayerMode::ROOT, |v| {
                    serde_json::from_str(&("\"".to_string() + v + "\"")).unwrap_or_default()
                })
            })
            .unwrap_or(LayerMode::ROOT);

        let request_fail_mode = parts
            .headers
            .get(headers::FAIL_MODE)
            .map(|v| {
                v.to_str().map_or(LayerMode::ROOT, |v| {
                    serde_json::from_str(&("\"".to_string() + v + "\"")).unwrap_or_default()
                })
            })
            .unwrap_or(LayerMode::ROOT);

        let request_target = parts
            .headers
            .get(headers::TARGET)
            .map(|v| v.to_str().map_or(None, |v| Some(v.to_string())))
            .unwrap_or(None);

        let request_fail_target = parts
            .headers
            .get(headers::FAIL_TARGET)
            .map(|v| v.to_str().map_or(None, |v| Some(v.to_string())))
            .unwrap_or(None);

        let request_validate = parts.headers.get(headers::VALIDATE).map_or(vec![], |v| {
            v.to_str()
                .unwrap_or("")
                .split_whitespace()
                .map(|v| v.trim().to_string())
                .collect()
        });

        Ok(Unpoly {
            request_version,
            request_context,
            request_fail_context,
            request_fail_mode,
            request_mode,
            request_target,
            request_fail_target,
            request_validate,
            ..Default::default()
        })
    }
}

#[cfg(test)]
mod tests {
    extern crate axum;
    use axum::{body::Body, http::Request};

    use super::*;

    #[tokio::test]
    async fn test_no_unpoly_request() {
        let request = Request::builder()
            .method("GET")
            .uri("https://www.unpoly.com/")
            .header("X-Custom-Foo", "Bar")
            .body(Body::empty())
            .unwrap();
        let mut parts = request.into_parts();

        let mut unpoly = Unpoly::from_request_parts(&mut parts.0, &()).await.unwrap();

        assert_eq!(unpoly.request_version, None);
        assert_eq!(unpoly.request_context, None);
        assert_eq!(unpoly.request_fail_context, None);
        assert_eq!(unpoly.request_fail_mode, LayerMode::ROOT);
        assert_eq!(unpoly.request_mode, LayerMode::ROOT);
        assert_eq!(unpoly.request_target, None);

        assert!(!unpoly.is_up());

        assert!(!unpoly.get_headers().unwrap().contains_key("Vary"));
    }

    #[tokio::test]
    async fn test_unpoly_default() {
        let request = Request::builder()
            .method("GET")
            .uri("https://www.unpoly.com/")
            .header("X-Up-Version", "1.0.0")
            .body(Body::empty())
            .unwrap();
        let mut parts = request.into_parts();

        let mut unpoly = Unpoly::from_request_parts(&mut parts.0, &()).await.unwrap();

        assert_eq!(unpoly.request_version, Some("1.0.0".to_string()));
        assert_eq!(unpoly.request_context, None);
        assert_eq!(unpoly.request_fail_context, None);
        assert_eq!(unpoly.request_fail_mode, LayerMode::ROOT);
        assert_eq!(unpoly.request_mode, LayerMode::ROOT);
        assert_eq!(unpoly.request_target, None);

        unpoly.is_up();
        unpoly.set_success(true);
        unpoly.mode();

        assert_eq!(
            unpoly.get_headers().unwrap()["Vary"],
            "X-Up-Mode,X-Up-Target,X-Up-Version".to_string()
        );

        let mut unpoly = Unpoly::from_request_parts(&mut parts.0, &()).await.unwrap();
        unpoly.is_up();
        unpoly.set_success(false);
        unpoly.mode();

        assert_eq!(
            unpoly.get_headers().unwrap()["Vary"],
            "X-Up-Fail-Mode,X-Up-Fail-Target,X-Up-Version".to_string()
        );
    }

    #[tokio::test]
    async fn test_unpoly_success() {
        let request = Request::builder()
            .method("GET")
            .uri("https://www.unpoly.com/")
            .header("X-Up-Version", "1.0.0")
            .header("X-Up-Context", "{\"lives\": 42}")
            .header("X-Up-Fail-Context", "{\"lives\": 2}")
            .header("X-Up-Target", "main")
            .header("X-Up-Fail-Target", "root")
            .header("X-Up-Mode", "root")
            .header("X-Up-Fail-Mode", "cover")
            .header("X-Up-Validate", "name")
            .body(Body::empty())
            .unwrap();
        let mut parts = request.into_parts();

        let mut unpoly = Unpoly::from_request_parts(&mut parts.0, &()).await.unwrap();
        unpoly.set_success(true);

        unpoly.is_up();
        assert_eq!(unpoly.context(), Some(&serde_json::json!({"lives": 42})));
        assert_eq!(unpoly.target(), Some("main"));
        assert_eq!(*unpoly.mode(), LayerMode::ROOT);

        assert_eq!(
            unpoly.get_headers().unwrap()["Vary"],
            "X-Up-Context,X-Up-Mode,X-Up-Target,X-Up-Version".to_string()
        );
    }

    #[tokio::test]
    async fn test_unpoly_fail() {
        let request = Request::builder()
            .method("GET")
            .uri("https://www.unpoly.com/")
            .header("X-Up-Version", "1.0.0")
            .header("X-Up-Context", "{\"lives\": 42}")
            .header("X-Up-Fail-Context", "{\"lives\": 2}")
            .header("X-Up-Target", "main")
            .header("X-Up-Fail-Target", "root")
            .header("X-Up-Mode", "root")
            .header("X-Up-Fail-Mode", "cover")
            .header("X-Up-Validate", "name")
            .body(Body::empty())
            .unwrap();
        let mut parts = request.into_parts();

        let mut unpoly = Unpoly::from_request_parts(&mut parts.0, &()).await.unwrap();
        unpoly.set_success(false);

        unpoly.is_up();
        assert_eq!(unpoly.context(), Some(&serde_json::json!({"lives": 2})));
        assert_eq!(unpoly.target(), Some("root"));
        assert_eq!(*unpoly.mode(), LayerMode::COVER);

        assert_eq!(
            unpoly.get_headers().unwrap()["Vary"],
            "X-Up-Fail-Context,X-Up-Fail-Mode,X-Up-Fail-Target,X-Up-Version".to_string()
        );
    }

    #[tokio::test]
    async fn test_unpoly_set_responses() {
        let request = Request::builder()
            .method("GET")
            .uri("https://www.unpoly.com/")
            .header("X-Up-Version", "1.0.0")
            .header("X-Up-Context", "{\"lives\": 42}")
            .header("X-Up-Fail-Context", "{\"lives\": 2}")
            .header("X-Up-Target", "main")
            .header("X-Up-Fail-Target", "root")
            .header("X-Up-Mode", "root")
            .header("X-Up-Fail-Mode", "cover")
            .header("X-Up-Validate", "name")
            .body(Body::empty())
            .unwrap();
        let mut parts = request.into_parts();

        let mut unpoly = Unpoly::from_request_parts(&mut parts.0, &()).await.unwrap();

        unpoly.set_context(serde_json::json!({"lives": 43}));

        unpoly.set_title("Hello");
        unpoly.set_location("https://unpoly.com/");
        unpoly.set_method("PUT");
        unpoly.set_target("main");
        unpoly.set_evict_cache("main".to_string());
        unpoly.set_expire_cache("main".to_string());

        assert_eq!(unpoly.get_headers().unwrap()["X-Up-Title"], "Hello");
        assert_eq!(
            unpoly.get_headers().unwrap()["X-Up-Location"],
            "https://unpoly.com/"
        );
        assert_eq!(unpoly.get_headers().unwrap()["X-Up-Method"], "PUT");
        assert_eq!(unpoly.get_headers().unwrap()["X-Up-Evict-Cache"], "main");
        assert_eq!(unpoly.get_headers().unwrap()["X-Up-Expire-Cache"], "main");
    }
}