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
21pub 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, body) = match self.kind {
57 JsonRejectionKind::ContentType => (
58 StatusCode::UNSUPPORTED_MEDIA_TYPE,
59 String::from("Expected request with `Content-Type: application/json`"),
60 ),
61 JsonRejectionKind::Report(report) => (StatusCode::BAD_REQUEST, report),
62 JsonRejectionKind::BytesRejection05(rejection) => {
63 return rejection.into_response();
64 }
65 };
66
67 (
68 status,
69 [(
70 header::CONTENT_TYPE,
71 HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
72 )],
73 body,
74 )
75 .into_response()
76 }
77}
78
79pub struct Json<T>(pub T);
81
82#[cfg(feature = "axum-core05")]
83#[cfg_attr(doc_cfg, doc(cfg(feature = "axum-core05")))]
84impl<T, S> extract05::FromRequest<S> for Json<T>
85where
86 T: DecodeOwned<Text, Global>,
87 S: Send + Sync,
88{
89 type Rejection = JsonRejection;
90
91 async fn from_request(req: extract05::Request, state: &S) -> Result<Self, Self::Rejection> {
92 if !json_content_type(req.headers()) {
93 return Err(JsonRejection {
94 kind: JsonRejectionKind::ContentType,
95 });
96 }
97
98 let bytes = Bytes::from_request(req, state).await?;
99 Self::from_bytes(&bytes)
100 }
101}
102
103fn json_content_type(headers: &HeaderMap) -> bool {
104 let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
105 content_type
106 } else {
107 return false;
108 };
109
110 let content_type = if let Ok(content_type) = content_type.to_str() {
111 content_type
112 } else {
113 return false;
114 };
115
116 let mime = if let Ok(mime) = content_type.parse::<mime::Mime>() {
117 mime
118 } else {
119 return false;
120 };
121
122 mime.type_() == "application"
123 && (mime.subtype() == "json" || mime.suffix().is_some_and(|name| name == "json"))
124}
125
126#[cfg(feature = "axum-core05")]
127#[cfg_attr(doc_cfg, doc(cfg(feature = "axum-core05")))]
128impl<T> response05::IntoResponse for Json<T>
129where
130 T: Encode<Text>,
131{
132 fn into_response(self) -> response05::Response {
133 let cx = musli::context::new().with_trace();
134
135 let mut buf = BytesMut::with_capacity(128).writer();
138
139 match ENCODING.to_writer_with(&cx, &mut buf, &self.0) {
140 Ok(()) => {
141 let content_type = [(
142 header::CONTENT_TYPE,
143 HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
144 )];
145 let report = buf.into_inner().freeze();
146 (content_type, report).into_response()
147 }
148 Err(ErrorMarker { .. }) => {
149 let status = StatusCode::INTERNAL_SERVER_ERROR;
150 let content_type = [(
151 header::CONTENT_TYPE,
152 HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
153 )];
154 let report = cx.report().to_string();
155 (status, content_type, report).into_response()
156 }
157 }
158 }
159}
160
161impl<T> Json<T>
162where
163 T: DecodeOwned<Text, Global>,
164{
165 #[inline]
166 fn from_bytes(bytes: &[u8]) -> Result<Self, JsonRejection> {
167 let cx = musli::context::new().with_trace();
168
169 if let Ok(value) = ENCODING.from_slice_with(&cx, bytes) {
170 return Ok(Json(value));
171 }
172
173 let report = cx.report();
174 let report = report.to_string();
175 Err(JsonRejection::report(report))
176 }
177}