indieweb 0.10.0

A collection of utilities for working with the IndieWeb.
Documentation
use std::str::FromStr;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MimeType(pub mime::Mime);

impl std::hash::Hash for MimeType {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.to_string().hash(state);
    }
}

impl MimeType {
    pub fn type_(&self) -> &str {
        self.0.type_().as_str()
    }

    pub fn subtype(&self) -> &str {
        self.0.subtype().as_str()
    }

    pub fn matches(&self, pattern: &str) -> bool {
        let pattern = pattern.trim().to_lowercase();

        if pattern == "*/*" || pattern == "*" {
            return true;
        }

        if self.type_() == "*" {
            return true;
        }

        if let Some(slash_pos) = pattern.find('/') {
            let pattern_type = &pattern[..slash_pos];
            let pattern_subtype = &pattern[slash_pos + 1..];

            if pattern_subtype == "*" {
                return self.type_().to_lowercase() == pattern_type;
            }

            return self.type_().to_lowercase() == pattern_type
                && self.subtype().to_lowercase() == pattern_subtype;
        }

        self.type_().to_lowercase() == pattern
    }

    pub fn matches_self(&self, other: &MimeType) -> bool {
        self.0.type_() == other.0.type_() && self.0.subtype() == other.0.subtype()
    }
}

impl std::fmt::Display for MimeType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}/{}", self.type_(), self.subtype())
    }
}

impl FromStr for MimeType {
    type Err = crate::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = s.trim();

        if s == "*" || s == "*/*" {
            return Ok(MimeType(mime::Mime::from_str("*/*").unwrap()));
        }

        if !s.contains('/') {
            let type_only = format!("{}/*", s);
            return mime::Mime::from_str(&type_only)
                .map(MimeType)
                .map_err(|_| crate::Error::InvalidMimeType(s.to_string()));
        }

        mime::Mime::from_str(s)
            .map(MimeType)
            .map_err(|_| crate::Error::InvalidMimeType(s.to_string()))
    }
}

impl serde::Serialize for MimeType {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        self.to_string().serialize(serializer)
    }
}

impl<'de> serde::Deserialize<'de> for MimeType {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        String::deserialize(deserializer).and_then(|s| s.parse().map_err(serde::de::Error::custom))
    }
}

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

    #[test]
    fn mime_type_parsing() {
        let mt: MimeType = "image/jpeg".parse().unwrap();
        assert_eq!(mt.type_(), "image");
        assert_eq!(mt.subtype(), "jpeg");
    }

    #[test]
    fn mime_type_type_only() {
        let mt: MimeType = "image".parse().unwrap();
        assert_eq!(mt.type_(), "image");
    }

    #[test]
    fn mime_type_wildcard_global() {
        let mt: MimeType = "*/*".parse().unwrap();
        assert!(mt.matches("image/jpeg"));
        assert!(mt.matches("*/*"));
    }

    #[test]
    fn mime_type_matches_exact() {
        let mt: MimeType = "image/jpeg".parse().unwrap();
        assert!(mt.matches("image/jpeg"));
        assert!(!mt.matches("image/png"));
        assert!(!mt.matches("video/mp4"));
    }

    #[test]
    fn mime_type_matches_type_wildcard() {
        let jpeg: MimeType = "image/jpeg".parse().unwrap();
        let png: MimeType = "image/png".parse().unwrap();

        assert!(jpeg.matches("image"));
        assert!(png.matches("image"));
        assert!(!jpeg.matches("video"));

        assert!(jpeg.matches("image/*"));
        assert!(png.matches("image/*"));
    }

    #[test]
    fn mime_type_matches_global_wildcard() {
        let mt: MimeType = "image/jpeg".parse().unwrap();
        assert!(mt.matches("*/*"));
        assert!(mt.matches("*"));
    }

    #[test]
    fn mime_type_invalid() {
        // The mime crate is quite permissive - "invalid-mime" is parsed as type="invalid-mime" with subtype=""
        // This test checks that completely malformed strings fail
        assert!("".parse::<MimeType>().is_err());
    }

    #[test]
    fn mime_type_serialization() {
        let mt: MimeType = "image/jpeg".parse().unwrap();
        assert_eq!(serde_json::to_string(&mt).unwrap(), "\"image/jpeg\"");
    }

    #[test]
    fn mime_type_deserialization() {
        let mt: MimeType = serde_json::from_str("\"image/jpeg\"").unwrap();
        assert_eq!(mt.type_(), "image");
        assert_eq!(mt.subtype(), "jpeg");
    }
}