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#[derive(Debug, Clone)]
34pub struct MediaType<'a> {
35 pub ty: Name<'a>,
37
38 pub subty: Name<'a>,
40
41 pub suffix: Option<Name<'a>>,
43
44 pub params: Cow<'a, [(Name<'a>, Value<'a>)]>,
46}
47
48impl<'a> MediaType<'a> {
49 #[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 #[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 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 #[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(|&¶m| 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(|¶m| name == param.0);
170 if key_exists {
171 self.params.to_mut().retain(|¶m| 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}