http_types/content/
content_type.rs

1use std::{convert::TryInto, str::FromStr};
2
3use crate::headers::{HeaderName, HeaderValue, Headers, CONTENT_TYPE};
4use crate::Mime;
5
6/// Indicate the media type of a resource's content.
7///
8/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type)
9///
10/// # Specifications
11///
12/// - [RFC 7231, section 3.1.1.5: Content-Type](https://tools.ietf.org/html/rfc7231#section-3.1.1.5)
13/// - [RFC 7233, section 4.1: Content-Type in multipart](https://tools.ietf.org/html/rfc7233#section-4.1)
14///
15/// # Examples
16///
17/// ```
18/// # fn main() -> http_types::Result<()> {
19/// #
20/// use http_types::content::ContentType;
21/// use http_types::{Response, Mime};
22/// use std::str::FromStr;
23///
24/// let content_type = ContentType::new("text/*");
25///
26/// let mut res = Response::new(200);
27/// content_type.apply(&mut res);
28///
29/// let content_type = ContentType::from_headers(res)?.unwrap();
30/// assert_eq!(content_type.value(), format!("{}", Mime::from_str("text/*")?).as_str());
31/// #
32/// # Ok(()) }
33/// ```
34#[derive(Debug)]
35pub struct ContentType {
36    media_type: Mime,
37}
38
39impl ContentType {
40    /// Create a new instance.
41    pub fn new<U>(media_type: U) -> Self
42    where
43        U: TryInto<Mime>,
44        U::Error: std::fmt::Debug,
45    {
46        Self {
47            media_type: media_type
48                .try_into()
49                .expect("could not convert into a valid Mime type"),
50        }
51    }
52
53    /// Create a new instance from headers.
54    ///
55    /// `Content-Type` headers can provide both full and partial URLs. In
56    /// order to always return fully qualified URLs, a base URL must be passed to
57    /// reference the current environment. In HTTP/1.1 and above this value can
58    /// always be determined from the request.
59    pub fn from_headers(headers: impl AsRef<Headers>) -> crate::Result<Option<Self>> {
60        let headers = match headers.as_ref().get(CONTENT_TYPE) {
61            Some(headers) => headers,
62            None => return Ok(None),
63        };
64
65        // If we successfully parsed the header then there's always at least one
66        // entry. We want the last entry.
67        let ctation = headers.iter().last().unwrap();
68
69        let media_type = Mime::from_str(ctation.as_str()).map_err(|mut e| {
70            e.set_status(400);
71            e
72        })?;
73        Ok(Some(Self { media_type }))
74    }
75
76    /// Sets the header.
77    pub fn apply(&self, mut headers: impl AsMut<Headers>) {
78        headers.as_mut().insert(self.name(), self.value());
79    }
80
81    /// Get the `HeaderName`.
82    pub fn name(&self) -> HeaderName {
83        CONTENT_TYPE
84    }
85
86    /// Get the `HeaderValue`.
87    pub fn value(&self) -> HeaderValue {
88        let output = format!("{}", self.media_type);
89        // SAFETY: the internal string is validated to be ASCII.
90        unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
91    }
92}
93
94impl PartialEq<Mime> for ContentType {
95    fn eq(&self, other: &Mime) -> bool {
96        &self.media_type == other
97    }
98}
99
100impl PartialEq<&Mime> for ContentType {
101    fn eq(&self, other: &&Mime) -> bool {
102        &&self.media_type == other
103    }
104}
105
106impl From<Mime> for ContentType {
107    fn from(media_type: Mime) -> Self {
108        Self { media_type }
109    }
110}
111
112#[cfg(test)]
113mod test {
114    use super::*;
115    use crate::headers::Headers;
116
117    #[test]
118    fn smoke() -> crate::Result<()> {
119        let ct = ContentType::new(Mime::from_str("text/*")?);
120
121        let mut headers = Headers::new();
122        ct.apply(&mut headers);
123
124        let ct = ContentType::from_headers(headers)?.unwrap();
125        assert_eq!(
126            ct.value(),
127            format!("{}", Mime::from_str("text/*")?).as_str()
128        );
129        Ok(())
130    }
131
132    #[test]
133    fn bad_request_on_parse_error() {
134        let mut headers = Headers::new();
135        headers.insert(CONTENT_TYPE, "<nori ate the tag. yum.>");
136        let err = ContentType::from_headers(headers).unwrap_err();
137        assert_eq!(err.status(), 400);
138    }
139}