perma-core 0.1.0

Shared types and parsers for permalink service
Documentation
//! Definitions for a Netlify-style `_redirects` file.

use core::error::Error;
use core::fmt::{self, Display};
use core::str::FromStr;

use http::status::InvalidStatusCode;
pub use http::StatusCode;

use serde::{Deserialize, Serialize};

use crate::uri::{RelativeRef, Uri, UriParseError};

#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(untagged)]
pub enum RedirectTo {
    Uri(Uri),
    Path(RelativeRef),
}

impl RedirectTo {
    pub fn is_uri(&self) -> bool {
        matches!(self, Self::Uri(..))
    }

    pub fn is_path(&self) -> bool {
        matches!(self, Self::Path(..))
    }

    pub fn path(&self) -> &str {
        match self {
            Self::Uri(uri) => uri.path(),
            Self::Path(fake) => fake.path(),
        }
    }

    pub fn resolve(&self, base: Uri) -> Result<Uri, UriParseError> {
        match self {
            Self::Uri(uri) => Ok(uri.clone()),
            Self::Path(fake) => base.join(fake.path()),
        }
    }
}

impl From<Uri> for RedirectTo {
    fn from(uri: Uri) -> Self {
        Self::Uri(uri)
    }
}

impl From<RelativeRef> for RedirectTo {
    fn from(relative_ref: RelativeRef) -> Self {
        Self::Path(relative_ref)
    }
}

impl FromStr for RedirectTo {
    type Err = UriParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.starts_with('/') {
            RelativeRef::parse(s).map(Self::from)
        } else {
            Uri::parse(s).map(Self::from)
        }
    }
}

impl Display for RedirectTo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Uri(uri) => uri.fmt(f),
            Self::Path(path) => path.fmt(f),
        }
    }
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Redirect {
    from: RelativeRef,
    to: RedirectTo,
    #[serde(with = "crate::http_serde::status_code")]
    code: StatusCode,
}

impl Redirect {
    pub fn from(&self) -> &RelativeRef {
        &self.from
    }

    pub fn to(&self) -> &RedirectTo {
        &self.to
    }

    pub fn code(&self) -> StatusCode {
        self.code
    }
}

#[derive(Debug)]
pub enum RedirectParseError {
    MissingFrom,
    InvalidFrom(UriParseError),
    MissingTo,
    InvalidTo(UriParseError),
    MissingCode,
    InvalidCode(InvalidStatusCode),
}

impl Display for RedirectParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MissingFrom => write!(f, "missing redirect from"),
            Self::InvalidFrom(err) => write!(f, "invalid redirect from: {}", err),
            Self::MissingTo => write!(f, "missing redirect to"),
            Self::InvalidTo(err) => write!(f, "invalid redirect to: {}", err),
            Self::MissingCode => write!(f, "missing status code"),
            Self::InvalidCode(err) => write!(f, "invalid status code: {}", err),
        }
    }
}

impl Error for RedirectParseError {}

impl FromStr for Redirect {
    type Err = RedirectParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut iter = s.split_whitespace();
        let from = iter.next().ok_or(RedirectParseError::MissingFrom)?;
        let from = RelativeRef::parse(from).map_err(RedirectParseError::InvalidFrom)?;
        let to = iter
            .next()
            .ok_or(RedirectParseError::MissingTo)?
            .parse::<RedirectTo>()
            .map_err(RedirectParseError::InvalidTo)?;
        let code = iter
            .next()
            .ok_or(RedirectParseError::MissingCode)?
            .parse::<StatusCode>()
            .map_err(RedirectParseError::InvalidCode)?;
        Ok(Self { from, to, code })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn simple_redirects() {
        const REDIRECTS: &'static str = r#"
/abc /def 200
/:name /def/:name 400
/abcd  https://www.example.com/ 302
"#;
        let redirects = REDIRECTS
            .lines()
            .map(|l| l.trim())
            .filter(|l| !l.is_empty() && !l.starts_with('#'))
            .map(|l| l.parse::<Redirect>())
            .collect::<Result<Vec<_>, _>>()
            .unwrap();

        assert_eq!(
            redirects
                .iter()
                .map(|r| (r.from().to_string(), r.to().to_string(), r.code().as_u16()))
                .collect::<Vec<_>>(),
            [
                ("/abc", "/def", 200u16),
                ("/:name", "/def/:name", 400u16),
                ("/abcd", "https://www.example.com/", 302u16),
            ]
            .map(|(from, to, code)| (from.to_string(), to.to_string(), code)),
        );
    }
}