axum_codec/
content.rs

1use core::fmt;
2use std::{convert::Infallible, str::FromStr};
3
4use axum::{
5	extract::FromRequestParts,
6	http::{header, request::Parts, HeaderValue},
7};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum ContentType {
12	#[cfg(feature = "json")]
13	Json,
14	#[cfg(feature = "form")]
15	Form,
16	#[cfg(feature = "msgpack")]
17	MsgPack,
18	#[cfg(feature = "bincode")]
19	Bincode,
20	#[cfg(feature = "bitcode")]
21	Bitcode,
22	#[cfg(feature = "cbor")]
23	Cbor,
24	#[cfg(feature = "yaml")]
25	Yaml,
26	#[cfg(feature = "toml")]
27	Toml,
28}
29
30#[cfg(not(any(
31	feature = "json",
32	feature = "form",
33	feature = "msgpack",
34	feature = "bincode",
35	feature = "bitcode",
36	feature = "cbor",
37	feature = "yaml",
38	feature = "toml"
39)))]
40const _: () = {
41	compile_error!(
42		"At least one of the following features must be enabled: `json`, `form`, `msgpack`, \
43		 `bincode`, `bitcode`, `cbor`, `yaml`, `toml`."
44	);
45
46	impl Default for ContentType {
47		fn default() -> Self {
48			unimplemented!()
49		}
50	}
51};
52
53#[cfg(any(
54	feature = "json",
55	feature = "form",
56	feature = "msgpack",
57	feature = "bincode",
58	feature = "bitcode",
59	feature = "cbor",
60	feature = "yaml",
61	feature = "toml"
62))]
63impl Default for ContentType {
64	#[allow(unreachable_code)]
65	fn default() -> Self {
66		#[cfg(feature = "json")]
67		return Self::Json;
68		#[cfg(feature = "form")]
69		return Self::Form;
70		#[cfg(feature = "msgpack")]
71		return Self::MsgPack;
72		#[cfg(feature = "bincode")]
73		return Self::Bincode;
74		#[cfg(feature = "bitcode")]
75		return Self::Bitcode;
76		#[cfg(feature = "cbor")]
77		return Self::Cbor;
78		#[cfg(feature = "yaml")]
79		return Self::Yaml;
80		#[cfg(feature = "toml")]
81		return Self::Toml;
82	}
83}
84
85impl fmt::Display for ContentType {
86	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87		f.write_str(self.as_str())
88	}
89}
90
91#[derive(Debug, thiserror::Error)]
92pub enum FromStrError {
93	#[error("invalid content type")]
94	InvalidContentType,
95	#[error(transparent)]
96	Mime(#[from] mime::FromStrError),
97}
98
99impl FromStr for ContentType {
100	type Err = FromStrError;
101
102	fn from_str(s: &str) -> Result<Self, Self::Err> {
103		let mime = s.parse::<mime::Mime>()?;
104		let subtype = mime.suffix().unwrap_or_else(|| mime.subtype());
105
106		Ok(match (mime.type_().as_str(), subtype.as_str()) {
107			#[cfg(feature = "json")]
108			("application", "json") => Self::Json,
109			#[cfg(feature = "form")]
110			("application", "x-www-form-urlencoded") => Self::Form,
111			#[cfg(feature = "msgpack")]
112			("application", "msgpack" | "vnd.msgpack" | "x-msgpack" | "x.msgpack") => Self::MsgPack,
113			#[cfg(feature = "bincode")]
114			("application", "bincode" | "vnd.bincode" | "x-bincode" | "x.bincode") => Self::Bincode,
115			#[cfg(feature = "bitcode")]
116			("application", "bitcode" | "vnd.bitcode" | "x-bitcode" | "x.bitcode") => Self::Bitcode,
117			#[cfg(feature = "cbor")]
118			("application", "cbor") => Self::Cbor,
119			#[cfg(feature = "yaml")]
120			("application" | "text", "yaml" | "yml" | "x-yaml") => Self::Yaml,
121			#[cfg(feature = "toml")]
122			("application" | "text", "toml" | "x-toml" | "vnd.toml") => Self::Toml,
123			_ => return Err(FromStrError::InvalidContentType),
124		})
125	}
126}
127
128impl ContentType {
129	/// Attempts to parse the given [`HeaderValue`] into a [`ContentType`]
130	/// by treating it as a MIME type.
131	///
132	/// Note that, along with official MIME types, this method also recognizes
133	/// some unofficial MIME types that are commonly used in practice.
134	///
135	/// ```edition2021
136	/// # use axum_codec::ContentType;
137	/// # use axum::http::HeaderValue;
138	/// #
139	/// # fn main() {
140	/// let header = HeaderValue::from_static("application/json");
141	/// let content_type = ContentType::from_header(&header).unwrap();
142	///
143	/// assert_eq!(content_type, ContentType::Json);
144	///
145	/// let header = HeaderValue::from_static("application/vnd.msgpack");
146	/// let content_type = ContentType::from_header(&header).unwrap();
147	///
148	/// assert_eq!(content_type, ContentType::MsgPack);
149	///
150	/// let header = HeaderValue::from_static("application/x-msgpack");
151	/// let content_type = ContentType::from_header(&header).unwrap();
152	///
153	/// assert_eq!(content_type, ContentType::MsgPack);
154	/// # }
155	pub fn from_header(header: &HeaderValue) -> Option<Self> {
156		header.to_str().ok()?.parse().ok()
157	}
158
159	/// Returns the MIME type as a string slice.
160	///
161	/// ```edition2021
162	/// # use axum_codec::ContentType;
163	/// #
164	/// let content_type = ContentType::Json;
165	///
166	/// assert_eq!(content_type.as_str(), "application/json");
167	/// ```
168	#[must_use]
169	pub fn as_str(&self) -> &'static str {
170		match *self {
171			#[cfg(feature = "json")]
172			Self::Json => "application/json",
173			#[cfg(feature = "form")]
174			Self::Form => "application/x-www-form-urlencoded",
175			#[cfg(feature = "msgpack")]
176			Self::MsgPack => "application/vnd.msgpack",
177			#[cfg(feature = "bincode")]
178			Self::Bincode => "application/vnd.bincode",
179			#[cfg(feature = "bitcode")]
180			Self::Bitcode => "application/vnd.bitcode",
181			#[cfg(feature = "cbor")]
182			Self::Cbor => "application/cbor",
183			#[cfg(feature = "yaml")]
184			Self::Yaml => "application/x-yaml",
185			#[cfg(feature = "toml")]
186			Self::Toml => "text/toml",
187		}
188	}
189
190	/// Converts the [`ContentType`] into a [`HeaderValue`].
191	///
192	/// ```edition2021
193	/// # use axum_codec::ContentType;
194	/// # use axum::http::HeaderValue;
195	/// #
196	/// # fn main() {
197	/// let content_type = ContentType::Json;
198	/// let header = content_type.into_header();
199	///
200	/// assert_eq!(header, HeaderValue::from_static("application/json"));
201	///
202	/// let content_type = ContentType::MsgPack;
203	/// let header = content_type.into_header();
204	///
205	/// assert_eq!(header, HeaderValue::from_static("application/vnd.msgpack"));
206	///
207	/// let content_type = ContentType::Yaml;
208	/// let header = content_type.into_header();
209	///
210	/// assert_eq!(header, HeaderValue::from_static("application/x-yaml"));
211	///
212	/// let content_type = ContentType::Toml;
213	/// let header = content_type.into_header();
214	///
215	/// assert_eq!(header, HeaderValue::from_static("text/toml"));
216	/// # }
217	#[must_use]
218	pub fn into_header(self) -> HeaderValue {
219		HeaderValue::from_static(self.as_str())
220	}
221}
222
223impl<S> FromRequestParts<S> for ContentType
224where
225	S: Sync,
226{
227	type Rejection = Infallible;
228
229	async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
230		let header = parts
231			.headers
232			.get(header::CONTENT_TYPE)
233			.and_then(Self::from_header);
234
235		Ok(header.unwrap_or_default())
236	}
237}
238
239/// Extractor for the request's desired response [`ContentType`].
240///
241/// # Examples
242///
243/// ```edition2021
244/// # use axum_codec::{Accept, Codec};
245/// # use axum::{http::HeaderValue, response::IntoResponse};
246/// # use serde::Serialize;
247/// #
248/// #[axum_codec::apply(encode)]
249/// struct User {
250///   name: String,
251///   age: u8,
252/// }
253///
254/// fn get_user(accept: Accept) -> impl IntoResponse {
255///   Codec(User {
256///     name: "Alice".into(),
257///     age: 42,
258///   })
259///   .to_response(accept)
260/// }
261/// #
262/// # fn main() {}
263/// ```
264#[derive(Debug, Clone, Copy)]
265pub struct Accept(ContentType);
266
267impl Accept {
268	/// Returns the request's desired response [`ContentType`].
269	#[inline]
270	#[must_use]
271	pub fn content_type(self) -> ContentType {
272		self.0
273	}
274}
275
276impl From<Accept> for ContentType {
277	#[inline]
278	fn from(accept: Accept) -> Self {
279		accept.0
280	}
281}
282
283impl<S> FromRequestParts<S> for Accept
284where
285	S: Send + Sync + 'static,
286{
287	type Rejection = Infallible;
288
289	async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
290		let header = None
291			.or_else(|| {
292				parts
293					.headers
294					.get(header::ACCEPT)
295					.and_then(ContentType::from_header)
296			})
297			.or_else(|| {
298				parts
299					.headers
300					.get(header::CONTENT_TYPE)
301					.and_then(ContentType::from_header)
302			})
303			.unwrap_or_default();
304
305		Ok(Self(header))
306	}
307}