trillium_api/api_conn_ext.rs
1use mime::Mime;
2use serde::{de::DeserializeOwned, Serialize};
3use trillium::{
4 Conn,
5 KnownHeaderName::{Accept, ContentType},
6 Status,
7};
8
9use crate::{Error, Result};
10
11/// Extension trait that adds api methods to [`trillium::Conn`]
12#[trillium::async_trait]
13pub trait ApiConnExt {
14 /**
15 Sends a json response body. This sets a status code of 200,
16 serializes the body with serde_json, sets the content-type to
17 application/json, and [halts](trillium::Conn::halt) the
18 conn. If serialization fails, a 500 status code is sent as per
19 [`trillium::conn_try`]
20
21
22 ## Examples
23
24 ```
25 use trillium_api::{json, ApiConnExt};
26 async fn handler(conn: trillium::Conn) -> trillium::Conn {
27 conn.with_json(&json!({ "json macro": "is reexported" }))
28 }
29
30 # use trillium_testing::prelude::*;
31 assert_ok!(
32 get("/").on(&handler),
33 r#"{"json macro":"is reexported"}"#,
34 "content-type" => "application/json"
35 );
36 ```
37
38 ### overriding status code
39 ```
40 use trillium_api::ApiConnExt;
41 use serde::Serialize;
42
43 #[derive(Serialize)]
44 struct ApiResponse {
45 string: &'static str,
46 number: usize
47 }
48
49 async fn handler(conn: trillium::Conn) -> trillium::Conn {
50 conn.with_json(&ApiResponse { string: "not the most creative example", number: 100 })
51 .with_status(201)
52 }
53
54 # use trillium_testing::prelude::*;
55 assert_response!(
56 get("/").on(&handler),
57 Status::Created,
58 r#"{"string":"not the most creative example","number":100}"#,
59 "content-type" => "application/json"
60 );
61 ```
62 */
63 fn with_json(self, response: &impl Serialize) -> Self;
64
65 /**
66 Attempts to deserialize a type from the request body, based on the
67 request content type.
68
69 By default, both application/json and
70 application/x-www-form-urlencoded are supported, and future
71 versions may add accepted request content types. Please open an
72 issue if you need to accept another content type.
73
74
75 To exclusively accept application/json, disable default features
76 on this crate.
77
78 This sets a status code of Status::Ok if and only if no status
79 code has been explicitly set.
80
81 ## Examples
82
83 ### Deserializing to [`Value`]
84
85 ```
86 use trillium_api::{ApiConnExt, Value};
87
88 async fn handler(mut conn: trillium::Conn) -> trillium::Conn {
89 let value: Value = trillium::conn_try!(conn.deserialize().await, conn);
90 conn.with_json(&value)
91 }
92
93 # use trillium_testing::prelude::*;
94 assert_ok!(
95 post("/")
96 .with_request_body(r#"key=value"#)
97 .with_request_header("content-type", "application/x-www-form-urlencoded")
98 .on(&handler),
99 r#"{"key":"value"}"#,
100 "content-type" => "application/json"
101 );
102
103 ```
104
105 ### Deserializing a concrete type
106
107 ```
108 use trillium_api::ApiConnExt;
109
110 #[derive(serde::Deserialize)]
111 struct KvPair { key: String, value: String }
112
113 async fn handler(mut conn: trillium::Conn) -> trillium::Conn {
114 match conn.deserialize().await {
115 Ok(KvPair { key, value }) => {
116 conn.with_status(201)
117 .with_body(format!("{} is {}", key, value))
118 .halt()
119 }
120
121 Err(_) => conn.with_status(422).with_body("nope").halt()
122 }
123 }
124
125 # use trillium_testing::prelude::*;
126 assert_response!(
127 post("/")
128 .with_request_body(r#"key=name&value=trillium"#)
129 .with_request_header("content-type", "application/x-www-form-urlencoded")
130 .on(&handler),
131 Status::Created,
132 r#"name is trillium"#,
133 );
134
135 assert_response!(
136 post("/")
137 .with_request_body(r#"name=trillium"#)
138 .with_request_header("content-type", "application/x-www-form-urlencoded")
139 .on(&handler),
140 Status::UnprocessableEntity,
141 r#"nope"#,
142 );
143
144
145 ```
146
147 */
148 async fn deserialize<T>(&mut self) -> Result<T>
149 where
150 T: DeserializeOwned;
151
152 /// Deserializes json without any Accepts header content negotiation
153 async fn deserialize_json<T>(&mut self) -> Result<T>
154 where
155 T: DeserializeOwned;
156
157 /// Serializes the provided body using Accepts header content negotiation
158 async fn serialize<T>(&mut self, body: &T) -> Result<()>
159 where
160 T: Serialize + Sync;
161
162 /// Returns a parsed content type for this conn.
163 ///
164 /// Note that this function considers a missing content type an error of variant
165 /// [`Error::MissingContentType`].
166 fn content_type(&self) -> Result<Mime>;
167}
168
169#[trillium::async_trait]
170impl ApiConnExt for Conn {
171 fn with_json(mut self, response: &impl Serialize) -> Self {
172 match serde_json::to_string(&response) {
173 Ok(body) => {
174 if self.status().is_none() {
175 self.set_status(Status::Ok)
176 }
177
178 self.response_headers_mut()
179 .try_insert(ContentType, "application/json");
180
181 self.with_body(body)
182 }
183
184 Err(error) => self.with_state(Error::from(error)),
185 }
186 }
187
188 async fn deserialize<T>(&mut self) -> Result<T>
189 where
190 T: DeserializeOwned,
191 {
192 let body = self.request_body_string().await?;
193 let content_type = self.content_type()?;
194 let suffix_or_subtype = content_type
195 .suffix()
196 .unwrap_or_else(|| content_type.subtype())
197 .as_str();
198 match suffix_or_subtype {
199 "json" => {
200 let json_deserializer = &mut serde_json::Deserializer::from_str(&body);
201 Ok(serde_path_to_error::deserialize::<_, T>(json_deserializer)?)
202 }
203
204 #[cfg(feature = "forms")]
205 "x-www-form-urlencoded" => {
206 let body = form_urlencoded::parse(body.as_bytes());
207 let deserializer = serde_urlencoded::Deserializer::new(body);
208 Ok(serde_path_to_error::deserialize::<_, T>(deserializer)?)
209 }
210
211 _ => Err(Error::UnsupportedMimeType {
212 mime_type: content_type.to_string(),
213 }),
214 }
215 }
216
217 fn content_type(&self) -> Result<Mime> {
218 let header_str = self
219 .request_headers()
220 .get_str(ContentType)
221 .ok_or(Error::MissingContentType)?;
222
223 header_str.parse().map_err(|_| Error::UnsupportedMimeType {
224 mime_type: header_str.into(),
225 })
226 }
227
228 async fn deserialize_json<T>(&mut self) -> Result<T>
229 where
230 T: DeserializeOwned,
231 {
232 let content_type = self.content_type()?;
233 let suffix_or_subtype = content_type
234 .suffix()
235 .unwrap_or_else(|| content_type.subtype())
236 .as_str();
237 if suffix_or_subtype != "json" {
238 return Err(Error::UnsupportedMimeType {
239 mime_type: content_type.to_string(),
240 });
241 }
242
243 log::debug!("extracting json");
244 let body = self.request_body_string().await?;
245 let json_deserializer = &mut serde_json::Deserializer::from_str(&body);
246 Ok(serde_path_to_error::deserialize::<_, T>(json_deserializer)?)
247 }
248
249 async fn serialize<T>(&mut self, body: &T) -> Result<()>
250 where
251 T: Serialize + Sync,
252 {
253 let accept = self
254 .request_headers()
255 .get_str(Accept)
256 .unwrap_or("*/*")
257 .split(',')
258 .map(|s| s.trim())
259 .find_map(acceptable_mime_type);
260
261 match accept {
262 Some(AcceptableMime::Json) => {
263 self.set_body(serde_json::to_string(body)?);
264 self.response_headers_mut()
265 .insert(ContentType, "application/json");
266 Ok(())
267 }
268
269 #[cfg(feature = "forms")]
270 Some(AcceptableMime::Form) => {
271 self.set_body(serde_urlencoded::to_string(body)?);
272 self.response_headers_mut()
273 .insert(ContentType, "application/x-www-form-urlencoded");
274 Ok(())
275 }
276
277 None => Err(Error::FailureToNegotiateContent),
278 }
279 }
280}
281
282enum AcceptableMime {
283 Json,
284 #[cfg(feature = "forms")]
285 Form,
286}
287
288fn acceptable_mime_type(mime: &str) -> Option<AcceptableMime> {
289 let mime: Mime = mime.parse().ok()?;
290 let suffix_or_subtype = mime.suffix().unwrap_or_else(|| mime.subtype()).as_str();
291 match suffix_or_subtype {
292 "*" | "json" => Some(AcceptableMime::Json),
293
294 #[cfg(feature = "forms")]
295 "x-www-form-urlencoded" => Some(AcceptableMime::Form),
296
297 _ => None,
298 }
299}