1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! Building blocks for writing HTTP handlers.
//!
//! Try using [`RpcApp::http_rpc_route`] first, and if that doesn't give
//! enough control, use the building blocks in this module.
//!
//! [`RpcApp::http_rpc_route`]: crate::RpcApp::http_rpc_route
use std::{convert::identity, pin::Pin, str::FromStr, sync::Arc};

use actix_web::{
    body::BoxBody,
    error::{self, ErrorBadRequest, ErrorNotAcceptable, ErrorUnsupportedMediaType},
    http::header::{HeaderValue, ACCEPT, CONTENT_TYPE},
    web::Bytes,
    FromRequest, HttpRequest, HttpResponse, Responder,
};
use arpy::{FnRemote, MimeType};
use arpy_server::FnRemoteBody;
use futures::Future;
use serde::Serialize;

/// An extractor for RPC requests.
///
/// When you need more control over the handler than [`RpcApp::http_rpc_route`]
/// gives, you can implement your own RPC handler. Use this to extract an RPC
/// request in your handler implementation. See [`actix_web::Handler`] and
/// [`actix_web::FromRequest`] for more details.
///
/// # Example
///
/// ```
#[doc = include_doc::function_body!("tests/doc.rs", extractor_example, [my_handler, MyAdd])]
/// ```
/// 
/// [`RpcApp::http_rpc_route`]: crate::RpcApp::http_rpc_route
pub struct ArpyRequest<T>(pub T);

impl<Args: FnRemote> FromRequest for ArpyRequest<Args> {
    type Error = error::Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;

    fn from_request(req: &HttpRequest, payload: &mut actix_web::dev::Payload) -> Self::Future {
        let content_type = mime_type(req.headers().get(CONTENT_TYPE)).unwrap();
        let bytes = Bytes::from_request(req, payload);

        Box::pin(async move {
            let body = bytes.await?;
            let body = body.as_ref();

            let args: Args = match content_type {
                MimeType::Cbor => ciborium::de::from_reader(body).map_err(ErrorBadRequest)?,
                MimeType::Json => serde_json::from_slice(body).map_err(ErrorBadRequest)?,
                MimeType::XwwwFormUrlencoded => {
                    serde_urlencoded::from_bytes(body).map_err(ErrorBadRequest)?
                }
            };

            Ok(ArpyRequest(args))
        })
    }
}

/// A responder for RPC requests.
///
/// Use this to construct a response for an RPC request handler when you need
/// more control than [`RpcApp::http_rpc_route`] gives. See
/// [`actix_web::Responder`] for more details, and [`ArpyRequest`] for an
/// example.
///
/// [`RpcApp::http_rpc_route`]: crate::RpcApp::http_rpc_route
pub struct ArpyResponse<T>(pub T);

impl<T> Responder for ArpyResponse<T>
where
    T: Serialize,
{
    type Body = BoxBody;

    fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
        try_respond_to(self.0, req).map_or_else(|e| e.error_response(), identity)
    }
}

fn try_respond_to<T>(response: T, req: &HttpRequest) -> Result<HttpResponse, error::Error>
where
    T: Serialize,
{
    let response_type = mime_type(req.headers().get(ACCEPT))?;

    let body = match response_type {
        MimeType::Cbor => {
            let mut response_body = Vec::new();

            ciborium::ser::into_writer(&response, &mut response_body).map_err(ErrorBadRequest)?;
            BoxBody::new(response_body)
        }
        MimeType::Json => BoxBody::new(serde_json::to_vec(&response).map_err(ErrorBadRequest)?),
        MimeType::XwwwFormUrlencoded => BoxBody::new(serde_urlencoded::to_string(&response)?),
    };

    Ok(HttpResponse::Ok()
        .content_type(response_type.as_str())
        .body(body))
}

/// An Actix handler for RPC requests.
///
/// Use this when you want more control over the route than
/// [`RpcApp::http_rpc_route`] gives.
///
/// # Example
///
/// ```
#[doc = include_doc::function_body!("tests/doc.rs", router_example, [my_add, MyAdd])]
/// ```
/// 
/// [`RpcApp::http_rpc_route`]: crate::RpcApp::http_rpc_route
pub async fn handler<F, Args>(f: Arc<F>, ArpyRequest(args): ArpyRequest<Args>) -> impl Responder
where
    F: FnRemoteBody<Args>,
    Args: FnRemote,
{
    ArpyResponse(f.run(args).await)
}

fn mime_type(header_value: Option<&HeaderValue>) -> Result<MimeType, error::Error> {
    if let Some(accept) = header_value {
        let accept = accept.to_str().map_err(ErrorNotAcceptable)?;
        MimeType::from_str(accept)
            .map_err(|_| ErrorUnsupportedMediaType(format!("Unsupport mime type '{accept}'")))
    } else {
        Ok(MimeType::Cbor)
    }
}