1use 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#[derive(Clone)]
37pub struct Connection {
38 client: Client,
39 url: String,
40}
41
42impl Connection {
43 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#[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}