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}