Skip to main content

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}