Skip to content

Commit 5a9fd58

Browse files
committed
feat: add more impls, make enums non_exhaustive
1 parent 4574210 commit 5a9fd58

File tree

4 files changed

+101
-34
lines changed

4 files changed

+101
-34
lines changed

src/content.rs

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
use std::convert::Infallible;
1+
use core::fmt;
2+
use std::{convert::Infallible, str::FromStr};
23

34
use axum::{
45
extract::FromRequestParts,
56
http::{header, request::Parts, HeaderValue},
67
};
78

89
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10+
#[non_exhaustive]
911
pub enum ContentType {
1012
#[cfg(feature = "json")]
1113
Json,
@@ -71,6 +73,47 @@ impl Default for ContentType {
7173
}
7274
}
7375

76+
impl fmt::Display for ContentType {
77+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78+
f.write_str(self.as_str())
79+
}
80+
}
81+
82+
#[derive(Debug, thiserror::Error)]
83+
pub enum FromStrError {
84+
#[error("invalid content type")]
85+
InvalidContentType,
86+
#[error(transparent)]
87+
Mime(#[from] mime::FromStrError),
88+
}
89+
90+
impl FromStr for ContentType {
91+
type Err = FromStrError;
92+
93+
fn from_str(s: &str) -> Result<Self, Self::Err> {
94+
let mime = s.parse::<mime::Mime>()?;
95+
let subtype = mime.suffix().unwrap_or_else(|| mime.subtype());
96+
97+
Ok(match (mime.type_().as_str(), subtype.as_str()) {
98+
#[cfg(feature = "json")]
99+
("application", "json") => Self::Json,
100+
#[cfg(feature = "msgpack")]
101+
("application", "msgpack" | "vnd.msgpack" | "x-msgpack" | "x.msgpack") => Self::MsgPack,
102+
#[cfg(feature = "bincode")]
103+
("application", "bincode" | "vnd.bincode" | "x-bincode" | "x.bincode") => Self::Bincode,
104+
#[cfg(feature = "bitcode")]
105+
("application", "bitcode" | "vnd.bitcode" | "x-bitcode" | "x.bitcode") => Self::Bitcode,
106+
#[cfg(feature = "cbor")]
107+
("application", "cbor") => Self::Cbor,
108+
#[cfg(feature = "yaml")]
109+
("application" | "text", "yaml" | "yml" | "x-yaml") => Self::Yaml,
110+
#[cfg(feature = "toml")]
111+
("application" | "text", "toml" | "x-toml" | "vnd.toml") => Self::Toml,
112+
_ => return Err(FromStrError::InvalidContentType),
113+
})
114+
}
115+
}
116+
74117
impl ContentType {
75118
/// Attempts to parse the given [`HeaderValue`] into a [`ContentType`]
76119
/// by treating it as a MIME type.
@@ -99,26 +142,36 @@ impl ContentType {
99142
/// assert_eq!(content_type, ContentType::MsgPack);
100143
/// # }
101144
pub fn from_header(header: &HeaderValue) -> Option<Self> {
102-
let mime = header.to_str().ok()?.parse::<mime::Mime>().ok()?;
103-
let subtype = mime.suffix().map_or_else(|| mime.subtype(), |name| name);
145+
header.to_str().ok()?.parse().ok()
146+
}
104147

105-
Some(match (mime.type_().as_str(), subtype.as_str()) {
148+
/// Returns the MIME type as a string slice.
149+
///
150+
/// ```edition2021
151+
/// # use axum_codec::ContentType;
152+
/// #
153+
/// let content_type = ContentType::Json;
154+
///
155+
/// assert_eq!(content_type.as_str(), "application/json");
156+
/// ```
157+
#[must_use]
158+
pub fn as_str(&self) -> &'static str {
159+
match self {
106160
#[cfg(feature = "json")]
107-
("application", "json") => Self::Json,
161+
Self::Json => "application/json",
108162
#[cfg(feature = "msgpack")]
109-
("application", "msgpack" | "vnd.msgpack" | "x-msgpack" | "x.msgpack") => Self::MsgPack,
163+
Self::MsgPack => "application/vnd.msgpack",
110164
#[cfg(feature = "bincode")]
111-
("application", "bincode" | "vnd.bincode" | "x-bincode" | "x.bincode") => Self::Bincode,
165+
Self::Bincode => "application/vnd.bincode",
112166
#[cfg(feature = "bitcode")]
113-
("application", "bitcode" | "vnd.bitcode" | "x-bitcode" | "x.bitcode") => Self::Bitcode,
167+
Self::Bitcode => "application/vnd.bitcode",
114168
#[cfg(feature = "cbor")]
115-
("application", "cbor") => Self::Cbor,
169+
Self::Cbor => "application/cbor",
116170
#[cfg(feature = "yaml")]
117-
("application" | "text", "yaml" | "yml" | "x-yaml") => Self::Yaml,
171+
Self::Yaml => "application/x-yaml",
118172
#[cfg(feature = "toml")]
119-
("application" | "text", "toml" | "x-toml" | "vnd.toml") => Self::Toml,
120-
_ => return None,
121-
})
173+
Self::Toml => "text/toml",
174+
}
122175
}
123176

124177
/// Converts the [`ContentType`] into a [`HeaderValue`].
@@ -150,24 +203,7 @@ impl ContentType {
150203
/// # }
151204
#[must_use]
152205
pub fn into_header(self) -> HeaderValue {
153-
let text = match self {
154-
#[cfg(feature = "json")]
155-
Self::Json => "application/json",
156-
#[cfg(feature = "msgpack")]
157-
Self::MsgPack => "application/vnd.msgpack",
158-
#[cfg(feature = "bincode")]
159-
Self::Bincode => "application/vnd.bincode",
160-
#[cfg(feature = "bitcode")]
161-
Self::Bitcode => "application/vnd.bitcode",
162-
#[cfg(feature = "cbor")]
163-
Self::Cbor => "application/cbor",
164-
#[cfg(feature = "yaml")]
165-
Self::Yaml => "application/x-yaml",
166-
#[cfg(feature = "toml")]
167-
Self::Toml => "text/toml",
168-
};
169-
170-
HeaderValue::from_static(text)
206+
HeaderValue::from_static(self.as_str())
171207
}
172208
}
173209

src/decode.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ impl<T> Codec<T> {
134134
ContentType::Yaml => Self::from_yaml(core::str::from_utf8(bytes)?)?,
135135
#[cfg(feature = "toml")]
136136
ContentType::Toml => Self::from_toml(core::str::from_utf8(bytes)?)?,
137+
#[allow(unreachable_patterns)]
138+
_ => return Err(CodecRejection::UnsupportedContentType(content_type)),
137139
};
138140

139141
#[cfg(feature = "validator")]

src/encode.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ crate::macros::__private_encode_trait! {
1515
/// In debug mode this will include the error message. In release mode it will
1616
/// only include a status code of `500 Internal Server Error`.
1717
#[derive(Debug, thiserror::Error)]
18+
#[non_exhaustive]
1819
pub enum Error {
1920
#[cfg(feature = "json")]
2021
#[error(transparent)]

src/rejection.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use axum::{extract::rejection::BytesRejection, response::Response};
1+
use axum::{extract::rejection::BytesRejection, http::StatusCode, response::Response};
22

33
use crate::{ContentType, IntoCodecResponse};
44

@@ -7,6 +7,7 @@ use crate::{ContentType, IntoCodecResponse};
77
/// Contains one variant for each way the [`Codec`](crate::Codec) extractor
88
/// can fail.
99
#[derive(Debug, thiserror::Error)]
10+
#[non_exhaustive]
1011
pub enum CodecRejection {
1112
#[error(transparent)]
1213
Bytes(#[from] BytesRejection),
@@ -42,7 +43,10 @@ pub enum CodecRejection {
4243
#[cfg(not(feature = "pretty-errors"))]
4344
impl IntoCodecResponse for CodecRejection {
4445
fn into_codec_response(self, _content_type: ContentType) -> Response {
45-
self.to_string().into_response()
46+
let mut response = self.to_string().into_response();
47+
48+
*response.status_mut() = self.status_code();
49+
response
4650
}
4751
}
4852

@@ -68,7 +72,10 @@ impl aide::OperationOutput for CodecRejection {
6872
#[cfg(feature = "pretty-errors")]
6973
impl IntoCodecResponse for CodecRejection {
7074
fn into_codec_response(self, content_type: ContentType) -> Response {
71-
crate::Codec(self.message()).into_codec_response(content_type)
75+
let mut response = crate::Codec(self.message()).into_codec_response(content_type);
76+
77+
*response.status_mut() = self.status_code();
78+
response
7279
}
7380
}
7481

@@ -105,6 +112,27 @@ impl aide::OperationOutput for Message {
105112
}
106113

107114
impl CodecRejection {
115+
/// Returns the HTTP status code for the rejection.
116+
///
117+
/// # Examples
118+
///
119+
/// ```edition2021
120+
/// # use axum_codec::CodecRejection;
121+
/// # use axum::http::StatusCode;
122+
/// #
123+
/// let rejection = CodecRejection::UnsupportedContentType("application/xml".parse().unwrap());
124+
///
125+
/// assert_eq!(rejection.status_code(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
126+
/// ```
127+
#[must_use]
128+
pub fn status_code(&self) -> StatusCode {
129+
if matches!(self, Self::Bytes(..)) {
130+
StatusCode::PAYLOAD_TOO_LARGE
131+
} else {
132+
StatusCode::BAD_REQUEST
133+
}
134+
}
135+
108136
/// Consumes the rejection and returns a pretty [`Message`] representing the error.
109137
///
110138
/// Useful for sending a detailed error message to the client, but not so much

0 commit comments

Comments
 (0)