mediatype/
media_type_buf.rs

1use super::{error::*, media_type::*, name::*, params::*, parse::*, value::*};
2use std::{
3    borrow::Cow,
4    collections::BTreeMap,
5    fmt,
6    hash::{Hash, Hasher},
7    str::FromStr,
8};
9
10/// An owned and immutable media type.
11///
12/// ```
13/// use mediatype::{names::*, values::*, MediaType, MediaTypeBuf, ReadParams};
14///
15/// let text_plain: MediaTypeBuf = "text/plain; charset=UTF-8".parse().unwrap();
16/// assert_eq!(text_plain.get_param(CHARSET).unwrap(), UTF_8);
17///
18/// let mut text_markdown: MediaType = text_plain.to_ref();
19/// text_markdown.subty = MARKDOWN;
20/// assert_eq!(text_markdown.to_string(), "text/markdown; charset=UTF-8");
21/// ```
22#[derive(Debug, Clone)]
23pub struct MediaTypeBuf {
24    data: Box<str>,
25    indices: Indices,
26}
27
28impl MediaTypeBuf {
29    /// Constructs a `MediaTypeBuf` from a top-level type and a subtype.
30    #[must_use]
31    pub fn new(ty: Name, subty: Name) -> Self {
32        Self::from_string(format!("{}/{}", ty, subty)).expect("`ty` and `subty` should be valid")
33    }
34
35    /// Constructs a `MediaTypeBuf` with an optional suffix and parameters.
36    #[must_use]
37    pub fn from_parts(
38        ty: Name,
39        subty: Name,
40        suffix: Option<Name>,
41        params: &[(Name, Value)],
42    ) -> Self {
43        use std::fmt::Write;
44        let mut s = String::new();
45        write!(s, "{}/{}", ty, subty).expect("`ty` and `subty` should be valid");
46        if let Some(suffix) = suffix {
47            write!(s, "+{}", suffix).unwrap();
48        }
49        for (name, value) in params {
50            write!(s, "; {}={}", name, value).unwrap();
51        }
52        Self::from_string(s).expect("all values should be valid")
53    }
54
55    /// Constructs a `MediaTypeBuf` from [`String`].
56    ///
57    /// Unlike [`FromStr::from_str`], this function takes the ownership of [`String`]
58    /// instead of making a new copy.
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the string fails to be parsed.
63    ///
64    /// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html
65    /// [`FromStr::from_str`]: https://doc.rust-lang.org/std/str/trait.FromStr.html
66    pub fn from_string(mut s: String) -> Result<Self, MediaTypeError> {
67        let (indices, len) = Indices::parse(&s)?;
68        s.truncate(len);
69        Ok(Self {
70            data: s.into(),
71            indices,
72        })
73    }
74
75    /// Returns the top-level type.
76    #[must_use]
77    pub fn ty(&self) -> Name {
78        Name::new_unchecked(&self.data[self.indices.ty()])
79    }
80
81    /// Returns the subtype.
82    #[must_use]
83    pub fn subty(&self) -> Name {
84        Name::new_unchecked(&self.data[self.indices.subty()])
85    }
86
87    /// Returns the suffix.
88    #[must_use]
89    pub fn suffix(&self) -> Option<Name> {
90        self.indices
91            .suffix()
92            .map(|range| Name::new_unchecked(&self.data[range]))
93    }
94
95    /// Returns a [`MediaType`] without parameters.
96    ///
97    /// ```
98    /// # use mediatype::MediaTypeBuf;
99    /// # use std::str::FromStr;
100    /// let media_type: MediaTypeBuf = "image/svg+xml; charset=UTF-8".parse().unwrap();
101    /// assert_eq!(media_type.essence().to_string(), "image/svg+xml");
102    /// ```
103    ///
104    /// [`MadiaType`]: ./struct.MediaType.html
105    #[must_use]
106    pub fn essence(&self) -> MediaType<'_> {
107        MediaType::from_parts(self.ty(), self.subty(), self.suffix(), &[])
108    }
109
110    /// Returns the underlying string.
111    #[must_use]
112    pub const fn as_str(&self) -> &str {
113        &self.data
114    }
115
116    /// Returns the canonicalized `MediaTypeBuf`.
117    ///
118    /// All strings except parameter values will be converted to lowercase.
119    ///
120    /// ```
121    /// # use mediatype::MediaTypeBuf;
122    /// let media_type: MediaTypeBuf = "IMAGE/SVG+XML;  CHARSET=UTF-8;  ".parse().unwrap();
123    /// assert_eq!(
124    ///     media_type.canonicalize().to_string(),
125    ///     "image/svg+xml; charset=UTF-8"
126    /// );
127    /// ```
128    #[must_use]
129    pub fn canonicalize(&self) -> Self {
130        use std::fmt::Write;
131        let mut s = String::with_capacity(self.data.len());
132        write!(
133            s,
134            "{}/{}",
135            self.ty().as_str().to_ascii_lowercase(),
136            self.subty().as_str().to_ascii_lowercase()
137        )
138        .unwrap();
139        if let Some(suffix) = self.suffix() {
140            write!(s, "+{}", suffix.as_str().to_ascii_lowercase())
141                .expect("`write` should not fail on a `String`");
142        }
143        for (name, value) in self.params() {
144            write!(s, "; {}={}", name.as_str().to_ascii_lowercase(), value)
145                .expect("`write` should not fail on a `String`");
146        }
147        s.shrink_to_fit();
148        Self::from_string(s).expect("all values should be valid")
149    }
150
151    /// Constructs a `MediaType` from `self`.
152    #[must_use]
153    pub fn to_ref(&self) -> MediaType {
154        let params = self.params().collect::<Vec<_>>();
155        let params = if params.is_empty() {
156            Cow::Borrowed([].as_slice())
157        } else {
158            Cow::Owned(params)
159        };
160        MediaType::from_parts_unchecked(self.ty(), self.subty(), self.suffix(), params)
161    }
162}
163
164impl ReadParams for MediaTypeBuf {
165    fn params(&self) -> Params {
166        Params::from_indices(&self.data, &self.indices)
167    }
168
169    fn get_param(&self, name: Name) -> Option<Value> {
170        self.indices
171            .params()
172            .iter()
173            .rev()
174            .find(|&&[start, end, _, _]| {
175                name == Name::new_unchecked(&self.data[start..end])
176            })
177            .map(|&[_, _, start, end]| {
178                Value::new_unchecked(&self.data[start..end])
179            })
180    }
181}
182
183impl FromStr for MediaTypeBuf {
184    type Err = MediaTypeError;
185
186    fn from_str(s: &str) -> Result<Self, Self::Err> {
187        let (indices, len) = Indices::parse(s)?;
188        Ok(Self {
189            data: s[..len].into(),
190            indices,
191        })
192    }
193}
194
195impl From<MediaType<'_>> for MediaTypeBuf {
196    fn from(t: MediaType) -> Self {
197        Self::from_string(t.to_string()).expect("`t` should be valid")
198    }
199}
200
201impl From<&MediaType<'_>> for MediaTypeBuf {
202    fn from(t: &MediaType) -> Self {
203        Self::from_string(t.to_string()).expect("`t` should be valid")
204    }
205}
206
207impl AsRef<str> for MediaTypeBuf {
208    fn as_ref(&self) -> &str {
209        &self.data
210    }
211}
212
213impl PartialEq for MediaTypeBuf {
214    fn eq(&self, other: &Self) -> bool {
215        self.ty() == other.ty()
216            && self.subty() == other.subty()
217            && self.suffix() == other.suffix()
218            && self.params().collect::<BTreeMap<_, _>>()
219                == other.params().collect::<BTreeMap<_, _>>()
220    }
221}
222
223impl Eq for MediaTypeBuf {}
224
225impl PartialEq<MediaType<'_>> for MediaTypeBuf {
226    fn eq(&self, other: &MediaType) -> bool {
227        self.ty() == other.ty
228            && self.subty() == other.subty
229            && self.suffix() == other.suffix
230            && self.params().collect::<BTreeMap<_, _>>()
231                == other.params().collect::<BTreeMap<_, _>>()
232    }
233}
234
235impl PartialEq<&MediaType<'_>> for MediaTypeBuf {
236    fn eq(&self, other: &&MediaType) -> bool {
237        self == *other
238    }
239}
240
241impl PartialEq<MediaType<'_>> for &MediaTypeBuf {
242    fn eq(&self, other: &MediaType) -> bool {
243        *self == other
244    }
245}
246
247impl fmt::Display for MediaTypeBuf {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        write!(f, "{}/{}", self.ty(), self.subty())?;
250        if let Some(suffix) = self.suffix() {
251            write!(f, "+{}", suffix)?;
252        }
253        for (name, value) in self.params() {
254            write!(f, "; {}={}", name, value)?;
255        }
256        Ok(())
257    }
258}
259
260impl Hash for MediaTypeBuf {
261    fn hash<H: Hasher>(&self, state: &mut H) {
262        self.ty().hash(state);
263        self.subty().hash(state);
264        self.suffix().hash(state);
265        self.params()
266            .collect::<BTreeMap<_, _>>()
267            .into_iter()
268            .collect::<Vec<_>>()
269            .hash(state);
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::{media_type, names::*, values::*};
277    use std::collections::hash_map::DefaultHasher;
278
279    fn calculate_hash<T: Hash>(t: &T) -> u64 {
280        let mut s = DefaultHasher::new();
281        t.hash(&mut s);
282        s.finish()
283    }
284
285    #[test]
286    fn from_parts() {
287        assert_eq!(
288            MediaTypeBuf::from_parts(IMAGE, SVG, Some(XML), &[(CHARSET, UTF_8)]).to_string(),
289            "image/svg+xml; charset=UTF-8"
290        );
291    }
292
293    #[test]
294    fn get_param() {
295        assert_eq!(
296            MediaTypeBuf::from_str("image/svg+xml")
297                .unwrap()
298                .get_param(CHARSET),
299            None
300        );
301        assert_eq!(
302            MediaTypeBuf::from_str("image/svg+xml; charset=UTF-8")
303                .unwrap()
304                .get_param(CHARSET),
305            Some(UTF_8)
306        );
307        assert_eq!(
308            MediaTypeBuf::from_str("image/svg+xml; charset=UTF-8; HELLO=WORLD; HELLO=world")
309                .unwrap()
310                .get_param(Name::new("hello").unwrap()),
311            Some(Value::new("world").unwrap())
312        );
313    }
314
315    #[test]
316    fn essence() {
317        assert_eq!(
318            MediaTypeBuf::from_str("image/svg+xml")
319                .unwrap()
320                .essence()
321                .to_string(),
322            "image/svg+xml"
323        );
324        assert_eq!(
325            MediaTypeBuf::from_str("image/svg+xml;  ")
326                .unwrap()
327                .essence()
328                .to_string(),
329            "image/svg+xml"
330        );
331        assert_eq!(
332            MediaTypeBuf::from_str("image/svg+xml; charset=UTF-8")
333                .unwrap()
334                .essence()
335                .to_string(),
336            "image/svg+xml"
337        );
338        assert_eq!(
339            MediaTypeBuf::from_str("image/svg+xml  ; charset=UTF-8")
340                .unwrap()
341                .essence()
342                .to_string(),
343            "image/svg+xml"
344        );
345    }
346
347    #[test]
348    fn canonicalize() {
349        assert_eq!(
350            MediaTypeBuf::from_str("IMAGE/SVG+XML;         CHARSET=UTF-8;     ")
351                .unwrap()
352                .canonicalize()
353                .to_string(),
354            "image/svg+xml; charset=UTF-8"
355        );
356    }
357
358    #[test]
359    fn cmp() {
360        assert_eq!(
361            MediaTypeBuf::from_str("text/plain").unwrap(),
362            MediaTypeBuf::from_str("TEXT/PLAIN").unwrap()
363        );
364        assert_eq!(
365            MediaTypeBuf::from_str("image/svg+xml; charset=UTF-8").unwrap(),
366            MediaTypeBuf::from_str("IMAGE/SVG+XML; CHARSET=UTF-8").unwrap()
367        );
368        assert_eq!(
369            MediaTypeBuf::from_str("image/svg+xml; hello=WORLD; charset=UTF-8").unwrap(),
370            MediaTypeBuf::from_str("IMAGE/SVG+XML; HELLO=WORLD; CHARSET=UTF-8").unwrap()
371        );
372        assert_eq!(
373            MediaTypeBuf::from_str("image/svg+xml; charset=UTF-8").unwrap(),
374            MediaType::from_parts(
375                IMAGE,
376                SVG,
377                Some(XML),
378                &[(CHARSET, US_ASCII), (CHARSET, UTF_8)]
379            ),
380        );
381        assert_eq!(
382            &MediaTypeBuf::from_str("image/svg+xml").unwrap(),
383            media_type!(IMAGE / SVG + XML)
384        );
385    }
386
387    #[test]
388    fn hash() {
389        assert_eq!(
390            calculate_hash(&MediaTypeBuf::from_str("text/plain").unwrap()),
391            calculate_hash(&MediaTypeBuf::from_str("TEXT/PLAIN").unwrap())
392        );
393        assert_eq!(
394            calculate_hash(&MediaTypeBuf::from_str("image/svg+xml; charset=UTF-8").unwrap()),
395            calculate_hash(&MediaTypeBuf::from_str("IMAGE/SVG+XML; CHARSET=UTF-8").unwrap())
396        );
397        assert_eq!(
398            calculate_hash(
399                &MediaTypeBuf::from_str("image/svg+xml; hello=WORLD; charset=UTF-8").unwrap()
400            ),
401            calculate_hash(
402                &MediaTypeBuf::from_str("IMAGE/SVG+XML; HELLO=WORLD; CHARSET=UTF-8").unwrap()
403            )
404        );
405        assert_eq!(
406            calculate_hash(&MediaTypeBuf::from_str("image/svg+xml; charset=UTF-8").unwrap()),
407            calculate_hash(
408                &MediaTypeBuf::from_str("image/svg+xml; charset=US-ASCII; charset=UTF-8").unwrap()
409            ),
410        );
411    }
412}