arpy_actix/
http.rs

1//! Building blocks for writing HTTP handlers.
2//!
3//! Try using [`RpcApp::http_rpc_route`] first, and if that doesn't give
4//! enough control, use the building blocks in this module.
5//!
6//! [`RpcApp::http_rpc_route`]: crate::RpcApp::http_rpc_route
7use std::{convert::identity, pin::Pin, str::FromStr, sync::Arc};
8
9use actix_web::{
10    body::BoxBody,
11    error::{self, ErrorBadRequest, ErrorNotAcceptable, ErrorUnsupportedMediaType},
12    http::header::{HeaderValue, ACCEPT, CONTENT_TYPE},
13    web::Bytes,
14    FromRequest, HttpRequest, HttpResponse, Responder,
15};
16use arpy::{FnRemote, MimeType};
17use arpy_server::FnRemoteBody;
18use futures::Future;
19use serde::Serialize;
20
21/// An extractor for RPC requests.
22///
23/// When you need more control over the handler than [`RpcApp::http_rpc_route`]
24/// gives, you can implement your own RPC handler. Use this to extract an RPC
25/// request in your handler implementation. See [`actix_web::Handler`] and
26/// [`actix_web::FromRequest`] for more details.
27///
28/// # Example
29///
30/// ```
31#[doc = include_doc::function_body!("tests/doc.rs", extractor_example, [my_handler, MyAdd])]
32/// ```
33/// 
34/// [`RpcApp::http_rpc_route`]: crate::RpcApp::http_rpc_route
35pub struct ArpyRequest<T>(pub T);
36
37impl<Args: FnRemote> FromRequest for ArpyRequest<Args> {
38    type Error = error::Error;
39    type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
40
41    fn from_request(req: &HttpRequest, payload: &mut actix_web::dev::Payload) -> Self::Future {
42        let content_type = mime_type(req.headers().get(CONTENT_TYPE));
43        let bytes = Bytes::from_request(req, payload);
44
45        Box::pin(async move {
46            let body = bytes.await?;
47            let body = body.as_ref();
48
49            let args: Args = match content_type? {
50                MimeType::Cbor => ciborium::de::from_reader(body).map_err(ErrorBadRequest)?,
51                MimeType::Json => serde_json::from_slice(body).map_err(ErrorBadRequest)?,
52                MimeType::XwwwFormUrlencoded => {
53                    serde_urlencoded::from_bytes(body).map_err(ErrorBadRequest)?
54                }
55            };
56
57            Ok(ArpyRequest(args))
58        })
59    }
60}
61
62/// A responder for RPC requests.
63///
64/// Use this to construct a response for an RPC request handler when you need
65/// more control than [`RpcApp::http_rpc_route`] gives. See
66/// [`actix_web::Responder`] for more details, and [`ArpyRequest`] for an
67/// example.
68///
69/// [`RpcApp::http_rpc_route`]: crate::RpcApp::http_rpc_route
70pub struct ArpyResponse<T>(pub T);
71
72impl<T> Responder for ArpyResponse<T>
73where
74    T: Serialize,
75{
76    type Body = BoxBody;
77
78    fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
79        try_respond_to(self.0, req).map_or_else(|e| e.error_response(), identity)
80    }
81}
82
83fn try_respond_to<T>(response: T, req: &HttpRequest) -> Result<HttpResponse, error::Error>
84where
85    T: Serialize,
86{
87    let response_type = mime_type(req.headers().get(ACCEPT))?;
88
89    let body = match response_type {
90        MimeType::Cbor => {
91            let mut response_body = Vec::new();
92
93            ciborium::ser::into_writer(&response, &mut response_body).map_err(ErrorBadRequest)?;
94            BoxBody::new(response_body)
95        }
96        MimeType::Json => BoxBody::new(serde_json::to_vec(&response).map_err(ErrorBadRequest)?),
97        MimeType::XwwwFormUrlencoded => BoxBody::new(serde_urlencoded::to_string(&response)?),
98    };
99
100    Ok(HttpResponse::Ok()
101        .content_type(response_type.as_str())
102        .body(body))
103}
104
105/// An Actix handler for RPC requests.
106///
107/// Use this when you want more control over the route than
108/// [`RpcApp::http_rpc_route`] gives.
109///
110/// # Example
111///
112/// ```
113#[doc = include_doc::function_body!("tests/doc.rs", router_example, [my_add, MyAdd])]
114/// ```
115/// 
116/// [`RpcApp::http_rpc_route`]: crate::RpcApp::http_rpc_route
117pub async fn handler<F, Args>(f: Arc<F>, ArpyRequest(args): ArpyRequest<Args>) -> impl Responder
118where
119    F: FnRemoteBody<Args>,
120    Args: FnRemote,
121{
122    ArpyResponse(f.run(args).await)
123}
124
125fn mime_type(header_value: Option<&HeaderValue>) -> Result<MimeType, error::Error> {
126    if let Some(accept) = header_value {
127        let accept = accept.to_str().map_err(ErrorNotAcceptable)?;
128        MimeType::from_str(accept)
129            .map_err(|_| ErrorUnsupportedMediaType(format!("Unsupport mime type '{accept}'")))
130    } else {
131        Ok(MimeType::Cbor)
132    }
133}