mediatype/
media_type.rs

1use super::{error::*, media_type_buf::*, name::*, params::*, parse::*, value::*};
2use alloc::{borrow::Cow, collections::BTreeMap, vec::Vec};
3use core::{
4    fmt,
5    hash::{Hash, Hasher},
6};
7
8/// A borrowed media type.
9///
10/// ```
11/// use mediatype::{names::*, MediaType, Value, WriteParams};
12///
13/// let mut multipart = MediaType::new(MULTIPART, FORM_DATA);
14///
15/// let boundary = Value::new("dyEV84n7XNJ").unwrap();
16/// multipart.set_param(BOUNDARY, boundary);
17/// assert_eq!(
18///     multipart.to_string(),
19///     "multipart/form-data; boundary=dyEV84n7XNJ"
20/// );
21///
22/// multipart.subty = RELATED;
23/// assert_eq!(
24///     multipart.to_string(),
25///     "multipart/related; boundary=dyEV84n7XNJ"
26/// );
27///
28/// const IMAGE_SVG: MediaType = MediaType::from_parts(IMAGE, SVG, Some(XML), &[]);
29/// let svg = MediaType::parse("IMAGE/SVG+XML").unwrap();
30/// assert_eq!(svg, IMAGE_SVG);
31/// ```
32#[derive(Debug, Clone)]
33pub struct MediaType<'a> {
34    /// Top-level type.
35    pub ty: Name<'a>,
36
37    /// Subtype.
38    pub subty: Name<'a>,
39
40    /// Optional suffix.
41    pub suffix: Option<Name<'a>>,
42
43    /// Parameters.
44    pub params: Cow<'a, [(Name<'a>, Value<'a>)]>,
45}
46
47impl<'a> MediaType<'a> {
48    /// Constructs a `MediaType` from a top-level type and a subtype.
49    /// ```
50    /// # use mediatype::{names::*, MediaType};
51    /// const IMAGE_PNG: MediaType = MediaType::new(IMAGE, PNG);
52    /// assert_eq!(IMAGE_PNG, MediaType::parse("image/png").unwrap());
53    /// ```
54    #[must_use]
55    pub const fn new(ty: Name<'a>, subty: Name<'a>) -> Self {
56        Self {
57            ty,
58            subty,
59            suffix: None,
60            params: Cow::Borrowed(&[]),
61        }
62    }
63
64    /// Constructs a `MediaType` with an optional suffix and parameters.
65    ///
66    /// ```
67    /// # use mediatype::{names::*, values::*, MediaType};
68    /// const IMAGE_SVG: MediaType = MediaType::from_parts(IMAGE, SVG, Some(XML), &[(CHARSET, UTF_8)]);
69    /// assert_eq!(
70    ///     IMAGE_SVG,
71    ///     MediaType::parse("image/svg+xml; charset=UTF-8").unwrap()
72    /// );
73    /// ```
74    #[must_use]
75    pub const fn from_parts(
76        ty: Name<'a>,
77        subty: Name<'a>,
78        suffix: Option<Name<'a>>,
79        params: &'a [(Name<'a>, Value<'a>)],
80    ) -> Self {
81        Self {
82            ty,
83            subty,
84            suffix,
85            params: Cow::Borrowed(params),
86        }
87    }
88
89    pub(crate) const fn from_parts_unchecked(
90        ty: Name<'a>,
91        subty: Name<'a>,
92        suffix: Option<Name<'a>>,
93        params: Cow<'a, [(Name<'a>, Value<'a>)]>,
94    ) -> Self {
95        Self {
96            ty,
97            subty,
98            suffix,
99            params,
100        }
101    }
102
103    /// Constructs a `MediaType` from `str` without copying the string.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if the string fails to be parsed.
108    pub fn parse<'s: 'a>(s: &'s str) -> Result<Self, MediaTypeError> {
109        let (indices, _) = Indices::parse(s)?;
110        let params = indices
111            .params()
112            .iter()
113            .map(|param| {
114                (
115                    Name::new_unchecked(&s[param[0]..param[1]]),
116                    Value::new_unchecked(&s[param[2]..param[3]]),
117                )
118            })
119            .collect();
120        Ok(Self {
121            ty: Name::new_unchecked(&s[indices.ty()]),
122            subty: Name::new_unchecked(&s[indices.subty()]),
123            suffix: indices.suffix().map(|range| Name::new_unchecked(&s[range])),
124            params: Cow::Owned(params),
125        })
126    }
127
128    /// Returns a [`MediaType`] without parameters.
129    ///
130    /// ```
131    /// # use mediatype::{names::*, values::*, MediaType};
132    /// const IMAGE_SVG: MediaType = MediaType::from_parts(IMAGE, SVG, Some(XML), &[(CHARSET, UTF_8)]);
133    /// assert_eq!(
134    ///     IMAGE_SVG.essence(),
135    ///     MediaType::parse("image/svg+xml").unwrap()
136    /// );
137    /// ```
138    ///
139    /// [`MadiaType`]: ./struct.MediaType.html
140    #[must_use]
141    pub const fn essence(&self) -> MediaType<'_> {
142        MediaType::from_parts(self.ty, self.subty, self.suffix, &[])
143    }
144}
145
146impl ReadParams for MediaType<'_> {
147    fn params(&self) -> Params {
148        Params::from_slice(&self.params)
149    }
150
151    fn get_param(&self, name: Name) -> Option<Value> {
152        self.params
153            .iter()
154            .rev()
155            .find(|&&param| name == param.0)
156            .map(|&(_, value)| value)
157    }
158}
159
160impl<'a> WriteParams<'a> for MediaType<'a> {
161    fn set_param<'n: 'a, 'v: 'a>(&mut self, name: Name<'n>, value: Value<'v>) {
162        self.remove_params(name);
163        let params = self.params.to_mut();
164        params.push((name, value));
165    }
166
167    fn remove_params(&mut self, name: Name) {
168        let key_exists = self.params.iter().any(|&param| name == param.0);
169        if key_exists {
170            self.params.to_mut().retain(|&param| name != param.0);
171        }
172    }
173
174    fn clear_params(&mut self) {
175        if !self.params.is_empty() {
176            self.params.to_mut().clear();
177        }
178    }
179}
180
181impl fmt::Display for MediaType<'_> {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        write!(f, "{}/{}", self.ty, self.subty)?;
184        if let Some(suffix) = self.suffix {
185            write!(f, "+{}", suffix)?;
186        }
187        for (name, value) in &*self.params {
188            write!(f, "; {}={}", name, value)?;
189        }
190        Ok(())
191    }
192}
193
194impl<'a> From<&'a MediaTypeBuf> for MediaType<'a> {
195    fn from(t: &'a MediaTypeBuf) -> Self {
196        t.to_ref()
197    }
198}
199
200impl<'b> PartialEq<MediaType<'b>> for MediaType<'_> {
201    fn eq(&self, other: &MediaType<'b>) -> bool {
202        self.ty == other.ty
203            && self.subty == other.subty
204            && self.suffix == other.suffix
205            && self.params().collect::<BTreeMap<_, _>>()
206                == other.params().collect::<BTreeMap<_, _>>()
207    }
208}
209
210impl Eq for MediaType<'_> {}
211
212impl PartialEq<MediaTypeBuf> for MediaType<'_> {
213    fn eq(&self, other: &MediaTypeBuf) -> bool {
214        self.ty == other.ty()
215            && self.subty == other.subty()
216            && self.suffix == other.suffix()
217            && self.params().collect::<BTreeMap<_, _>>()
218                == other.params().collect::<BTreeMap<_, _>>()
219    }
220}
221
222impl PartialEq<&MediaTypeBuf> for MediaType<'_> {
223    fn eq(&self, other: &&MediaTypeBuf) -> bool {
224        self == *other
225    }
226}
227
228impl Hash for MediaType<'_> {
229    fn hash<H: Hasher>(&self, state: &mut H) {
230        self.ty.hash(state);
231        self.subty.hash(state);
232        self.suffix.hash(state);
233        self.params()
234            .collect::<BTreeMap<_, _>>()
235            .into_iter()
236            .collect::<Vec<_>>()
237            .hash(state);
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::{names::*, values::*};
245    use alloc::string::ToString;
246    use core::str::FromStr;
247    use std::collections::hash_map::DefaultHasher;
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}