musli_web/
json.rs

1use alloc::string::{String, ToString};
2
3#[cfg(feature = "axum-core05")]
4use axum_core05::extract as extract05;
5#[cfg(feature = "axum-core05")]
6use axum_core05::extract::rejection as rejection05;
7#[cfg(feature = "axum-core05")]
8use axum_core05::response as response05;
9use bytes::{BufMut, Bytes, BytesMut};
10use http::header::{self, HeaderValue};
11use http::{HeaderMap, StatusCode};
12use musli::Encode;
13use musli::alloc::Global;
14use musli::context::ErrorMarker;
15use musli::de::DecodeOwned;
16use musli::json::Encoding;
17use musli::mode::Text;
18
19const ENCODING: Encoding = Encoding::new();
20
21/// A rejection from the JSON extractor.
22pub struct JsonRejection {
23    kind: JsonRejectionKind,
24}
25
26impl JsonRejection {
27    #[inline]
28    pub(crate) fn report(report: String) -> Self {
29        Self {
30            kind: JsonRejectionKind::Report(report),
31        }
32    }
33}
34
35enum JsonRejectionKind {
36    ContentType,
37    Report(String),
38    #[cfg(feature = "axum-core05")]
39    BytesRejection05(rejection05::BytesRejection),
40}
41
42#[cfg(feature = "axum-core05")]
43impl From<rejection05::BytesRejection> for JsonRejection {
44    #[inline]
45    fn from(rejection: rejection05::BytesRejection) -> Self {
46        JsonRejection {
47            kind: JsonRejectionKind::BytesRejection05(rejection),
48        }
49    }
50}
51
52#[cfg(feature = "axum-core05")]
53#[cfg_attr(doc_cfg, doc(cfg(feature = "axum-core05")))]
54impl response05::IntoResponse for JsonRejection {
55    fn into_response(self) -> response05::Response {
56        let status;
57        let body;
58
59        match self.kind {
60            JsonRejectionKind::ContentType => {
61                status = StatusCode::UNSUPPORTED_MEDIA_TYPE;
62                body = String::from("Expected request with `Content-Type: application/json`");
63            }
64            JsonRejectionKind::Report(report) => {
65                status = StatusCode::BAD_REQUEST;
66                body = report;
67            }
68            JsonRejectionKind::BytesRejection05(rejection) => {
69                return rejection.into_response();
70            }
71        }
72
73        (
74            status,
75            [(
76                header::CONTENT_TYPE,
77                HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
78            )],
79            body,
80        )
81            .into_response()
82    }
83}
84
85/// Encode the given value as JSON.
86pub struct Json<T>(pub T);
87
88#[cfg(feature = "axum-core05")]
89#[cfg_attr(doc_cfg, doc(cfg(feature = "axum-core05")))]
90impl<T, S> extract05::FromRequest<S> for Json<T>
91where
92    T: DecodeOwned<Text, Global>,
93    S: Send + Sync,
94{
95    type Rejection = JsonRejection;
96
97    async fn from_request(req: extract05::Request, state: &S) -> Result<Self, Self::Rejection> {
98        if !json_content_type(req.headers()) {
99            return Err(JsonRejection {
100                kind: JsonRejectionKind::ContentType,
101            });
102        }
103
104        let bytes = Bytes::from_request(req, state).await?;
105        Self::from_bytes(&bytes)
106    }
107}
108
109fn json_content_type(headers: &HeaderMap) -> bool {
110    let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
111        content_type
112    } else {
113        return false;
114    };
115
116    let content_type = if let Ok(content_type) = content_type.to_str() {
117        content_type
118    } else {
119        return false;
120    };
121
122    let mime = if let Ok(mime) = content_type.parse::<mime::Mime>() {
123        mime
124    } else {
125        return false;
126    };
127
128    mime.type_() == "application"
129        && (mime.subtype() == "json" || mime.suffix().is_some_and(|name| name == "json"))
130}
131
132#[cfg(feature = "axum-core05")]
133#[cfg_attr(doc_cfg, doc(cfg(feature = "axum-core05")))]
134impl<T> response05::IntoResponse for Json<T>
135where
136    T: Encode<Text>,
137{
138    fn into_response(self) -> response05::Response {
139        let cx = musli::context::new().with_trace();
140
141        // Use a small initial capacity of 128 bytes like serde_json::to_vec
142        // https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189
143        let mut buf = BytesMut::with_capacity(128).writer();
144
145        match ENCODING.to_writer_with(&cx, &mut buf, &self.0) {
146            Ok(()) => {
147                let content_type = [(
148                    header::CONTENT_TYPE,
149                    HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
150                )];
151                let report = buf.into_inner().freeze();
152                (content_type, report).into_response()
153            }
154            Err(ErrorMarker { .. }) => {
155                let status = StatusCode::INTERNAL_SERVER_ERROR;
156                let content_type = [(
157                    header::CONTENT_TYPE,
158                    HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
159                )];
160                let report = cx.report().to_string();
161                (status, content_type, report).into_response()
162            }
163        }
164    }
165}
166
167impl<T> Json<T>
168where
169    T: DecodeOwned<Text, Global>,
170{
171    #[inline]
172    fn from_bytes(bytes: &[u8]) -> Result<Self, JsonRejection> {
173        let cx = musli::context::new().with_trace();
174
175        if let Ok(value) = ENCODING.from_slice_with(&cx, bytes) {
176            return Ok(Json(value));
177        }
178
179        let report = cx.report();
180        let report = report.to_string();
181        Err(JsonRejection::report(report))
182    }
183}