mediatype/
media_type.rs

1use super::{error::*, media_type_buf::*, name::*, params::*, parse::*, value::*};
2use std::{
3    borrow::Cow,
4    collections::BTreeMap,
5    fmt,
6    hash::{Hash, Hasher},
7};
8
9/// A borrowed media type.
10///
11/// ```
12/// use mediatype::{names::*, MediaType, Value, WriteParams};
13///
14/// let mut multipart = MediaType::new(MULTIPART, FORM_DATA);
15///
16/// let boundary = Value::new("dyEV84n7XNJ").unwrap();
17/// multipart.set_param(BOUNDARY, boundary);
18/// assert_eq!(
19///     multipart.to_string(),
20///     "multipart/form-data; boundary=dyEV84n7XNJ"
21/// );
22///
23/// multipart.subty = RELATED;
24/// assert_eq!(
25///     multipart.to_string(),
26///     "multipart/related; boundary=dyEV84n7XNJ"
27/// );
28///
29/// const IMAGE_SVG: MediaType = MediaType::from_parts(IMAGE, SVG, Some(XML), &[]);
30/// let svg = MediaType::parse("IMAGE/SVG+XML").unwrap();
31/// assert_eq!(svg, IMAGE_SVG);
32/// ```
33#[derive(Debug, Clone)]
34pub struct MediaType<'a> {
35    /// Top-level type.
36    pub ty: Name<'a>,
37
38    /// Subtype.
39    pub subty: Name<'a>,
40
41    /// Optional suffix.
42    pub suffix: Option<Name<'a>>,
43
44    /// Parameters.
45    pub params: Cow<'a, [(Name<'a>, Value<'a>)]>,
46}
47
48impl<'a> MediaType<'a> {
49    /// Constructs a `MediaType` from a top-level type and a subtype.
50    /// ```
51    /// # use mediatype::{names::*, MediaType};
52    /// const IMAGE_PNG: MediaType = MediaType::new(IMAGE, PNG);
53    /// assert_eq!(IMAGE_PNG, MediaType::parse("image/png").unwrap());
54    /// ```
55    #[must_use]
56    pub const fn new(ty: Name<'a>, subty: Name<'a>) -> Self {
57        Self {
58            ty,
59            subty,
60            suffix: None,
61            params: Cow::Borrowed(&[]),
62        }
63    }
64
65    /// Constructs a `MediaType` with an optional suffix and parameters.
66    ///
67    /// ```
68    /// # use mediatype::{names::*, values::*, MediaType};
69    /// const IMAGE_SVG: MediaType = MediaType::from_parts(IMAGE, SVG, Some(XML), &[(CHARSET, UTF_8)]);
70    /// assert_eq!(
71    ///     IMAGE_SVG,
72    ///     MediaType::parse("image/svg+xml; charset=UTF-8").unwrap()
73    /// );
74    /// ```
75    #[must_use]
76    pub const fn from_parts(
77        ty: Name<'a>,
78        subty: Name<'a>,
79        suffix: Option<Name<'a>>,
80        params: &'a [(Name<'a>, Value<'a>)],
81    ) -> Self {
82        Self {
83            ty,
84            subty,
85            suffix,
86            params: Cow::Borrowed(params),
87        }
88    }
89
90    pub(crate) const fn from_parts_unchecked(
91        ty: Name<'a>,
92        subty: Name<'a>,
93        suffix: Option<Name<'a>>,
94        params: Cow<'a, [(Name<'a>, Value<'a>)]>,
95    ) -> Self {
96        Self {
97            ty,
98            subty,
99            suffix,
100            params,
101        }
102    }
103
104    /// Constructs a `MediaType` from `str` without copying the string.
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if the string fails to be parsed.
109    pub fn parse<'s: 'a>(s: &'s str) -> Result<Self, MediaTypeError> {
110        let (indices, _) = Indices::parse(s)?;
111        let params = indices
112            .params()
113            .iter()
114            .map(|param| {
115                (
116                    Name::new_unchecked(&s[param[0]..param[1]]),
117                    Value::new_unchecked(&s[param[2]..param[3]]),
118                )
119            })
120            .collect();
121        Ok(Self {
122            ty: Name::new_unchecked(&s[indices.ty()]),
123            subty: Name::new_unchecked(&s[indices.subty()]),
124            suffix: indices.suffix().map(|range| Name::new_unchecked(&s[range])),
125            params: Cow::Owned(params),
126        })
127    }
128
129    /// Returns a [`MediaType`] without parameters.
130    ///
131    /// ```
132    /// # use mediatype::{names::*, values::*, MediaType};
133    /// const IMAGE_SVG: MediaType = MediaType::from_parts(IMAGE, SVG, Some(XML), &[(CHARSET, UTF_8)]);
134    /// assert_eq!(
135    ///     IMAGE_SVG.essence(),
136    ///     MediaType::parse("image/svg+xml").unwrap()
137    /// );
138    /// ```
139    ///
140    /// [`MadiaType`]: ./struct.MediaType.html
141    #[must_use]
142    pub const fn essence(&self) -> MediaType<'_> {
143        MediaType::from_parts(self.ty, self.subty, self.suffix, &[])
144    }
145}
146
147impl ReadParams for MediaType<'_> {
148    fn params(&self) -> Params {
149        Params::from_slice(&self.params)
150    }
151
152    fn get_param(&self, name: Name) -> Option<Value> {
153        self.params
154            .iter()
155            .rev()
156            .find(|&&param| name == param.0)
157            .map(|&(_, value)| value)
158    }
159}
160
161impl<'a> WriteParams<'a> for MediaType<'a> {
162    fn set_param<'n: 'a, 'v: 'a>(&mut self, name: Name<'n>, value: Value<'v>) {
163        self.remove_params(name);
164        let params = self.params.to_mut();
165        params.push((name, value));
166    }
167
168    fn remove_params(&mut self, name: Name) {
169        let key_exists = self.params.iter().any(|&param| name == param.0);
170        if key_exists {
171            self.params.to_mut().retain(|&param| name != param.0);
172        }
173    }
174
175    fn clear_params(&mut self) {
176        if !self.params.is_empty() {
177            self.params.to_mut().clear();
178        }
179    }
180}
181
182impl fmt::Display for MediaType<'_> {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        write!(f, "{}/{}", self.ty, self.subty)?;
185        if let Some(suffix) = self.suffix {
186            write!(f, "+{}", suffix)?;
187        }
188        for (name, value) in &*self.params {
189            write!(f, "; {}={}", name, value)?;
190        }
191        Ok(())
192    }
193}
194
195impl<'a> From<&'a MediaTypeBuf> for MediaType<'a> {
196    fn from(t: &'a MediaTypeBuf) -> Self {
197        t.to_ref()
198    }
199}
200
201impl<'b> PartialEq<MediaType<'b>> for MediaType<'_> {
202    fn eq(&self, other: &MediaType<'b>) -> bool {
203        self.ty == other.ty
204            && self.subty == other.subty
205            && self.suffix == other.suffix
206            && self.params().collect::<BTreeMap<_, _>>()
207                == other.params().collect::<BTreeMap<_, _>>()
208    }
209}
210
211impl Eq for MediaType<'_> {}
212
213impl PartialEq<MediaTypeBuf> for MediaType<'_> {
214    fn eq(&self, other: &MediaTypeBuf) -> 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 PartialEq<&MediaTypeBuf> for MediaType<'_> {
224    fn eq(&self, other: &&MediaTypeBuf) -> bool {
225        self == *other
226    }
227}
228
229impl Hash for MediaType<'_> {
230    fn hash<H: Hasher>(&self, state: &mut H) {
231        self.ty.hash(state);
232        self.subty.hash(state);
233        self.suffix.hash(state);
234        self.params()
235            .collect::<BTreeMap<_, _>>()
236            .into_iter()
237            .collect::<Vec<_>>()
238            .hash(state);
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::{names::*, values::*};
246    use std::collections::hash_map::DefaultHasher;
247    use std::str::FromStr;
248
249    fn calculate_hash<T: Hash>(t: &T) -> u64 {
250        let mut s = DefaultHasher::new();
251        t.hash(&mut s);
252        s.finish()
253    }
254
255    #[test]
256    fn to_string() {
257        assert_eq!(MediaType::new(_STAR, _STAR).to_string(), "*/*");
258        assert_eq!(MediaType::new(TEXT, PLAIN).to_string(), "text/plain");
259        assert_eq!(
260            MediaType::from_parts(IMAGE, SVG, Some(XML), &[]).to_string(),
261            "image/svg+xml"
262        );
263        assert_eq!(
264            MediaType::from_parts(TEXT, PLAIN, None, &[(CHARSET, UTF_8)]).to_string(),
265            "text/plain; charset=UTF-8"
266        );
267        assert_eq!(
268            MediaType::from_parts(IMAGE, SVG, Some(XML), &[(CHARSET, UTF_8)]).to_string(),
269            "image/svg+xml; charset=UTF-8"
270        );
271    }
272
273    #[test]
274    fn get_param() {
275        assert_eq!(MediaType::new(TEXT, PLAIN).get_param(CHARSET), None);
276        assert_eq!(
277            MediaType::from_parts(TEXT, PLAIN, None, &[(CHARSET, UTF_8)]).get_param(CHARSET),
278            Some(UTF_8)
279        );
280        assert_eq!(
281            MediaType::parse("image/svg+xml; charset=UTF-8; HELLO=WORLD; HELLO=world")
282                .unwrap()
283                .get_param(Name::new("hello").unwrap()),
284            Some(Value::new("world").unwrap())
285        );
286    }
287
288    #[test]
289    fn set_param() {
290        let mut media_type = MediaType::from_parts(TEXT, PLAIN, None, &[(CHARSET, UTF_8)]);
291        let lower_utf8 = Value::new("utf-8").unwrap();
292        media_type.set_param(CHARSET, lower_utf8);
293        assert_eq!(media_type.to_string(), "text/plain; charset=utf-8");
294
295        let alice = Name::new("ALICE").unwrap();
296        let bob = Value::new("bob").unwrap();
297        media_type.set_param(alice, bob);
298        media_type.set_param(alice, bob);
299
300        assert_eq!(
301            media_type.to_string(),
302            "text/plain; charset=utf-8; ALICE=bob"
303        );
304    }
305
306    #[test]
307    fn remove_params() {
308        let mut media_type = MediaType::from_parts(TEXT, PLAIN, None, &[(CHARSET, UTF_8)]);
309        media_type.remove_params(CHARSET);
310        assert_eq!(media_type.to_string(), "text/plain");
311
312        let mut media_type =
313            MediaType::parse("image/svg+xml; hello=WORLD; charset=UTF-8; HELLO=WORLD").unwrap();
314        media_type.remove_params(Name::new("hello").unwrap());
315        assert_eq!(media_type.to_string(), "image/svg+xml; charset=UTF-8");
316    }
317
318    #[test]
319    fn clear_params() {
320        let mut media_type = MediaType::parse("image/svg+xml; charset=UTF-8; HELLO=WORLD").unwrap();
321        media_type.clear_params();
322        assert_eq!(media_type.to_string(), "image/svg+xml");
323    }
324
325    #[test]
326    fn cmp() {
327        assert_eq!(
328            MediaType::parse("text/plain").unwrap(),
329            MediaType::parse("TEXT/PLAIN").unwrap()
330        );
331        assert_eq!(
332            MediaType::parse("image/svg+xml; charset=UTF-8").unwrap(),
333            MediaType::parse("IMAGE/SVG+XML; CHARSET=UTF-8").unwrap()
334        );
335        assert_eq!(
336            MediaType::parse("image/svg+xml; hello=WORLD; charset=UTF-8").unwrap(),
337            MediaType::parse("IMAGE/SVG+XML; HELLO=WORLD; CHARSET=UTF-8").unwrap()
338        );
339        assert_eq!(
340            MediaType::from_parts(
341                IMAGE,
342                SVG,
343                Some(XML),
344                &[(CHARSET, US_ASCII), (CHARSET, UTF_8)]
345            ),
346            MediaTypeBuf::from_str("image/svg+xml; charset=UTF-8").unwrap(),
347        );
348
349        const TEXT_PLAIN: MediaType = MediaType::from_parts(TEXT, PLAIN, None, &[]);
350        let text_plain = MediaType::parse("text/plain").unwrap();
351        assert_eq!(text_plain.essence(), TEXT_PLAIN);
352    }
353
354    #[test]
355    fn hash() {
356        assert_eq!(
357            calculate_hash(&MediaType::parse("text/plain").unwrap()),
358            calculate_hash(&MediaType::parse("TEXT/PLAIN").unwrap())
359        );
360        assert_eq!(
361            calculate_hash(&MediaType::parse("image/svg+xml; charset=UTF-8").unwrap()),
362            calculate_hash(&MediaType::parse("IMAGE/SVG+XML; CHARSET=UTF-8").unwrap())
363        );
364        assert_eq!(
365            calculate_hash(&MediaType::parse("image/svg+xml; hello=WORLD; charset=UTF-8").unwrap()),
366            calculate_hash(&MediaType::parse("IMAGE/SVG+XML; HELLO=WORLD; CHARSET=UTF-8").unwrap())
367        );
368        assert_eq!(
369            calculate_hash(&MediaType::from_parts(
370                IMAGE,
371                SVG,
372                Some(XML),
373                &[(CHARSET, UTF_8)]
374            )),
375            calculate_hash(&MediaType::from_parts(
376                IMAGE,
377                SVG,
378                Some(XML),
379                &[(CHARSET, US_ASCII), (CHARSET, UTF_8)]
380            )),
381        );
382    }
383}