arpy_reqwest/
lib.rs

1//! Reqwest client for Arpy.
2//!
3//! See [`Connection`] for an example.
4use arpy::{FnRemote, MimeType, RpcClient};
5use async_trait::async_trait;
6use reqwest::{
7    header::{HeaderValue, ACCEPT, CONTENT_TYPE},
8    Client,
9};
10use thiserror::Error;
11
12/// A connection to the server.
13///
14/// # Example
15///
16/// ```
17/// # use arpy::{FnRemote, MsgId};
18/// # use reqwest::Client;
19/// # use serde::{Deserialize, Serialize};
20/// # use arpy_reqwest::Connection;
21/// #
22/// #[derive(MsgId, Serialize, Deserialize, Debug)]
23/// struct MyAdd(u32, u32);
24///
25/// impl FnRemote for MyAdd {
26///     type Output = u32;
27/// }
28///
29/// async {
30///     let mut conn = Connection::new(&Client::new(), "http://127.0.0.1:9090/api");
31///     let result = MyAdd(1, 2).call(&mut conn).await.unwrap();
32///
33///     println!("1 + 2 = {result}");
34/// };
35/// ```
36#[derive(Clone)]
37pub struct Connection {
38    client: Client,
39    url: String,
40}
41
42impl Connection {
43    /// Constructor.
44    ///
45    /// This stores [`Client`] for connection pooling. `url` is the base url of
46    /// the server, and will have [`MsgId::ID`] appended for each RPC.
47    ///
48    /// [`MsgId::ID`]: arpy::protocol::MsgId::ID
49    pub fn new(client: &Client, url: impl Into<String>) -> Self {
50        Self {
51            client: client.clone(),
52            url: url.into(),
53        }
54    }
55}
56
57/// The errors that can happen during an RPC.
58///
59/// Note; This may contain sensitive information. In particular,
60/// [`reqwest::Error`] may contain URLs, and `DeserializeResult` may contain
61/// argument names/values.
62#[derive(Error, Debug)]
63pub enum Error {
64    #[error("Couldn't deserialize result: {0}")]
65    DeserializeResult(String),
66    #[error("Couldn't send request: {0}")]
67    Send(reqwest::Error),
68    #[error("Couldn't receive response: {0}")]
69    Receive(reqwest::Error),
70    #[error("HTTP error code: {0}")]
71    Http(reqwest::StatusCode),
72    #[error("Invalid response 'content_type'")]
73    UnknownContentType(HeaderValue),
74}
75
76#[async_trait(?Send)]
77impl RpcClient for Connection {
78    type Error = Error;
79
80    async fn call<Args>(&self, args: Args) -> Result<Args::Output, Self::Error>
81    where
82        Args: FnRemote,
83    {
84        let content_type = MimeType::Cbor;
85        let mut body = Vec::new();
86
87        ciborium::ser::into_writer(&args, &mut body).unwrap();
88
89        let result = self
90            .client
91            .post(format!("{}/{}", self.url, Args::ID))
92            .header(CONTENT_TYPE, content_type.as_str())
93            .header(ACCEPT, content_type.as_str())
94            .body(body)
95            .send()
96            .await
97            .map_err(Error::Send)?;
98
99        let status = result.status();
100
101        if !status.is_success() {
102            return Err(Error::Http(status));
103        }
104
105        if let Some(result_type) = result.headers().get(CONTENT_TYPE) {
106            if result_type != HeaderValue::from_static(content_type.as_str()) {
107                return Err(Error::UnknownContentType(result_type.clone()));
108            }
109        }
110
111        let result_bytes = result.bytes().await.map_err(Error::Receive)?;
112        let result: Args::Output = ciborium::de::from_reader(result_bytes.as_ref())
113            .map_err(|e| Error::DeserializeResult(e.to_string()))?;
114
115        Ok(result)
116    }
117}