actix_multiresponse/
lib.rs

1//! `actix-multiresponse` intents to allow supporting multiple response/request data formats depending on the
2//! `Content-Type` and `Accept` headers.
3//!
4//! ### Supported formats
5//! - Json
6//! - Protobuf
7//!
8//! All formats can be enabled with feature flags. At least one format should be enabled to make this library useful.
9//!
10//! ### Example
11//! ```
12//!     use prost_derive::Message;
13//!     use serde_derive::{Deserialize, Serialize};
14//!     use actix_multiresponse::Payload;
15//!
16//!     #[derive(Deserialize, Serialize, Message, Clone)]
17//!     struct TestPayload {
18//!         #[prost(string, tag = "1")]
19//!         foo: String,
20//!         #[prost(int64, tag = "2")]
21//!         bar: i64,
22//!     }
23//!
24//!     async fn responder(payload: Payload<TestPayload>) -> Payload<TestPayload> {
25//!         payload
26//!     }
27//! ```
28
29use crate::error::PayloadError;
30pub use crate::headers::ContentType;
31
32use actix_web::body::BoxBody;
33use actix_web::{FromRequest, HttpRequest, HttpResponse, Responder};
34use actix_web::http::StatusCode;
35
36use std::future::Future;
37use std::ops::{Deref, DerefMut};
38use std::pin::Pin;
39
40use futures_util::StreamExt;
41use thiserror::Error;
42
43mod error;
44mod headers;
45
46#[cfg(feature = "protobuf")]
47pub trait ProtobufSupport: prost::Message {}
48#[cfg(not(feature = "protobuf"))]
49pub trait ProtobufSupport {}
50
51#[cfg(feature = "protobuf")]
52impl<T: prost::Message> ProtobufSupport for T {}
53#[cfg(not(feature = "protobuf"))]
54impl<T> ProtobufSupport for T {}
55
56#[cfg(any(feature = "json", feature = "xml"))]
57pub trait SerdeSupportDeserialize: serde::de::DeserializeOwned {}
58#[cfg(not(any(feature = "json", feature = "xml")))]
59pub trait SerdeSupportDeserialize {}
60
61#[cfg(any(feature = "json", feature = "xml"))]
62impl<T: serde::de::DeserializeOwned> SerdeSupportDeserialize for T {}
63#[cfg(not(any(feature = "json", feature = "xml")))]
64impl<T> SerdeSupportDeserialize for T {}
65
66#[cfg(any(feature = "json", feature = "xml"))]
67pub trait SerdeSupportSerialize: serde::Serialize {}
68#[cfg(not(any(feature = "json", feature = "xml")))]
69pub trait SerdeSupportSerialize {}
70
71#[cfg(any(feature = "json", feature = "xml"))]
72impl<T: serde::Serialize> SerdeSupportSerialize for T {}
73#[cfg(not(any(feature = "json", feature = "xml")))]
74impl<T> SerdeSupportSerialize for T {}
75
76/// Payload wrapper which facilitates tje (de)serialization.
77/// This type can be used as both the request and response payload type.
78///
79/// The proper format is chosen based on the `Content-Type` and `Accept` headers.
80/// When deserializing only the `Content-Type` header is used.
81/// When serializing, the `Accept` header is checked first, if it is missing
82/// the `Content-Type` header will be used. If both are missing the payload will
83/// default to `JSON`.
84///
85/// # Errors
86///
87/// When the `Content-Type` header is not provided in the request or is invalid, this will return a HTTP 400 error.
88/// If the `Content-Type` header, or `Accept` header is invalid when responding this will return a HTTP 400 error,
89/// however this is *not* done if both headers are missing on response.
90///
91/// # Panics
92///
93/// If during serializing no format is enabled
94#[derive(Debug)]
95pub struct Payload<T: 'static + Default + Clone>(pub T);
96
97impl<T: 'static + Default + Clone> Deref for Payload<T> {
98    type Target = T;
99
100    fn deref(&self) -> &Self::Target {
101        &self.0
102    }
103}
104
105impl<T: 'static + Default + Clone> DerefMut for Payload<T> {
106    fn deref_mut(&mut self) -> &mut Self::Target {
107        &mut self.0
108    }
109}
110
111impl<T: 'static + SerdeSupportDeserialize + ProtobufSupport + Default + Clone> FromRequest
112    for Payload<T>
113{
114    type Error = PayloadError;
115    type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
116
117    fn from_request(req: &HttpRequest, payload: &mut actix_web::dev::Payload) -> Self::Future {
118        let req = req.clone();
119        #[allow(unused)]
120        let mut payload = payload.take();
121
122        Box::pin(async move {
123            let mut payload_bytes = Vec::new();
124            while let Some(Ok(b)) = payload.next().await {
125                payload_bytes.append(&mut b.to_vec())
126            }
127
128            let content_type = ContentType::from_request_content_type(&req);
129            if content_type.eq(&ContentType::Other) {
130                return Err(PayloadError::InvalidContentType)
131            }
132
133            let this = Payload::deserialize(&payload_bytes, content_type)?;
134
135            Ok(this)
136        })
137    }
138}
139
140impl<T: ProtobufSupport + SerdeSupportSerialize + Default + Clone> Responder for Payload<T> {
141    type Body = BoxBody;
142
143    fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
144        // Determine the response format
145        // - Check if the Accepts header was set to a valid value, use that
146        // - If not, check the Content-Type header, if that is valid, use that
147        // - Else, default to Json
148        let content_type = ContentType::from_request_accepts(req);
149        let content_type = if content_type.eq(&ContentType::Other) {
150            let content_type_second = ContentType::from_request_content_type(req);
151            if content_type_second.eq(&ContentType::Other) {
152                ContentType::default()
153            } else {
154                content_type_second
155            }
156        } else {
157            content_type
158        };
159
160        let serialized = match self.serialize(content_type.clone()) {
161            Ok(x) => x,
162            Err(e) => {
163                return HttpResponse::build(StatusCode::INTERNAL_SERVER_ERROR)
164                    .body(e.to_string());
165            }
166        };
167
168        let mut response = HttpResponse::build(StatusCode::OK);
169        match content_type {
170            #[cfg(feature = "json")]
171            ContentType::Json => response.insert_header(("Content-Type", "application/json")),
172            #[cfg(feature = "protobuf")]
173            ContentType::Protobuf => response.insert_header(("Content-Type", "application/protobuf")),
174            #[cfg(feature = "xml")]
175            ContentType::Xml => response.insert_header(("Content-Type", "application/xml")),
176            ContentType::Other => panic!("Must have ast least one format feature enabled.")
177        };
178
179        response.body(serialized)
180    }
181}
182
183#[derive(Debug, Error)]
184pub enum SerializeError {
185    #[cfg(feature = "json")]
186    #[error("Failed to serialize to JSON: {0}")]
187    SerdeJson(#[from] serde_json::Error),
188    #[cfg(feature = "json")]
189    #[error("Failed to encode to protobuf: {0}")]
190    Prost(String),
191    #[cfg(feature = "xml")]
192    #[error("Failed to serialize to XML: {0}")]
193    QuickXml(#[from] quick_xml::DeError),
194    #[error("Unable to serialize")]
195    Unserializable,
196}
197
198#[derive(Debug, Error)]
199pub enum DeserializeError {
200    #[cfg(feature = "json")]
201    #[error("Failed to deserialize from JSON: {0}")]
202    SerdeJson(#[from] serde_json::Error),
203    #[cfg(feature = "protobuf")]
204    #[error("Failed to decode from protobuf: {0}")]
205    Prost(String),
206    #[cfg(feature = "xml")]
207    #[error("Failed to deserialize from XML: {0}")]
208    Xml(#[from] quick_xml::DeError),
209    #[error("Unable to deserialize")]
210    Undeserializable
211}
212
213impl<T: ProtobufSupport + SerdeSupportSerialize + Default + Clone> Payload<T> {
214    pub fn serialize(&self, content_type: ContentType) -> Result<Vec<u8>, SerializeError> {
215        match content_type {
216            #[cfg(feature = "json")]
217            ContentType::Json => {
218                let json = serde_json::to_string_pretty(&self.0)?;
219                Ok(json.into_bytes())
220            },
221            #[cfg(feature = "protobuf")]
222            ContentType::Protobuf => {
223                let mut protobuf = Vec::new();
224                self.0.encode(&mut protobuf)
225                    .map_err(|e| SerializeError::Prost(e.to_string()))?;
226                Ok(protobuf)
227            },
228            #[cfg(feature = "xml")]
229            ContentType::Xml => {
230                let xml = quick_xml::se::to_string(&self.0)?;
231                Ok(xml.into_bytes())
232            }
233            ContentType::Other => Err(SerializeError::Unserializable)
234        }
235    }
236}
237
238impl<T: ProtobufSupport + SerdeSupportDeserialize + Default + Clone> Payload<T> {
239    pub fn deserialize(body: &[u8], content_type: ContentType) -> Result<Self, DeserializeError> {
240        match content_type {
241            #[cfg(feature = "json")]
242            ContentType::Json => {
243                let payload: T = serde_json::from_slice(body)?;
244                Ok(Self(payload))
245            },
246            #[cfg(feature = "protobuf")]
247            ContentType::Protobuf => {
248                let payload = T::decode(body)
249                    .map_err(|e| DeserializeError::Prost(e.to_string()))?;
250                Ok(Self(payload))
251            },
252            #[cfg(feature = "xml")]
253            ContentType::Xml => {
254                let payload: T = quick_xml::de::from_reader(body)?;
255                Ok(Self(payload) )
256            }
257            ContentType::Other => Err(DeserializeError::Undeserializable)
258        }
259    }
260
261}
262
263#[cfg(test)]
264mod test {
265    use super::*;
266    use prost_derive::Message;
267    use serde_derive::{Deserialize, Serialize};
268
269    #[derive(Deserialize, Serialize, Message, Clone)]
270    struct TestPayload {
271        #[prost(string, tag = "1")]
272        foo: String,
273        #[prost(int64, tag = "2")]
274        bar: i64,
275    }
276
277    impl TestPayload {
278        #[allow(unused)]
279        fn json() -> String {
280            serde_json::to_string_pretty(&Self::default()).unwrap()
281        }
282
283        #[allow(unused)]
284        fn protobuf() -> Vec<u8> {
285            use prost::Message;
286            Self::default().encode_to_vec()
287        }
288    }
289
290    #[allow(unused)]
291    async fn responder(payload: Payload<TestPayload>) -> Payload<TestPayload> {
292        payload
293    }
294
295    #[allow(unused)]
296    macro_rules! setup {
297        () => {
298            actix_web::test::init_service(
299                actix_web::App::new().route("/", actix_web::web::get().to(responder)),
300            )
301            .await
302        };
303    }
304
305    #[allow(unused)]
306    macro_rules! body {
307        ($res:expr) => {
308            actix_web::body::to_bytes($res.into_body()).await.unwrap()
309        };
310    }
311
312    #[actix_macros::test]
313    #[cfg(feature = "json")]
314    async fn test_json_req_json_res() {
315        let app = setup!();
316        let req = actix_web::test::TestRequest::default()
317            .insert_header(("Content-Type", "application/json"))
318            .set_payload(TestPayload::json())
319            .to_request();
320        let resp = actix_web::test::call_service(&app, req).await;
321
322        assert!(resp.status().is_success());
323
324        let body = body!(resp);
325        assert_eq!(
326            TestPayload::json(),
327            String::from_utf8(body.to_vec()).unwrap()
328        );
329    }
330
331    #[actix_macros::test]
332    #[cfg(all(feature = "json", feature = "protobuf"))]
333    async fn test_json_req_protobuf_response() {
334        let app = setup!();
335        let req = actix_web::test::TestRequest::default()
336            .insert_header(("Content-Type", "application/json"))
337            .insert_header(("Accept", "application/protobuf"))
338            .set_payload(TestPayload::json())
339            .to_request();
340        let resp = actix_web::test::call_service(&app, req).await;
341
342        assert!(resp.status().is_success());
343
344        let body = body!(resp);
345        assert_eq!(TestPayload::protobuf(), body.to_vec());
346    }
347
348    #[actix_macros::test]
349    #[cfg(all(feature = "json", feature = "protobuf"))]
350    async fn test_protobuf_req_json_response() {
351        let app = setup!();
352        let req = actix_web::test::TestRequest::default()
353            .insert_header(("Accept", "application/json"))
354            .insert_header(("Content-Type", "application/protobuf"))
355            .set_payload(TestPayload::protobuf())
356            .to_request();
357        let resp = actix_web::test::call_service(&app, req).await;
358
359        assert!(resp.status().is_success());
360
361        let body = body!(resp);
362        assert_eq!(
363            TestPayload::json(),
364            String::from_utf8(body.to_vec()).unwrap()
365        );
366    }
367
368    #[actix_macros::test]
369    #[cfg(feature = "protobuf")]
370    async fn test_protobuf_req_protobuf_response() {
371        let app = setup!();
372        let req = actix_web::test::TestRequest::default()
373            .insert_header(("Accept", "application/protobuf"))
374            .insert_header(("Content-Type", "application/protobuf"))
375            .set_payload(TestPayload::protobuf())
376            .to_request();
377        let resp = actix_web::test::call_service(&app, req).await;
378
379        assert!(resp.status().is_success());
380
381        let body = body!(resp);
382        assert_eq!(TestPayload::protobuf(), body.to_vec());
383    }
384}