atrium-xrpc 0.12.4

XRPC library for AT Protocol (Bluesky)
Documentation
#![doc = include_str!("../README.md")]
pub mod error;
mod traits;
pub mod types;

pub use crate::error::{Error, Result};
pub use crate::traits::{HttpClient, XrpcClient};
pub use crate::types::{InputDataOrBytes, OutputDataOrBytes, XrpcRequest};
pub use http;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::{XrpcError, XrpcErrorKind};
    use crate::{HttpClient, XrpcClient};
    use http::{Request, Response};
    #[cfg(target_arch = "wasm32")]
    use wasm_bindgen_test::*;

    struct DummyClient {
        status: http::StatusCode,
        json: bool,
        body: Vec<u8>,
    }

    impl HttpClient for DummyClient {
        async fn send_http(
            &self,
            _request: Request<Vec<u8>>,
        ) -> core::result::Result<
            Response<Vec<u8>>,
            Box<dyn std::error::Error + Send + Sync + 'static>,
        > {
            let mut builder = Response::builder().status(self.status);
            if self.json {
                builder = builder.header(http::header::CONTENT_TYPE, "application/json")
            }
            Ok(builder.body(self.body.clone())?)
        }
    }

    impl XrpcClient for DummyClient {
        fn base_uri(&self) -> String {
            "https://example.com".into()
        }
    }

    mod errors {
        use super::*;

        async fn get_example<T>(xrpc: &T, params: Parameters) -> Result<Output, Error>
        where
            T: crate::XrpcClient + Send + Sync,
        {
            let response = xrpc
                .send_xrpc::<_, (), _, _>(&XrpcRequest {
                    method: http::Method::GET,
                    nsid: "example".into(),
                    parameters: Some(params),
                    input: None,
                    encoding: None,
                })
                .await?;
            match response {
                crate::OutputDataOrBytes::Data(data) => Ok(data),
                _ => Err(crate::Error::UnexpectedResponseType),
            }
        }

        #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
        #[serde(rename_all = "camelCase")]
        struct Parameters {}

        #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
        #[serde(rename_all = "camelCase")]
        struct Output {
            return_value: i32,
        }

        #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
        #[serde(tag = "error", content = "message")]
        enum Error {
            InvalidToken(Option<String>),
            ExpiredToken(Option<String>),
        }

        #[test]
        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
        fn deserialize_xrpc_error() {
            {
                let body = r#"{"error":"InvalidToken","message":"Invalid token"}"#;
                let err = serde_json::from_str::<XrpcErrorKind<_>>(body).expect("deserialize");
                assert_eq!(
                    err,
                    XrpcErrorKind::Custom(Error::InvalidToken(Some(String::from("Invalid token"))))
                );
            }
            {
                let body = r#"{"error":"ExpiredToken"}"#;
                let err = serde_json::from_str::<XrpcErrorKind<_>>(body).expect("deserialize");
                assert_eq!(err, XrpcErrorKind::Custom(Error::ExpiredToken(None)));
            }
            {
                let body = r#"{"error":"Unknown","message":"Something wrong"}"#;
                let err = serde_json::from_str::<XrpcErrorKind<Error>>(body).expect("deserialize");
                assert_eq!(
                    err,
                    XrpcErrorKind::Undefined(crate::error::ErrorResponseBody {
                        error: Some(String::from("Unknown")),
                        message: Some(String::from("Something wrong")),
                    })
                );
            }
        }

        #[tokio::test]
        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
        async fn response_ok() {
            let client = DummyClient {
                status: http::StatusCode::OK,
                json: true,
                body: r#"{"returnValue":42}"#.as_bytes().to_vec(),
            };
            let out = get_example(&client, Parameters {}).await.expect("must be ok");
            assert_eq!(out.return_value, 42);
        }

        #[tokio::test]
        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
        async fn response_custom_error() {
            let client = DummyClient {
                status: http::StatusCode::BAD_REQUEST,
                json: true,
                body: r#"{"error":"InvalidToken","message":"Message"}"#.as_bytes().to_vec(),
            };
            let result = get_example(&client, Parameters {}).await;
            let error = result.expect_err("must be error");
            match &error {
                crate::Error::XrpcResponse(err) => {
                    assert_eq!(
                        err,
                        &XrpcError {
                            status: http::StatusCode::BAD_REQUEST,
                            error: Some(XrpcErrorKind::Custom(Error::InvalidToken(Some(
                                String::from("Message")
                            ))))
                        }
                    );
                }
                _ => panic!("must be Error::XrpcResponse, got {error:?}"),
            }
        }

        #[tokio::test]
        #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
        async fn response_undefined_error() {
            let client = DummyClient {
                status: http::StatusCode::INTERNAL_SERVER_ERROR,
                json: true,
                body: r#"{"error":"Unknown","message":"Something wrong"}"#.as_bytes().to_vec(),
            };
            let result = get_example(&client, Parameters {}).await;
            let error = result.expect_err("must be error");
            match &error {
                crate::Error::XrpcResponse(err) => {
                    assert_eq!(
                        err,
                        &XrpcError {
                            status: http::StatusCode::INTERNAL_SERVER_ERROR,
                            error: Some(XrpcErrorKind::Undefined(
                                crate::error::ErrorResponseBody {
                                    error: Some(String::from("Unknown")),
                                    message: Some(String::from("Something wrong"))
                                }
                            ))
                        }
                    );
                }
                _ => panic!("must be Error::XrpcResponse, got {error:?}"),
            };
        }
    }

    mod query {
        use super::*;

        mod bytes {
            use super::*;

            async fn get_bytes<T>(xrpc: &T, params: Parameters) -> Result<Vec<u8>, Error>
            where
                T: crate::XrpcClient + Send + Sync,
            {
                let response = xrpc
                    .send_xrpc::<_, (), (), _>(&XrpcRequest {
                        method: http::Method::GET,
                        nsid: "example".into(),
                        parameters: Some(params),
                        input: None,
                        encoding: None,
                    })
                    .await?;
                match response {
                    crate::OutputDataOrBytes::Bytes(bytes) => Ok(bytes),
                    _ => Err(crate::Error::UnexpectedResponseType),
                }
            }

            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
            #[serde(rename_all = "camelCase")]
            struct Parameters {
                query: String,
            }

            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
            #[serde(tag = "error", content = "message")]
            enum Error {}

            #[tokio::test]
            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
            async fn response_ok() {
                let body = r"data".as_bytes().to_vec();
                let client =
                    DummyClient { status: http::StatusCode::OK, json: false, body: body.clone() };
                let out = get_bytes(&client, Parameters { query: "foo".into() })
                    .await
                    .expect("must be ok");
                assert_eq!(out, body);
            }

            #[tokio::test]
            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
            async fn response_unexpected() {
                let client = DummyClient {
                    status: http::StatusCode::OK,
                    json: true,
                    body: r"null".as_bytes().to_vec(),
                };
                let result = get_bytes(&client, Parameters { query: "foo".into() }).await;
                let error = result.expect_err("must be error");
                match &error {
                    crate::Error::UnexpectedResponseType => {}
                    _ => panic!("must be Error::UnexpectedResponseType, got {error:?}"),
                }
            }
        }
    }

    mod procedure {
        use super::*;

        mod no_content {
            use super::*;

            async fn create_data<T>(xrpc: &T, input: Input) -> Result<(), Error>
            where
                T: crate::XrpcClient + Send + Sync,
            {
                let response = xrpc
                    .send_xrpc::<(), _, (), _>(&XrpcRequest {
                        method: http::Method::POST,
                        nsid: "example".into(),
                        parameters: None,
                        input: Some(InputDataOrBytes::Data(input)),
                        encoding: None,
                    })
                    .await?;
                match response {
                    crate::OutputDataOrBytes::Bytes(_) => Ok(()),
                    _ => Err(crate::Error::UnexpectedResponseType),
                }
            }

            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
            #[serde(rename_all = "camelCase")]
            struct Input {
                value: i32,
            }

            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
            #[serde(tag = "error", content = "message")]
            enum Error {}

            #[tokio::test]
            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
            async fn response_ok() {
                let client =
                    DummyClient { status: http::StatusCode::OK, json: false, body: Vec::new() };
                create_data(&client, Input { value: 42 }).await.expect("must be ok");
            }

            #[tokio::test]
            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
            async fn response_unexpected() {
                let client = DummyClient {
                    status: http::StatusCode::OK,
                    json: true,
                    body: r"null".as_bytes().to_vec(),
                };
                let result = create_data(&client, Input { value: 42 }).await;
                let error = result.expect_err("must be error");
                match &error {
                    crate::Error::UnexpectedResponseType => {}
                    _ => panic!("must be Error::UnexpectedResponseType, got {error:?}"),
                }
            }
        }

        mod bytes {
            use super::*;

            async fn create_data<T>(xrpc: &T, input: Vec<u8>) -> Result<Output, Error>
            where
                T: crate::XrpcClient + Send + Sync,
            {
                let response = xrpc
                    .send_xrpc::<(), Vec<u8>, _, _>(&XrpcRequest {
                        method: http::Method::POST,
                        nsid: "example".into(),
                        parameters: None,
                        input: Some(InputDataOrBytes::Bytes(input)),
                        encoding: None,
                    })
                    .await?;
                match response {
                    crate::OutputDataOrBytes::Data(data) => Ok(data),
                    _ => Err(crate::Error::UnexpectedResponseType),
                }
            }

            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
            #[serde(rename_all = "camelCase")]
            struct Output {
                return_value: i32,
            }

            #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
            #[serde(tag = "error", content = "message")]
            enum Error {}

            #[tokio::test]
            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
            async fn response_ok() {
                let client = DummyClient {
                    status: http::StatusCode::OK,
                    json: true,
                    body: r#"{"returnValue":42}"#.as_bytes().to_vec(),
                };
                create_data(&client, "data".as_bytes().to_vec()).await.expect("must be ok");
            }

            #[tokio::test]
            #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
            async fn response_unexpected() {
                let client = DummyClient {
                    status: http::StatusCode::OK,
                    json: false,
                    body: r#"{"returnValue":42}"#.as_bytes().to_vec(),
                };
                let result = create_data(&client, "data".as_bytes().to_vec()).await;
                let error = result.expect_err("must be error");
                match &error {
                    crate::Error::UnexpectedResponseType => {}
                    _ => panic!("must be Error::UnexpectedResponseType, got {error:?}"),
                }
            }
        }
    }
}