safe_uri 0.1.0-beta.4

Simple and safe URI types.
Documentation
mod generated_struct;

use shared_bytes::SharedStr;

pub use self::generated_struct::Scheme;

use crate::validation::InvalidByte;
use std::{error::Error, fmt};

impl Scheme {
    /// The HTTP URI scheme.
    pub const HTTP: Self = Self::from_static("http");

    /// The secure HTTP URI scheme.
    pub const HTTPS: Self = Self::new();

    /// The WebSocket URI scheme.
    pub const WS: Self = Self::from_static("ws");

    /// The secure WebSocket URI scheme.
    pub const WSS: Self = Self::from_static("wss");
}

#[derive(Debug, Clone)]
pub struct InvalidScheme(Invalid);

impl InvalidScheme {
    pub fn empty() -> Self {
        Self(Invalid::Empty)
    }
}

#[derive(Debug, Clone)]
enum Invalid {
    Byte(InvalidByte),
    Empty,
}

impl fmt::Display for InvalidScheme {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.0 {
            Invalid::Byte(e) => write!(f, "URI scheme is invalid: {}", e),
            Invalid::Empty => write!(f, "URI scheme is empty"),
        }
    }
}

impl Error for InvalidScheme {}

const fn validate_static(bytes: &[u8]) -> Result<(), InvalidScheme> {
    validate(bytes)
}

fn validate_with_normalized_percent_encoding(
    string: &str,
) -> Result<Option<SharedStr>, InvalidScheme> {
    validate(string.as_bytes()).map(|()| None)
}

const fn validate(bytes: &[u8]) -> Result<(), InvalidScheme> {
    match inner_validate(bytes) {
        Ok(x) => Ok(x),
        Err(e) => Err(InvalidScheme(e)),
    }
}

const fn inner_validate(bytes: &[u8]) -> Result<(), Invalid> {
    if bytes.is_empty() {
        return Err(Invalid::Empty);
    }
    let first_byte = bytes[0];
    if !first_byte.is_ascii_alphabetic() {
        return Err(Invalid::Byte(InvalidByte { byte: first_byte }));
    }
    let mut index = 1;
    while index < bytes.len() {
        let byte = bytes[index];
        if !is_scheme_char(byte) {
            return Err(Invalid::Byte(InvalidByte { byte }));
        }
        index += 1;
    }
    Ok(())
}

const fn is_scheme_char(b: u8) -> bool {
    b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b'.')
}

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

    #[test]
    fn one_ok_char() {
        let string = "a";
        assert_eq!(Scheme::try_from(string).unwrap(), string);
    }

    #[test]
    fn one_digit() {
        let string = "1";
        assert_matches!(
            Scheme::try_from(string),
            Err(InvalidScheme(Invalid::Byte(InvalidByte { byte: b'1' })))
        )
    }

    #[test]
    fn empty() {
        let string = "";
        assert_matches!(Scheme::try_from(string), Err(InvalidScheme(Invalid::Empty)))
    }

    #[test]
    fn default() {
        assert_matches!(Scheme::try_from(Scheme::default().into_shared_str()), Ok(_));
        assert_eq!(Scheme::default().as_str(), "https");
    }

    #[test]
    fn https_is_https() {
        assert_eq!(Scheme::HTTPS, "https");
    }
}