saphir 3.1.0

Fully async-await http server framework
Documentation
use crate::{body::TransmuteBody, http_context::HttpContext, responder::Responder, response::Builder as ResponseBuilder};
use http::{header::HeaderName, HeaderMap, HeaderValue, StatusCode, Uri};
use hyper::body::Body as RawBody;
use mime::Mime;
use saphir_cookie::{Cookie, CookieJar};
use serde::Serialize;
use std::{collections::HashMap, convert::TryInto, fmt::Debug};

#[derive(Debug)]
pub enum BuilderError {
    InvalidStatus,
    InvalidLocation,
    UnexpectedLocation,
    MissingLocation,
    InvalidQuery,
    UnexpectedQuery,
    InvalidFragment,
    UnexpectedFragment,
    InvalidFormData,
    UnexpectedFormData,
    UnexpectedContent,
    MissingContent,
    UnexpectedContentType,
    HeaderError(Box<http::Error>),
}

impl From<http::Error> for BuilderError {
    fn from(e: http::Error) -> Self {
        BuilderError::HeaderError(Box::new(e))
    }
}

#[derive(Default)]
pub struct Builder {
    status: StatusCode,
    location: Option<String>,
    query: Option<Result<String, BuilderError>>,
    fragment: Option<Result<String, BuilderError>>,
    form_data: Option<Result<HashMap<String, String>, BuilderError>>,
    content: Option<Box<dyn TransmuteBody + Send + Sync>>,
    content_type: Option<Mime>,
    extra_headers: HeaderMap<HeaderValue>,
    extra_headers_errors: Vec<http::Error>,
    cookies: Option<CookieJar>,
}

impl Builder {
    #[inline]
    pub fn location(mut self, location: &str) -> Self {
        self.location = Some(location.to_string());
        self
    }

    #[inline]
    pub fn query_param<T: Serialize>(mut self, query_param: &T) -> Self {
        self.query = Some(serde_urlencoded::to_string(query_param).map_err(|_e| BuilderError::InvalidQuery));
        self
    }

    #[inline]
    pub fn query_string(mut self, query_string: &str) -> Self {
        self.query = Some(Ok(query_string.to_string()));
        self
    }

    #[inline]
    pub fn fragment_param<T: Serialize>(mut self, fragment_param: &T) -> Self {
        self.fragment = Some(serde_urlencoded::to_string(fragment_param).map_err(|_e| BuilderError::InvalidQuery));
        self
    }

    #[inline]
    pub fn fragment_string(mut self, fragment_string: &str) -> Self {
        self.fragment = Some(Ok(fragment_string.to_string()));
        self
    }

    #[inline]
    pub fn choices<B: 'static + Into<RawBody> + Send + Sync>(mut self, content: B) -> Self {
        self.content = Some(Box::new(Some(content)));
        self
    }

    #[inline]
    pub fn header<E, K, V>(mut self, name: K, value: V) -> Self
    where
        E: Into<http::Error>,
        K: TryInto<HeaderName, Error = E>,
        V: TryInto<HeaderValue, Error = E>,
    {
        let name = match name.try_into() {
            Ok(name) => Some(name),
            Err(e) => {
                self.extra_headers_errors.push(e.into());
                None
            }
        };
        let value = match value.try_into() {
            Ok(value) => Some(value),
            Err(e) => {
                self.extra_headers_errors.push(e.into());
                None
            }
        };
        if let (Some(name), Some(value)) = (name, value) {
            self.extra_headers.insert(name, value);
        }
        self
    }

    #[inline]
    pub fn cookie(mut self, cookie: Cookie<'static>) -> Builder {
        self.cookies_mut().add(cookie);
        self
    }

    #[inline]
    pub fn cookies(mut self, cookies: CookieJar) -> Builder {
        self.cookies = Some(cookies);
        self
    }

    #[inline]
    pub fn cookies_mut(&mut self) -> &mut CookieJar {
        if self.cookies.is_none() {
            self.cookies = Some(CookieJar::new());
        }

        self.cookies.as_mut().expect("Checked above")
    }

    pub fn build(mut self) -> Result<Redirect, BuilderError> {
        match self.status {
            StatusCode::MOVED_PERMANENTLY | StatusCode::PERMANENT_REDIRECT | StatusCode::FOUND | StatusCode::SEE_OTHER | StatusCode::TEMPORARY_REDIRECT => {
                self.assert_location(true)?;
                self.assert_content(false)?;
                self.assert_form_data()?;
            }
            StatusCode::NOT_MODIFIED => {
                self.assert_location(false)?;
                self.assert_query()?;
                self.assert_fragment()?;
                self.assert_content(false)?;
                self.assert_form_data()?;
            }
            StatusCode::MULTIPLE_CHOICES => {
                self.assert_location(false)?;
                self.assert_query()?;
                self.assert_fragment()?;
                self.assert_content(true)?;
                self.assert_form_data()?;
            }
            #[cfg(feature = "post-redirect")]
            StatusCode::OK => {
                self.assert_location(true)?;
            }
            _ => return Err(BuilderError::InvalidStatus),
        }

        let mut location = self.format_location()?;

        #[cfg(feature = "post-redirect")]
        {
            if let StatusCode::OK = self.status {
                self.format_form_data(location.take().as_deref().unwrap_or("/"))?;
            }
        }

        let Builder {
            status,
            content,
            content_type,
            extra_headers,
            cookies,
            ..
        } = self;

        Ok(Redirect {
            status,
            location,
            content,
            content_type,
            extra_headers,
            cookies,
        })
    }

    fn format_location(&mut self) -> Result<Option<String>, BuilderError> {
        let mut url = match self.location.take() {
            Some(url) => url.parse::<Uri>().map_err(|_| BuilderError::InvalidLocation)?.to_string(),
            None => return Ok(None),
        };

        if let Some(query) = self.query.take().transpose()? {
            url.push('?');
            url.push_str(query.as_str());
        }

        if let Some(fragment) = self.fragment.take().transpose()? {
            url.push('#');
            url.push_str(fragment.as_str());
        }

        Ok(Some(url))
    }

    #[inline]
    fn assert_form_data(&self) -> Result<(), BuilderError> {
        if self.form_data.is_some() {
            Err(BuilderError::UnexpectedFormData)
        } else {
            Ok(())
        }
    }

    #[inline]
    fn assert_location(&self, needed: bool) -> Result<(), BuilderError> {
        if self.location.is_some() && !needed {
            Err(BuilderError::UnexpectedLocation)
        } else if self.location.is_none() && needed {
            Err(BuilderError::MissingLocation)
        } else {
            Ok(())
        }
    }

    #[inline]
    fn assert_query(&self) -> Result<(), BuilderError> {
        if self.query.is_some() {
            Err(BuilderError::UnexpectedQuery)
        } else {
            Ok(())
        }
    }

    #[inline]
    fn assert_fragment(&self) -> Result<(), BuilderError> {
        if self.fragment.is_some() {
            Err(BuilderError::UnexpectedFragment)
        } else {
            Ok(())
        }
    }

    #[inline]
    fn assert_content(&self, needed: bool) -> Result<(), BuilderError> {
        if self.content.is_some() && !needed {
            Err(BuilderError::UnexpectedContent)
        } else if self.content_type.is_some() && !needed {
            Err(BuilderError::UnexpectedContentType)
        } else if self.content.is_none() && needed {
            Err(BuilderError::MissingContent)
        } else {
            Ok(())
        }
    }
}

pub struct Redirect {
    status: StatusCode,
    location: Option<String>,
    content: Option<Box<dyn TransmuteBody + Send + Sync>>,
    content_type: Option<Mime>,
    extra_headers: HeaderMap<HeaderValue>,
    cookies: Option<CookieJar>,
}

impl Redirect {
    #[inline]
    pub fn status(&self) -> &StatusCode {
        &self.status
    }

    #[inline]
    pub fn location(&self) -> Option<&str> {
        self.location.as_deref()
    }

    #[inline]
    pub fn content_type(&self) -> Option<&Mime> {
        self.content_type.as_ref()
    }

    #[inline]
    pub fn cookies(&self) -> Option<&CookieJar> {
        self.cookies.as_ref()
    }

    #[inline]
    pub fn cookies_mut(&mut self) -> Option<&mut CookieJar> {
        self.cookies.as_mut()
    }

    #[inline]
    pub fn moved_permanently() -> Builder {
        Builder {
            status: StatusCode::MOVED_PERMANENTLY,
            ..Default::default()
        }
    }

    #[inline]
    pub fn permanent_redirect() -> Builder {
        Builder {
            status: StatusCode::PERMANENT_REDIRECT,
            ..Default::default()
        }
    }

    #[inline]
    pub fn found() -> Builder {
        Builder {
            status: StatusCode::FOUND,
            ..Default::default()
        }
    }

    #[inline]
    pub fn see_other() -> Builder {
        Builder {
            status: StatusCode::SEE_OTHER,
            ..Default::default()
        }
    }

    #[inline]
    pub fn temporary_redirect() -> Builder {
        Builder {
            status: StatusCode::TEMPORARY_REDIRECT,
            ..Default::default()
        }
    }

    #[inline]
    pub fn not_modified() -> Builder {
        Builder {
            status: StatusCode::NOT_MODIFIED,
            ..Default::default()
        }
    }

    #[inline]
    pub fn multiple_choice() -> Builder {
        Builder {
            status: StatusCode::MULTIPLE_CHOICES,
            content_type: Some(mime::TEXT_HTML),
            ..Default::default()
        }
    }
}

impl Responder for Redirect {
    fn respond_with_builder(self, mut builder: ResponseBuilder, _ctx: &HttpContext) -> ResponseBuilder {
        builder = builder.status(self.status);

        if let Some(headers) = builder.headers_mut() {
            headers.extend(self.extra_headers);
        }

        if let Some(location) = self.location {
            builder = builder.header("Location", location.as_str())
        }

        if let Some(mut c) = self.content {
            builder = builder.body(c.transmute())
        }

        if let Some(ct) = self.content_type {
            builder = builder.header("Content-Type", ct.to_string())
        }

        if let Some(cookies) = self.cookies {
            builder = builder.cookies(cookies)
        }

        builder
    }
}

#[cfg(feature = "post-redirect")]
#[cfg_attr(docsrs, doc(cfg(feature = "post-redirect")))]
mod post_redirect {
    use super::*;

    fn format_input(name: &str, value: &str) -> String {
        format!("<input type=\"hidden\" name=\"{name}\" value=\"{value}\"/>\n", name = name, value = value)
    }

    fn format_form(loc: &str, inputs: &str) -> String {
        format!(
            "<body onload=\"javascript:document.forms[0].submit()\">\n\
    <form method=\"post\" action=\"{location}\">\n\
        {inputs}\n\
    </form>\n\
</body>",
            location = loc,
            inputs = inputs
        )
    }

    impl Builder {
        #[inline]
        pub fn form_data<T: Serialize>(mut self, data: &T) -> Self {
            self.form_data = Some(
                serde_json::to_value(data)
                    .and_then(serde_json::from_value::<HashMap<String, String>>)
                    .map_err(|_e| BuilderError::InvalidFormData),
            );
            self
        }

        #[doc(hidden)]
        pub fn format_form_data(&mut self, loc: &str) -> Result<(), BuilderError> {
            let mut inputs = String::new();

            if let Some(data) = self.form_data.take().transpose()? {
                for (n, v) in data.into_iter() {
                    inputs.push_str(format_input(&n, &v).as_str())
                }
            }
            self.content = Some(Box::new(Some(format_form(loc, &inputs))));
            Ok(())
        }
    }

    impl Redirect {
        #[inline]
        pub fn post_redirect() -> Builder {
            Builder {
                status: StatusCode::OK,
                content_type: Some(mime::TEXT_HTML_UTF_8),
                ..Default::default()
            }
        }
    }
}