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