atrium_xrpc/
lib.rs

1#![doc = include_str!("../README.md")]
2pub mod error;
3mod traits;
4pub mod types;
5
6pub use crate::error::{Error, Result};
7pub use crate::traits::{HttpClient, XrpcClient};
8pub use crate::types::{InputDataOrBytes, OutputDataOrBytes, XrpcRequest};
9pub use http;
10
11#[cfg(test)]
12mod tests {
13    use super::*;
14    use crate::error::{XrpcError, XrpcErrorKind};
15    use crate::{HttpClient, XrpcClient};
16    use http::{Request, Response};
17    #[cfg(target_arch = "wasm32")]
18    use wasm_bindgen_test::*;
19
20    struct DummyClient {
21        status: http::StatusCode,
22        json: bool,
23        body: Vec<u8>,
24    }
25
26    impl HttpClient for DummyClient {
27        async fn send_http(
28            &self,
29            _request: Request<Vec<u8>>,
30        ) -> core::result::Result<
31            Response<Vec<u8>>,
32            Box<dyn std::error::Error + Send + Sync + 'static>,
33        > {
34            let mut builder = Response::builder().status(self.status);
35            if self.json {
36                builder = builder.header(http::header::CONTENT_TYPE, "application/json")
37            }
38            Ok(builder.body(self.body.clone())?)
39        }
40    }
41
42    impl XrpcClient for DummyClient {
43        fn base_uri(&self) -> String {
44            "https://example.com".into()
45        }
46    }
47
48    mod errors {
49        use super::*;
50
51        async fn get_example<T>(xrpc: &T, params: Parameters) -> Result<Output, Error>
52        where
53            T: crate::XrpcClient + Send + Sync,
54        {
55            let response = xrpc
56                .send_xrpc::<_, (), _, _>(&XrpcRequest {
57                    method: http::Method::GET,
58                    nsid: "example".into(),
59                    parameters: Some(params),
60                    input: None,
61                    encoding: None,
62                })
63                .await?;
64            match response {
65                crate::OutputDataOrBytes::Data(data) => Ok(data),
66                _ => Err(crate::Error::UnexpectedResponseType),
67            }
68        }
69
70        #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
71        #[serde(rename_all = "camelCase")]
72        struct Parameters {}
73
74        #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
75        #[serde(rename_all = "camelCase")]
76        struct Output {
77            return_value: i32,
78        }
79
80        #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
81        #[serde(tag = "error", content = "message")]
82        enum Error {
83            InvalidToken(Option<String>),
84            ExpiredToken(Option<String>),
85        }
86
87        #[test]
88        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
89        fn deserialize_xrpc_error() {
90            {
91                let body = r#"{"error":"InvalidToken","message":"Invalid token"}"#;
92                let err = serde_json::from_str::<XrpcErrorKind<_>>(body).expect("deserialize");
93                assert_eq!(
94                    err,
95                    XrpcErrorKind::Custom(Error::InvalidToken(Some(String::from("Invalid token"))))
96                );
97            }
98            {
99                let body = r#"{"error":"ExpiredToken"}"#;
100                let err = serde_json::from_str::<XrpcErrorKind<_>>(body).expect("deserialize");
101                assert_eq!(err, XrpcErrorKind::Custom(Error::ExpiredToken(None)));
102            }
103            {
104                let body = r#"{"error":"Unknown","message":"Something wrong"}"#;
105                let err = serde_json::from_str::<XrpcErrorKind<Error>>(body).expect("deserialize");
106                assert_eq!(
107                    err,
108                    XrpcErrorKind::Undefined(crate::error::ErrorResponseBody {
109                        error: Some(String::from("Unknown")),
110                        message: Some(String::from("Something wrong")),
111                    })
112                );
113            }
114        }
115
116        #[tokio::test]
117        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
118        async fn response_ok() {
119            let client = DummyClient {
120                status: http::StatusCode::OK,
121                json: true,
122                body: r#"{"returnValue":42}"#.as_bytes().to_vec(),
123            };
124            let out = get_example(&client, Parameters {}).await.expect("must be ok");
125            assert_eq!(out.return_value, 42);
126        }
127
128        #[tokio::test]
129        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
130        async fn response_custom_error() {
131            let client = DummyClient {
132                status: http::StatusCode::BAD_REQUEST,
133                json: true,
134                body: r#"{"error":"InvalidToken","message":"Message"}"#.as_bytes().to_vec(),
135            };
136            let result = get_example(&client, Parameters {}).await;
137            let error = result.expect_err("must be error");
138            match &error {
139                crate::Error::XrpcResponse(err) => {
140                    assert_eq!(
141                        err,
142                        &XrpcError {
143                            status: http::StatusCode::BAD_REQUEST,
144                            error: Some(XrpcErrorKind::Custom(Error::InvalidToken(Some(
145                                String::from("Message")
146                            ))))
147                        }
148                    );
149                }
150                _ => panic!("must be Error::XrpcResponse, got {error:?}"),
151            }
152        }
153
154        #[tokio::test]
155        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
156        async fn response_undefined_error() {
157            let client = DummyClient {
158                status: http::StatusCode::INTERNAL_SERVER_ERROR,
159                json: true,
160                body: r#"{"error":"Unknown","message":"Something wrong"}"#.as_bytes().to_vec(),
161            };
162            let result = get_example(&client, Parameters {}).await;
163            let error = result.expect_err("must be error");
164            match &error {
165                crate::Error::XrpcResponse(err) => {
166                    assert_eq!(
167                        err,
168                        &XrpcError {
169                            status: http::StatusCode::INTERNAL_SERVER_ERROR,
170                            error: Some(XrpcErrorKind::Undefined(
171                                crate::error::ErrorResponseBody {
172                                    error: Some(String::from("Unknown")),
173                                    message: Some(String::from("Something wrong"))
174                                }
175                            ))
176                        }
177                    );
178                }
179                _ => panic!("must be Error::XrpcResponse, got {error:?}"),
180            };
181        }
182    }
183
184    mod query {
185        use super::*;
186
187        mod bytes {
188            use super::*;
189
190            async fn get_bytes<T>(xrpc: &T, params: Parameters) -> Result<Vec<u8>, Error>
191            where
192                T: crate::XrpcClient + Send + Sync,
193            {
194                let response = xrpc
195                    .send_xrpc::<_, (), (), _>(&XrpcRequest {
196                        method: http::Method::GET,
197                        nsid: "example".into(),
198                        parameters: Some(params),
199                        input: None,
200                        encoding: None,
201                    })
202                    .await?;
203                match response {
204                    crate::OutputDataOrBytes::Bytes(bytes) => Ok(bytes),
205                    _ => Err(crate::Error::UnexpectedResponseType),
206                }
207            }
208
209            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
210            #[serde(rename_all = "camelCase")]
211            struct Parameters {
212                query: String,
213            }
214
215            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
216            #[serde(tag = "error", content = "message")]
217            enum Error {}
218
219            #[tokio::test]
220            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
221            async fn response_ok() {
222                let body = r"data".as_bytes().to_vec();
223                let client =
224                    DummyClient { status: http::StatusCode::OK, json: false, body: body.clone() };
225                let out = get_bytes(&client, Parameters { query: "foo".into() })
226                    .await
227                    .expect("must be ok");
228                assert_eq!(out, body);
229            }
230
231            #[tokio::test]
232            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
233            async fn response_unexpected() {
234                let client = DummyClient {
235                    status: http::StatusCode::OK,
236                    json: true,
237                    body: r"null".as_bytes().to_vec(),
238                };
239                let result = get_bytes(&client, Parameters { query: "foo".into() }).await;
240                let error = result.expect_err("must be error");
241                match &error {
242                    crate::Error::UnexpectedResponseType => {}
243                    _ => panic!("must be Error::UnexpectedResponseType, got {error:?}"),
244                }
245            }
246        }
247    }
248
249    mod procedure {
250        use super::*;
251
252        mod no_content {
253            use super::*;
254
255            async fn create_data<T>(xrpc: &T, input: Input) -> Result<(), Error>
256            where
257                T: crate::XrpcClient + Send + Sync,
258            {
259                let response = xrpc
260                    .send_xrpc::<(), _, (), _>(&XrpcRequest {
261                        method: http::Method::POST,
262                        nsid: "example".into(),
263                        parameters: None,
264                        input: Some(InputDataOrBytes::Data(input)),
265                        encoding: None,
266                    })
267                    .await?;
268                match response {
269                    crate::OutputDataOrBytes::Bytes(_) => Ok(()),
270                    _ => Err(crate::Error::UnexpectedResponseType),
271                }
272            }
273
274            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
275            #[serde(rename_all = "camelCase")]
276            struct Input {
277                value: i32,
278            }
279
280            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
281            #[serde(tag = "error", content = "message")]
282            enum Error {}
283
284            #[tokio::test]
285            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
286            async fn response_ok() {
287                let client =
288                    DummyClient { status: http::StatusCode::OK, json: false, body: Vec::new() };
289                create_data(&client, Input { value: 42 }).await.expect("must be ok");
290            }
291
292            #[tokio::test]
293            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
294            async fn response_unexpected() {
295                let client = DummyClient {
296                    status: http::StatusCode::OK,
297                    json: true,
298                    body: r"null".as_bytes().to_vec(),
299                };
300                let result = create_data(&client, Input { value: 42 }).await;
301                let error = result.expect_err("must be error");
302                match &error {
303                    crate::Error::UnexpectedResponseType => {}
304                    _ => panic!("must be Error::UnexpectedResponseType, got {error:?}"),
305                }
306            }
307        }
308
309        mod bytes {
310            use super::*;
311
312            async fn create_data<T>(xrpc: &T, input: Vec<u8>) -> Result<Output, Error>
313            where
314                T: crate::XrpcClient + Send + Sync,
315            {
316                let response = xrpc
317                    .send_xrpc::<(), Vec<u8>, _, _>(&XrpcRequest {
318                        method: http::Method::POST,
319                        nsid: "example".into(),
320                        parameters: None,
321                        input: Some(InputDataOrBytes::Bytes(input)),
322                        encoding: None,
323                    })
324                    .await?;
325                match response {
326                    crate::OutputDataOrBytes::Data(data) => Ok(data),
327                    _ => Err(crate::Error::UnexpectedResponseType),
328                }
329            }
330
331            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
332            #[serde(rename_all = "camelCase")]
333            struct Output {
334                return_value: i32,
335            }
336
337            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
338            #[serde(tag = "error", content = "message")]
339            enum Error {}
340
341            #[tokio::test]
342            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
343            async fn response_ok() {
344                let client = DummyClient {
345                    status: http::StatusCode::OK,
346                    json: true,
347                    body: r#"{"returnValue":42}"#.as_bytes().to_vec(),
348                };
349                create_data(&client, "data".as_bytes().to_vec()).await.expect("must be ok");
350            }
351
352            #[tokio::test]
353            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
354            async fn response_unexpected() {
355                let client = DummyClient {
356                    status: http::StatusCode::OK,
357                    json: false,
358                    body: r#"{"returnValue":42}"#.as_bytes().to_vec(),
359                };
360                let result = create_data(&client, "data".as_bytes().to_vec()).await;
361                let error = result.expect_err("must be error");
362                match &error {
363                    crate::Error::UnexpectedResponseType => {}
364                    _ => panic!("must be Error::UnexpectedResponseType, got {error:?}"),
365                }
366            }
367        }
368    }
369}