diff --git a/atrium-api/src/agent/inner.rs b/atrium-api/src/agent/inner.rs index fb7473a4..37938abc 100644 --- a/atrium-api/src/agent/inner.rs +++ b/atrium-api/src/agent/inner.rs @@ -1,8 +1,8 @@ use super::{Session, SessionStore}; use crate::did_doc::DidDocument; use async_trait::async_trait; -use atrium_xrpc::error::{Error, XrpcErrorKind}; -use atrium_xrpc::{HttpClient, OutputDataOrBytes, XrpcClient, XrpcRequest, XrpcResult}; +use atrium_xrpc::error::{Error, Result, XrpcErrorKind}; +use atrium_xrpc::{HttpClient, OutputDataOrBytes, XrpcClient, XrpcRequest}; use http::{Method, Request, Response, Uri}; use serde::{de::DeserializeOwned, Serialize}; use std::sync::{Arc, RwLock}; @@ -25,7 +25,8 @@ where async fn send_http( &self, request: Request>, - ) -> Result>, Box> { + ) -> core::result::Result>, Box> + { self.inner.send_http(request).await } } @@ -113,7 +114,7 @@ where &self, ) -> Result< crate::com::atproto::server::refresh_session::Output, - Error, + crate::com::atproto::server::refresh_session::Error, > { let response = self .inner @@ -130,7 +131,7 @@ where _ => Err(Error::UnexpectedResponseType), } } - fn is_expired(result: &XrpcResult) -> bool + fn is_expired(result: &Result, E>) -> bool where O: DeserializeOwned + Send + Sync, E: DeserializeOwned + Send + Sync, @@ -156,7 +157,8 @@ where async fn send_http( &self, request: Request>, - ) -> Result>, Box> { + ) -> core::result::Result>, Box> + { self.inner.send_http(request).await } } @@ -171,7 +173,10 @@ where fn base_uri(&self) -> String { self.inner.base_uri() } - async fn send_xrpc(&self, request: &XrpcRequest) -> XrpcResult + async fn send_xrpc( + &self, + request: &XrpcRequest, + ) -> Result, E> where P: Serialize + Send + Sync, I: Serialize + Send + Sync, diff --git a/atrium-xrpc/src/error.rs b/atrium-xrpc/src/error.rs index d76daf9f..dae3507c 100644 --- a/atrium-xrpc/src/error.rs +++ b/atrium-xrpc/src/error.rs @@ -1,7 +1,28 @@ #![doc = "Error types."] use http::StatusCode; +use serde::{Deserialize, Serialize}; use std::fmt::{self, Debug, Display}; +/// An enum of possible error kinds. +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("xrpc response error: {0}")] + XrpcResponse(XrpcError), + #[error("http request error: {0}")] + HttpRequest(#[from] http::Error), + #[error("http client error: {0}")] + HttpClient(Box), + #[error("serde_json error: {0}")] + SerdeJson(#[from] serde_json::Error), + #[error("serde_html_form error: {0}")] + SerdeHtmlForm(#[from] serde_html_form::ser::Error), + #[error("unexpected response type")] + UnexpectedResponseType, +} + +/// Type alias to use this library's [`Error`] type in a [`Result`](core::result::Result). +pub type Result = core::result::Result>; + /// [A standard error response schema](https://atproto.com/specs/xrpc#error-responses) /// /// ```typescript @@ -11,7 +32,7 @@ use std::fmt::{self, Debug, Display}; /// }) /// export type ErrorResponseBody = z.infer /// ``` -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct ErrorResponseBody { pub error: Option, pub message: Option, @@ -30,7 +51,7 @@ impl Display for ErrorResponseBody { /// An enum of possible XRPC error kinds. /// /// Error defined in Lexicon schema or other standard error. -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(untagged)] pub enum XrpcErrorKind { Custom(E), @@ -66,20 +87,3 @@ impl Display for XrpcError { Ok(()) } } - -/// An enum of possible error kinds. -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("xrpc response error: {0}")] - XrpcResponse(XrpcError), - #[error("http request error: {0}")] - HttpRequest(#[from] http::Error), - #[error("http client error: {0}")] - HttpClient(Box), - #[error("serde_json error: {0}")] - SerdeJson(#[from] serde_json::Error), - #[error("serde_html_form error: {0}")] - SerdeHtmlForm(#[from] serde_html_form::ser::Error), - #[error("unexpected response type")] - UnexpectedResponseType, -} diff --git a/atrium-xrpc/src/lib.rs b/atrium-xrpc/src/lib.rs index a131544d..654dbc9c 100644 --- a/atrium-xrpc/src/lib.rs +++ b/atrium-xrpc/src/lib.rs @@ -1,134 +1,19 @@ #![doc = include_str!("../README.md")] pub mod error; +mod traits; +mod types; -use crate::error::{Error, XrpcError, XrpcErrorKind}; -use async_trait::async_trait; -use http::{Method, Request, Response}; -use serde::{de::DeserializeOwned, Serialize}; - -/// A type which can be used as a parameter of [`XrpcRequest`]. -/// -/// JSON serializable data or raw bytes. -pub enum InputDataOrBytes -where - T: Serialize, -{ - Data(T), - Bytes(Vec), -} - -/// A type which can be used as a return value of [`XrpcClient::send_xrpc()`]. -/// -/// JSON deserializable data or raw bytes. -pub enum OutputDataOrBytes -where - T: DeserializeOwned, -{ - Data(T), - Bytes(Vec), -} - -/// An abstract HTTP client. -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait HttpClient { - async fn send_http( - &self, - request: Request>, - ) -> Result>, Box>; -} - -/// A request which can be executed with [`XrpcClient::send_xrpc()`]. -pub struct XrpcRequest -where - I: Serialize, -{ - pub method: Method, - pub path: String, - pub parameters: Option

, - pub input: Option>, - pub encoding: Option, -} - -pub type XrpcResult = Result, self::Error>; - -/// An abstract XRPC client. -/// -/// [`send_xrpc()`](XrpcClient::send_xrpc) method has a default implementation, -/// which wraps the [`HttpClient::send_http()`]` method to handle input and output as an XRPC Request. -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait XrpcClient: HttpClient { - fn base_uri(&self) -> String; - #[allow(unused_variables)] - async fn auth(&self, is_refresh: bool) -> Option { - None - } - async fn send_xrpc(&self, request: &XrpcRequest) -> XrpcResult - where - P: Serialize + Send + Sync, - I: Serialize + Send + Sync, - O: DeserializeOwned + Send + Sync, - E: DeserializeOwned + Send + Sync, - { - let mut uri = format!("{}/xrpc/{}", self.base_uri(), request.path); - if let Some(p) = &request.parameters { - serde_html_form::to_string(p).map(|qs| { - uri += "?"; - uri += &qs; - })?; - }; - let mut builder = Request::builder().method(&request.method).uri(&uri); - if let Some(encoding) = &request.encoding { - builder = builder.header(http::header::CONTENT_TYPE, encoding); - } - if let Some(token) = self - .auth( - request.method == Method::POST - && request.path == "com.atproto.server.refreshSession", - ) - .await - { - builder = builder.header(http::header::AUTHORIZATION, format!("Bearer {}", token)); - } - let body = if let Some(input) = &request.input { - match input { - InputDataOrBytes::Data(data) => serde_json::to_vec(&data)?, - InputDataOrBytes::Bytes(bytes) => bytes.clone(), - } - } else { - Vec::new() - }; - let (parts, body) = self - .send_http(builder.body(body)?) - .await - .map_err(Error::HttpClient)? - .into_parts(); - if parts.status.is_success() { - if parts - .headers - .get(http::header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .map_or(false, |content_type| { - content_type.starts_with("application/json") - }) - { - Ok(OutputDataOrBytes::Data(serde_json::from_slice(&body)?)) - } else { - Ok(OutputDataOrBytes::Bytes(body)) - } - } else { - Err(Error::XrpcResponse(XrpcError { - status: parts.status, - error: serde_json::from_slice::>(&body).ok(), - })) - } - } -} +pub use crate::error::{Error, Result}; +pub use crate::traits::{HttpClient, XrpcClient}; +pub use crate::types::{InputDataOrBytes, OutputDataOrBytes, XrpcRequest}; #[cfg(test)] mod tests { use super::*; + use crate::error::{XrpcError, XrpcErrorKind}; + use crate::{HttpClient, XrpcClient}; + use async_trait::async_trait; + use http::{Request, Response}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; @@ -144,7 +29,10 @@ mod tests { async fn send_http( &self, _request: Request>, - ) -> Result>, Box> { + ) -> core::result::Result< + Response>, + Box, + > { let mut builder = Response::builder().status(self.status); if self.json { builder = builder.header(http::header::CONTENT_TYPE, "application/json") @@ -162,7 +50,7 @@ mod tests { mod errors { use super::*; - async fn get_example(xrpc: &T, params: Parameters) -> Result> + async fn get_example(xrpc: &T, params: Parameters) -> Result where T: crate::XrpcClient + Send + Sync, { @@ -305,10 +193,7 @@ mod tests { mod bytes { use super::*; - async fn get_bytes( - xrpc: &T, - params: Parameters, - ) -> Result, crate::Error> + async fn get_bytes(xrpc: &T, params: Parameters) -> Result, Error> where T: crate::XrpcClient + Send + Sync, { @@ -387,7 +272,7 @@ mod tests { mod no_content { use super::*; - async fn create_data(xrpc: &T, input: Input) -> Result<(), crate::Error> + async fn create_data(xrpc: &T, input: Input) -> Result<(), Error> where T: crate::XrpcClient + Send + Sync, { @@ -449,7 +334,7 @@ mod tests { mod bytes { use super::*; - async fn create_data(xrpc: &T, input: Vec) -> Result> + async fn create_data(xrpc: &T, input: Vec) -> Result where T: crate::XrpcClient + Send + Sync, { diff --git a/atrium-xrpc/src/traits.rs b/atrium-xrpc/src/traits.rs new file mode 100644 index 00000000..d8e19589 --- /dev/null +++ b/atrium-xrpc/src/traits.rs @@ -0,0 +1,98 @@ +use crate::error::Error; +use crate::error::{XrpcError, XrpcErrorKind}; +use crate::types::{InputDataOrBytes, OutputDataOrBytes, XrpcRequest}; +use async_trait::async_trait; +use http::{Method, Request, Response}; +use serde::{de::DeserializeOwned, Serialize}; + +/// An abstract HTTP client. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait HttpClient { + async fn send_http( + &self, + request: Request>, + ) -> core::result::Result>, Box>; +} + +type XrpcResult = core::result::Result, self::Error>; + +/// An abstract XRPC client. +/// +/// [`send_xrpc()`](XrpcClient::send_xrpc) method has a default implementation, +/// which wraps the [`HttpClient::send_http()`]` method to handle input and output as an XRPC Request. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait XrpcClient: HttpClient { + fn base_uri(&self) -> String; + #[allow(unused_variables)] + async fn auth(&self, is_refresh: bool) -> Option { + None + } + async fn headers(&self) -> Vec<(String, String)> { + Vec::new() + } + async fn send_xrpc(&self, request: &XrpcRequest) -> XrpcResult + where + P: Serialize + Send + Sync, + I: Serialize + Send + Sync, + O: DeserializeOwned + Send + Sync, + E: DeserializeOwned + Send + Sync, + { + let mut uri = format!("{}/xrpc/{}", self.base_uri(), request.path); + if let Some(p) = &request.parameters { + serde_html_form::to_string(p).map(|qs| { + uri += "?"; + uri += &qs; + })?; + }; + let mut builder = Request::builder().method(&request.method).uri(&uri); + if let Some(encoding) = &request.encoding { + builder = builder.header(http::header::CONTENT_TYPE, encoding); + } + if let Some(token) = self + .auth( + request.method == Method::POST + && request.path == "com.atproto.server.refreshSession", + ) + .await + { + builder = builder.header(http::header::AUTHORIZATION, format!("Bearer {}", token)); + } + for (key, value) in self.headers().await { + builder = builder.header(key, value); + } + let body = if let Some(input) = &request.input { + match input { + InputDataOrBytes::Data(data) => serde_json::to_vec(&data)?, + InputDataOrBytes::Bytes(bytes) => bytes.clone(), + } + } else { + Vec::new() + }; + let (parts, body) = self + .send_http(builder.body(body)?) + .await + .map_err(Error::HttpClient)? + .into_parts(); + if parts.status.is_success() { + if parts + .headers + .get(http::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .map_or(false, |content_type| { + content_type.starts_with("application/json") + }) + { + Ok(OutputDataOrBytes::Data(serde_json::from_slice(&body)?)) + } else { + Ok(OutputDataOrBytes::Bytes(body)) + } + } else { + Err(Error::XrpcResponse(XrpcError { + status: parts.status, + error: serde_json::from_slice::>(&body).ok(), + })) + } + } +} diff --git a/atrium-xrpc/src/types.rs b/atrium-xrpc/src/types.rs new file mode 100644 index 00000000..f9a95945 --- /dev/null +++ b/atrium-xrpc/src/types.rs @@ -0,0 +1,36 @@ +use http::Method; +use serde::{de::DeserializeOwned, Serialize}; + +/// A type which can be used as a parameter of [`XrpcRequest`]. +/// +/// JSON serializable data or raw bytes. +pub enum InputDataOrBytes +where + T: Serialize, +{ + Data(T), + Bytes(Vec), +} + +/// A type which can be used as a return value of [`XrpcClient::send_xrpc()`](crate::XrpcClient::send_xrpc). +/// +/// JSON deserializable data or raw bytes. +pub enum OutputDataOrBytes +where + T: DeserializeOwned, +{ + Data(T), + Bytes(Vec), +} + +/// A request which can be executed with [`XrpcClient::send_xrpc()`](crate::XrpcClient::send_xrpc). +pub struct XrpcRequest +where + I: Serialize, +{ + pub method: Method, + pub path: String, + pub parameters: Option

, + pub input: Option>, + pub encoding: Option, +}