Skip to main content

ferrin_spec/shared/
media_type.rs

1//! Media type wrapper.
2
3use serde::Deserialize;
4use serde::Serialize;
5
6/// An IANA media type such as `image/png`, or a top-level type such as `image`.
7///
8/// The wrapper does not validate syntax; it provides normalization helpers
9/// used when matching provider capabilities. Comparison is case-sensitive on
10/// the stored string; callers should normalize before comparing.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct MediaType(String);
14
15impl MediaType {
16    /// Creates a media type from any string-like value.
17    #[must_use]
18    pub fn new(value: impl Into<String>) -> Self {
19        Self(value.into())
20    }
21
22    /// Returns the media type as a string slice.
23    #[must_use]
24    pub fn as_str(&self) -> &str {
25        &self.0
26    }
27
28    /// Consumes the media type and returns the inner `String`.
29    #[must_use]
30    pub fn into_string(self) -> String {
31        self.0
32    }
33
34    /// Returns `true` when the value is a full `type/subtype` media type.
35    ///
36    /// A wildcard subtype (`image/*`) is not considered full.
37    #[must_use]
38    pub fn is_full(&self) -> bool {
39        match self.0.split_once('/') {
40            Some((top, sub)) => !top.is_empty() && !sub.is_empty() && sub != "*",
41            None => false,
42        }
43    }
44
45    /// Returns the top-level type (`image` for `image/png`), lowercased.
46    #[must_use]
47    pub fn top_level(&self) -> String {
48        let top = self
49            .0
50            .split_once('/')
51            .map_or(self.0.as_str(), |(top, _)| top);
52        top.trim().to_ascii_lowercase()
53    }
54
55    /// Returns the subtype (`png` for `image/png`) without parameters, if any.
56    #[must_use]
57    pub fn subtype(&self) -> Option<&str> {
58        let (_, sub) = self.0.split_once('/')?;
59        let sub = sub.split(';').next().unwrap_or(sub).trim();
60        (!sub.is_empty()).then_some(sub)
61    }
62
63    /// Normalizes the media type for capability matching.
64    ///
65    /// Lowercases the value, drops parameters (`; charset=utf-8`) and turns a
66    /// wildcard subtype (`image/*`) into its top-level type (`image`).
67    #[must_use]
68    pub fn normalize(&self) -> MediaType {
69        let without_params = self.0.split(';').next().unwrap_or(&self.0).trim();
70        let lower = without_params.to_ascii_lowercase();
71        match lower.split_once('/') {
72            Some((top, "*")) => MediaType(top.to_owned()),
73            Some((top, "")) => MediaType(top.to_owned()),
74            _ => MediaType(lower),
75        }
76    }
77
78    /// Returns `true` when this media type matches `pattern`.
79    ///
80    /// `pattern` may be a full type, a wildcard (`image/*`) or a top-level
81    /// type (`image`). Matching is case-insensitive and ignores parameters.
82    #[must_use]
83    pub fn matches(&self, pattern: &MediaType) -> bool {
84        let this = self.normalize();
85        let pattern = pattern.normalize();
86        if this == pattern {
87            return true;
88        }
89        if pattern.0.contains('/') {
90            return false;
91        }
92        this.top_level() == pattern.0
93    }
94}
95
96impl std::fmt::Display for MediaType {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.write_str(&self.0)
99    }
100}
101
102impl From<String> for MediaType {
103    fn from(value: String) -> Self {
104        Self(value)
105    }
106}
107
108impl From<&str> for MediaType {
109    fn from(value: &str) -> Self {
110        Self(value.to_owned())
111    }
112}
113
114impl From<MediaType> for String {
115    fn from(value: MediaType) -> Self {
116        value.0
117    }
118}
119
120impl AsRef<str> for MediaType {
121    fn as_ref(&self) -> &str {
122        &self.0
123    }
124}
125
126impl PartialEq<str> for MediaType {
127    fn eq(&self, other: &str) -> bool {
128        self.0 == other
129    }
130}
131
132impl PartialEq<&str> for MediaType {
133    fn eq(&self, other: &&str) -> bool {
134        self.0 == *other
135    }
136}