1#[cfg(all(not(feature = "std"), feature = "alloc"))]
24use alloc::{borrow::ToOwned, format, string::String, vec, vec::Vec};
25use core::{fmt, str::FromStr};
26#[cfg(feature = "serde")]
27use serde::{Deserialize, Serialize};
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
41#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
42pub struct ContentType {
43 pub type_: String,
45 pub subtype: String,
47 pub params: Vec<(String, String)>,
49}
50
51#[cfg(feature = "serde")]
52impl Serialize for ContentType {
53 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
54 use core::fmt::Write;
55 let mut s = String::new();
56 let _ = write!(s, "{self}");
57 serializer.serialize_str(&s)
58 }
59}
60
61#[cfg(feature = "serde")]
62impl<'de> Deserialize<'de> for ContentType {
63 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
64 let s = String::deserialize(deserializer)?;
65 s.parse().map_err(serde::de::Error::custom)
66 }
67}
68
69impl ContentType {
70 #[must_use]
72 #[inline(never)]
73 pub fn new(type_: String, subtype: String) -> Self {
74 Self {
75 type_,
76 subtype,
77 params: Vec::new(),
78 }
79 }
80
81 #[must_use]
83 #[inline(never)]
84 pub fn with_params(type_: String, subtype: String, params: Vec<(String, String)>) -> Self {
85 Self {
86 type_,
87 subtype,
88 params,
89 }
90 }
91
92 #[must_use]
101 pub fn essence(&self) -> String {
102 format!("{}/{}", self.type_, self.subtype)
103 }
104
105 #[must_use]
114 pub fn param(&self, name: &str) -> Option<&str> {
115 self.params
116 .iter()
117 .find(|(k, _)| k.eq_ignore_ascii_case(name))
118 .map(|(_, v)| v.as_str())
119 }
120
121 #[must_use]
127 pub fn application_json() -> Self {
128 Self::new("application".into(), "json".into())
129 }
130
131 #[must_use]
133 pub fn application_problem_json() -> Self {
134 Self::new("application".into(), "problem+json".into())
135 }
136
137 #[must_use]
139 pub fn application_octet_stream() -> Self {
140 Self::new("application".into(), "octet-stream".into())
141 }
142
143 #[must_use]
145 pub fn multipart_form_data(boundary: impl Into<String>) -> Self {
146 Self::with_params(
147 "multipart".into(),
148 "form-data".into(),
149 vec![("boundary".to_owned(), boundary.into())],
150 )
151 }
152
153 #[must_use]
155 pub fn text_plain() -> Self {
156 Self::new("text".into(), "plain".into())
157 }
158
159 #[must_use]
161 pub fn text_plain_utf8() -> Self {
162 Self::with_params(
163 "text".into(),
164 "plain".into(),
165 vec![("charset".to_owned(), "utf-8".to_owned())],
166 )
167 }
168
169 #[must_use]
171 pub fn text_html() -> Self {
172 Self::new("text".into(), "html".into())
173 }
174}
175
176impl fmt::Display for ContentType {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 write!(f, "{}/{}", self.type_, self.subtype)?;
183 for (k, v) in &self.params {
184 write!(f, "; {k}={v}")?;
185 }
186 Ok(())
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct ParseContentTypeError;
197
198impl fmt::Display for ParseContentTypeError {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 f.write_str("invalid Content-Type value")
201 }
202}
203
204#[cfg(feature = "std")]
205impl std::error::Error for ParseContentTypeError {}
206
207impl FromStr for ContentType {
208 type Err = ParseContentTypeError;
209
210 fn from_str(s: &str) -> Result<Self, Self::Err> {
215 let s = s.trim();
216 let mut parts = s.splitn(2, ';');
217 let essence = parts.next().unwrap_or("").trim();
218 let mut type_sub = essence.splitn(2, '/');
219 let type_ = type_sub.next().unwrap_or("").trim();
220 let subtype = type_sub.next().unwrap_or("").trim();
221 if type_.is_empty() || subtype.is_empty() {
222 return Err(ParseContentTypeError);
223 }
224
225 let mut params = Vec::new();
226 if let Some(param_str) = parts.next() {
227 for param in param_str.split(';') {
228 let param = param.trim();
229 if param.is_empty() {
230 continue;
231 }
232 let mut kv = param.splitn(2, '=');
233 let k = kv.next().unwrap_or("").trim().to_ascii_lowercase();
234 let v = kv.next().unwrap_or("").trim().to_owned();
235 if k.is_empty() {
236 return Err(ParseContentTypeError);
237 }
238 params.push((k, v));
239 }
240 }
241
242 Ok(Self {
243 type_: type_.to_ascii_lowercase(),
244 subtype: subtype.to_ascii_lowercase(),
245 params,
246 })
247 }
248}
249
250#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn display_no_params() {
260 assert_eq!(
261 ContentType::application_json().to_string(),
262 "application/json"
263 );
264 assert_eq!(
265 ContentType::application_problem_json().to_string(),
266 "application/problem+json"
267 );
268 assert_eq!(
269 ContentType::application_octet_stream().to_string(),
270 "application/octet-stream"
271 );
272 }
273
274 #[test]
275 fn display_with_params() {
276 let ct = ContentType::text_plain_utf8();
277 assert_eq!(ct.to_string(), "text/plain; charset=utf-8");
278 }
279
280 #[test]
281 fn display_multipart() {
282 let ct = ContentType::multipart_form_data("abc123");
283 assert_eq!(ct.to_string(), "multipart/form-data; boundary=abc123");
284 }
285
286 #[test]
287 fn essence_strips_params() {
288 let ct = ContentType::text_plain_utf8();
289 assert_eq!(ct.essence(), "text/plain");
290 }
291
292 #[test]
293 fn param_lookup() {
294 let ct = ContentType::text_plain_utf8();
295 assert_eq!(ct.param("charset"), Some("utf-8"));
296 assert_eq!(ct.param("boundary"), None);
297 }
298
299 #[test]
300 fn parse_simple() {
301 let ct: ContentType = "application/json".parse().unwrap();
302 assert_eq!(ct.type_, "application");
303 assert_eq!(ct.subtype, "json");
304 assert!(ct.params.is_empty());
305 }
306
307 #[test]
308 fn parse_with_charset() {
309 let ct: ContentType = "text/plain; charset=utf-8".parse().unwrap();
310 assert_eq!(ct.type_, "text");
311 assert_eq!(ct.subtype, "plain");
312 assert_eq!(ct.param("charset"), Some("utf-8"));
313 }
314
315 #[test]
316 fn parse_case_insensitive_type() {
317 let ct: ContentType = "Application/JSON".parse().unwrap();
318 assert_eq!(ct.type_, "application");
319 assert_eq!(ct.subtype, "json");
320 }
321
322 #[test]
323 fn parse_invalid_no_slash() {
324 assert_eq!(
325 "application".parse::<ContentType>(),
326 Err(ParseContentTypeError)
327 );
328 }
329
330 #[test]
331 fn parse_invalid_empty() {
332 assert_eq!("".parse::<ContentType>(), Err(ParseContentTypeError));
333 }
334
335 #[test]
336 fn round_trip() {
337 let ct = ContentType::text_plain_utf8();
338 let s = ct.to_string();
339 let back: ContentType = s.parse().unwrap();
340 assert_eq!(back, ct);
341 }
342
343 #[test]
344 fn new_constructor() {
345 let ct = ContentType::new(String::from("image"), String::from("png"));
346 assert_eq!(ct.type_, "image");
347 assert_eq!(ct.subtype, "png");
348 assert!(ct.params.is_empty());
349 }
350
351 #[test]
352 fn with_params_constructor() {
353 let ct = ContentType::with_params(
354 String::from("application"),
355 String::from("xml"),
356 vec![("charset".into(), "utf-8".into())],
357 );
358 assert_eq!(ct.param("charset"), Some("utf-8"));
359 assert_eq!(ct.essence(), "application/xml");
360 }
361
362 #[test]
363 fn text_html_constructor() {
364 let ct = ContentType::text_html();
365 assert_eq!(ct.to_string(), "text/html");
366 }
367
368 #[test]
369 fn text_plain_constructor() {
370 let ct = ContentType::text_plain();
371 assert_eq!(ct.to_string(), "text/plain");
372 }
373
374 #[test]
375 fn parse_error_display() {
376 let err = ParseContentTypeError;
377 assert!(!err.to_string().is_empty());
378 }
379
380 #[test]
381 fn param_case_insensitive() {
382 let ct: ContentType = "text/plain; Charset=UTF-8".parse().unwrap();
384 assert_eq!(ct.param("charset"), Some("UTF-8"));
385 assert_eq!(ct.param("CHARSET"), Some("UTF-8"));
386 assert_eq!(ct.param("missing"), None);
387 }
388
389 #[cfg(feature = "serde")]
390 #[test]
391 fn serde_round_trip() {
392 let ct = ContentType::application_problem_json();
393 let json = serde_json::to_string(&ct).unwrap();
394 assert_eq!(json, r#""application/problem+json""#);
395 let back: ContentType = serde_json::from_str(&json).unwrap();
396 assert_eq!(back, ct);
397 }
398}