impulse_utils/
responses.rs

1//! Implementation of utilities for working with responses in `salvo` and `reqwest`.
2
3#[cfg(feature = "salvo")]
4#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
5use salvo::http::HeaderValue;
6
7#[cfg(feature = "salvo")]
8#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
9use salvo::hyper::header::CONTENT_TYPE;
10
11#[cfg(feature = "salvo")]
12#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
13use salvo::oapi::{EndpointOutRegister, ToSchema};
14
15#[cfg(feature = "salvo")]
16#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
17use salvo::{Depot, Request, Response};
18
19#[cfg(feature = "salvo")]
20#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
21use salvo::Writer as ServerResponseWriter;
22
23#[cfg(feature = "salvo")]
24#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
25use salvo::fs::NamedFile;
26
27/// Macro to define the function that called the response.
28#[cfg(feature = "salvo")]
29#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
30#[macro_export]
31macro_rules! fn_name {
32  () => {{
33    fn f() {}
34    fn type_name_of<T>(_: T) -> &'static str {
35      std::any::type_name::<T>()
36    }
37    let name = type_name_of(f);
38
39    // For `#[endpoint]` path can be shortened as follows:
40    match name[..name.len() - 3].rsplit("::").nth(2) {
41      Some(el) => el,
42      None => &name[..name.len() - 3],
43    }
44  }};
45}
46
47/// Macro for automating `EndpointOutRegister` implementations (for simple types)
48#[cfg(feature = "salvo")]
49#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
50macro_rules! impl_oapi_endpoint_out {
51  ($t:tt, $c:expr) => {
52    #[cfg(feature = "salvo")]
53    impl EndpointOutRegister for $t {
54      #[inline]
55      fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
56        operation.responses.insert(
57          "200",
58          salvo::oapi::Response::new("Ok").add_content($c, String::to_schema(components)),
59        );
60      }
61    }
62  };
63}
64
65/// Macro for automating `EndpointOutRegister` implementations (for template types)
66#[cfg(feature = "salvo")]
67#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
68macro_rules! impl_oapi_endpoint_out_t {
69  ($t:tt, $c:expr) => {
70    #[cfg(feature = "salvo")]
71    impl<T> EndpointOutRegister for $t<T> {
72      #[inline]
73      fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
74        operation.responses.insert(
75          "200",
76          salvo::oapi::Response::new("Ok").add_content($c, String::to_schema(components)),
77        );
78      }
79    }
80  };
81}
82
83#[cfg(feature = "salvo")]
84#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
85#[allow(async_fn_in_trait)]
86/// Trait that utilizes only mutable reference to `Response` and makes no need for `Request`/`Depot`.
87pub trait ExplicitServerWrite {
88  /// Write an actual response in a `Response` object.
89  async fn explicit_write(self, res: &mut Response);
90}
91
92/// Sends 200 without data.
93#[cfg(feature = "salvo")]
94#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
95pub struct OK(pub &'static str);
96
97#[cfg(feature = "salvo")]
98#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
99impl_oapi_endpoint_out!(OK, "text/plain");
100
101/// Returns empty `200 OK` response.
102#[cfg(feature = "salvo")]
103#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
104#[macro_export]
105macro_rules! ok {
106  () => {
107    Ok::<impulse_utils::responses::OK, impulse_utils::errors::ServerError>(impulse_utils::responses::OK(
108      $crate::fn_name!(),
109    ))
110  };
111}
112
113#[cfg(feature = "salvo")]
114#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
115impl ExplicitServerWrite for OK {
116  async fn explicit_write(self, res: &mut Response) {
117    res.status_code(salvo::http::StatusCode::OK);
118    res.render("");
119    tracing::trace!("[{}] => Received and sent result 200", self.0);
120  }
121}
122
123#[cfg(feature = "salvo")]
124#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
125#[salvo::async_trait]
126impl ServerResponseWriter for OK {
127  async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
128    ExplicitServerWrite::explicit_write(self, res).await
129  }
130}
131
132/// Sends 200 and plain text.
133#[cfg(feature = "salvo")]
134#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
135#[derive(Debug)]
136pub struct Plain(pub String, pub &'static str);
137
138#[cfg(feature = "salvo")]
139#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
140#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
141impl_oapi_endpoint_out!(Plain, "text/plain");
142
143/// Returns given plain text.
144#[cfg(feature = "salvo")]
145#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
146#[macro_export]
147macro_rules! plain {
148  ($plain_text:expr) => {
149    Ok::<impulse_utils::responses::Plain, impulse_utils::errors::ServerError>(impulse_utils::responses::Plain(
150      $plain_text,
151      $crate::fn_name!(),
152    ))
153  };
154}
155
156#[cfg(feature = "salvo")]
157#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
158impl ExplicitServerWrite for Plain {
159  async fn explicit_write(self, res: &mut Response) {
160    res.status_code(salvo::http::StatusCode::OK);
161    res.render(&self.0);
162    tracing::trace!("[{}] => Received and sent result 200 with text: {}", self.1, self.0);
163  }
164}
165
166#[cfg(feature = "salvo")]
167#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
168#[salvo::async_trait]
169impl ServerResponseWriter for Plain {
170  async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
171    ExplicitServerWrite::explicit_write(self, res).await
172  }
173}
174
175/// Sends 200 and HTML.
176#[cfg(feature = "salvo")]
177#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
178#[derive(Debug)]
179pub struct Html(pub String, pub &'static str);
180
181#[cfg(feature = "salvo")]
182#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
183impl_oapi_endpoint_out!(Html, "text/html");
184
185/// Returns given HTML code.
186#[cfg(feature = "salvo")]
187#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
188#[macro_export]
189macro_rules! html {
190  ($html_data:expr) => {
191    Ok::<impulse_utils::responses::Html, impulse_utils::errors::ServerError>(impulse_utils::responses::Html(
192      $html_data,
193      $crate::fn_name!(),
194    ))
195  };
196}
197
198#[cfg(feature = "salvo")]
199#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
200impl ExplicitServerWrite for Html {
201  async fn explicit_write(self, res: &mut Response) {
202    res.status_code(salvo::http::StatusCode::OK);
203    res.render(salvo::writing::Text::Html(&self.0));
204    tracing::trace!("[{}] => Received and sent result 200 with HTML", self.1);
205  }
206}
207
208#[cfg(feature = "salvo")]
209#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
210#[salvo::async_trait]
211impl ServerResponseWriter for Html {
212  async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
213    ExplicitServerWrite::explicit_write(self, res).await
214  }
215}
216
217/// Sends 200 and file.
218#[cfg(feature = "salvo")]
219#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
220#[derive(Debug)]
221pub struct File(pub std::path::PathBuf, pub String, pub &'static str);
222
223#[cfg(feature = "salvo")]
224#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
225impl_oapi_endpoint_out!(File, "application/octet-stream");
226
227/// File response.
228///
229/// Usage:
230///
231/// ```rust
232/// use impulse_utils::prelude::*;
233/// use salvo::prelude::*;
234/// use std::path::PathBuf;
235///
236/// pub async fn some_endpoint() -> MResult<File> {
237///   file_upload!(PathBuf::from("filepath.txt"), "Normal file name.txt".to_string())
238/// }
239/// ```
240#[cfg(feature = "salvo")]
241#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
242#[macro_export]
243macro_rules! file_upload {
244  ($filepath:expr, $attached_filename:expr) => {
245    Ok::<impulse_utils::responses::File, impulse_utils::errors::ServerError>(impulse_utils::responses::File(
246      $filepath,
247      $attached_filename,
248      $crate::fn_name!(),
249    ))
250  };
251}
252
253#[cfg(feature = "salvo")]
254#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
255#[salvo::async_trait]
256impl ServerResponseWriter for File {
257  async fn write(self, req: &mut Request, _depot: &mut Depot, res: &mut Response) {
258    res.status_code(salvo::http::StatusCode::OK);
259    res.headers_mut().append(
260      "Cache-Control",
261      HeaderValue::from_static("public, max-age=0, must-revalidate"),
262    );
263    NamedFile::builder(&self.0)
264      .attached_name(&self.1)
265      .use_etag(true)
266      .use_last_modified(true)
267      .send(req.headers(), res)
268      .await;
269    tracing::trace!("[{}] => Received and sent result 200 with file {}", self.2, self.1);
270  }
271}
272
273/// Sends 200 and JSON.
274#[cfg(feature = "salvo")]
275#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
276#[derive(Debug)]
277pub struct Json<T>(pub T, pub &'static str);
278
279#[cfg(feature = "salvo")]
280#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
281impl_oapi_endpoint_out_t!(Json, "application/json");
282
283/// Serializes to JSON and returns given object.
284#[cfg(feature = "salvo")]
285#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
286#[macro_export]
287macro_rules! json {
288  ($json_data:expr) => {
289    Ok::<impulse_utils::responses::Json<_>, impulse_utils::errors::ServerError>(impulse_utils::responses::Json(
290      $json_data,
291      $crate::fn_name!(),
292    ))
293  };
294}
295
296#[cfg(all(feature = "salvo", feature = "mresult"))]
297#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
298impl<T: serde::Serialize + Send> ExplicitServerWrite for Json<T> {
299  async fn explicit_write(self, res: &mut Response) {
300    res.status_code(salvo::http::StatusCode::OK);
301    match sonic_rs::to_string(&self.0) {
302      Ok(s) => {
303        res.headers_mut().insert(
304          CONTENT_TYPE,
305          HeaderValue::from_static("application/json; charset=utf-8"),
306        );
307        tracing::trace!("[{}] => Sending JSON: {:?}", self.1, s.as_str());
308        res.write_body(s).ok();
309        tracing::trace!("[{}] => Received and sent result 200 with JSON", self.1);
310      }
311      Err(e) => {
312        tracing::error!("[{}] => Failed to serialize data: {:?}", e, self.1);
313        crate::prelude::ServerError::from_private(e)
314          .with_public("Failed to serialize data.")
315          .with_500()
316          .explicit_write(res)
317          .await;
318      }
319    }
320  }
321}
322
323#[cfg(all(feature = "salvo", feature = "mresult"))]
324#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
325#[salvo::async_trait]
326impl<T: serde::Serialize + Send> ServerResponseWriter for Json<T> {
327  async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
328    ExplicitServerWrite::explicit_write(self, res).await
329  }
330}
331
332/// Sends 200 and MsgPack.
333#[cfg(feature = "salvo")]
334#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
335#[derive(Debug)]
336pub struct MsgPack<T>(pub T, pub &'static str);
337
338#[cfg(feature = "salvo")]
339#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
340impl_oapi_endpoint_out_t!(MsgPack, "application/msgpack");
341
342/// Serializes to MsgPack and returns given object.
343#[cfg(feature = "salvo")]
344#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
345#[macro_export]
346macro_rules! msgpack {
347  ($msgpack_data:expr) => {
348    Ok::<impulse_utils::responses::MsgPack<_>, impulse_utils::errors::ServerError>(impulse_utils::responses::MsgPack(
349      $msgpack_data,
350      $crate::fn_name!(),
351    ))
352  };
353}
354
355#[cfg(all(feature = "salvo", feature = "mresult"))]
356#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
357impl<T: serde::Serialize + Send> ExplicitServerWrite for MsgPack<T> {
358  async fn explicit_write(self, res: &mut Response) {
359    res.status_code(salvo::http::StatusCode::OK);
360    match rmp_serde::to_vec(&self.0) {
361      Ok(bytes) => {
362        res.headers_mut().insert(
363          CONTENT_TYPE,
364          HeaderValue::from_static("application/msgpack; charset=utf-8"),
365        );
366        tracing::trace!("[{}] => Sending bytes: {:04X?}", self.1, bytes);
367        res.write_body(bytes).ok();
368        tracing::trace!("[{}] => Received and sent result 200 with MsgPack", self.1);
369      }
370      Err(e) => {
371        tracing::error!("[{}] => Failed to serialize data: {:?}", e, self.1);
372        crate::prelude::ServerError::from_private(e)
373          .with_public("Failed to serialize data.")
374          .with_500()
375          .explicit_write(res)
376          .await;
377      }
378    }
379  }
380}
381
382#[cfg(all(feature = "salvo", feature = "mresult"))]
383#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
384#[salvo::async_trait]
385impl<T: serde::Serialize + Send> ServerResponseWriter for MsgPack<T> {
386  async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
387    ExplicitServerWrite::explicit_write(self, res).await
388  }
389}
390
391/// Trait to parse MessagePack responses from `reqwest` library.
392#[cfg(all(feature = "reqwest", feature = "cresult"))]
393#[allow(async_fn_in_trait)]
394pub trait MsgPackResponse {
395  /// Parses MessagePack from body.
396  async fn msgpack<T: serde::de::DeserializeOwned>(self) -> crate::prelude::CResult<T>;
397}
398
399#[cfg(all(feature = "reqwest", feature = "cresult"))]
400impl MsgPackResponse for reqwest::Response {
401  async fn msgpack<T: serde::de::DeserializeOwned>(self) -> crate::prelude::CResult<T> {
402    use crate::errors::ClientError;
403
404    let full = self.bytes().await.map_err(ClientError::from)?;
405    rmp_serde::from_slice(&full).map_err(ClientError::from)
406  }
407}
408
409/// Trait to recover public errors from server.
410#[cfg(all(feature = "reqwest", feature = "cresult"))]
411#[allow(async_fn_in_trait)]
412pub trait CollectServerError
413where
414  Self: Sized,
415{
416  /// Collects server error.
417  async fn collect_server_error(self) -> crate::prelude::CResult<Self>;
418}
419
420#[cfg(all(feature = "reqwest", feature = "cresult"))]
421impl CollectServerError for reqwest::Response {
422  async fn collect_server_error(self) -> crate::prelude::CResult<Self> {
423    use crate::errors::ClientError;
424
425    let status_code = self.status().as_u16();
426    if status_code >= 400 {
427      let ctype = self
428        .headers()
429        .get(reqwest::header::CONTENT_TYPE)
430        .and_then(|v| v.to_str().ok())
431        .and_then(|v| v.split(';').next())
432        .map(|v| v.to_string());
433
434      if ctype.as_ref().is_some_and(|ct| ct.as_str().eq("application/json")) {
435        let err_json = self
436          .json::<crate::errors::ErrorResponse>()
437          .await
438          .map_err(|_| ClientError::from_str(crate::errors::public_msg_from(&Some(status_code))))?;
439        return Err(ClientError::from_str(err_json.err));
440      }
441
442      Err(ClientError::from_str(crate::errors::public_msg_from(&Some(
443        status_code,
444      ))))
445    } else {
446      Ok(self)
447    }
448  }
449}
450
451/// Trait to recover public errors from server.
452#[cfg(all(feature = "reqwest", feature = "mresult"))]
453#[allow(async_fn_in_trait)]
454pub trait RedirectServerError
455where
456  Self: Sized,
457{
458  /// Redirects server error.
459  async fn redirect_server_error(self) -> crate::prelude::MResult<Self>;
460}
461
462#[cfg(all(feature = "reqwest", feature = "mresult"))]
463impl RedirectServerError for reqwest::Response {
464  async fn redirect_server_error(self) -> crate::prelude::MResult<Self> {
465    use crate::errors::ServerError;
466
467    let status_code = self.status();
468    if status_code.as_u16() >= 400 {
469      let ctype = self
470        .headers()
471        .get(reqwest::header::CONTENT_TYPE)
472        .and_then(|v| v.to_str().ok())
473        .and_then(|v| v.split(';').next())
474        .map(|v| v.to_string());
475
476      if ctype.as_ref().is_some_and(|ct| ct.as_str().eq("application/json")) {
477        let err_json = self.json::<crate::errors::ErrorResponse>().await.map_err(|e| {
478          ServerError::from_private(e)
479            .with_public(crate::errors::public_msg_from(&Some(status_code.as_u16())))
480            .with_code(status_code)
481        })?;
482        return Err(ServerError::from_public(err_json.err).with_code(status_code));
483      }
484
485      Err(ServerError::from_public(crate::errors::public_msg_from(&Some(status_code.as_u16()))).with_code(status_code))
486    } else {
487      Ok(self)
488    }
489  }
490}