mod error; pub use error::*; use { reqwest::{IntoUrl, RequestBuilder}, secrecy::{ExposeSecret, SecretString}, }; #[cfg(feature = "mock")] mod mock; #[cfg(feature = "mock")] pub use mock::MuralPayMock; use serde::de::DeserializeOwned; #[derive(Debug, Clone)] pub struct Client { pub http: reqwest::Client, pub api_url: String, pub api_key: SecretString, pub transfer_api_key: SecretString, #[cfg(feature = "mock")] pub mock: std::sync::Arc>, } impl Client { pub fn new( api_url: impl Into, api_key: impl Into, transfer_api_key: impl Into, ) -> Self { Self { http: reqwest::Client::new(), api_url: api_url.into(), api_key: api_key.into(), transfer_api_key: transfer_api_key.into(), #[cfg(feature = "mock")] mock: std::sync::Arc::new(arc_swap::ArcSwapOption::empty()), } } /// Creates a client which mocks responses. #[cfg(feature = "mock")] #[must_use] pub fn from_mock(mock: mock::MuralPayMock) -> Self { Self { http: reqwest::Client::new(), api_url: String::new(), api_key: SecretString::from(String::new()), transfer_api_key: SecretString::from(String::new()), mock: std::sync::Arc::new(arc_swap::ArcSwapOption::from_pointee(mock)), } } fn http_req(&self, make_req: impl FnOnce() -> RequestBuilder) -> RequestBuilder { make_req() .bearer_auth(self.api_key.expose_secret()) .header("accept", "application/json") .header("content-type", "application/json") } pub(crate) fn http_get(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder { self.http_req(|| self.http.get(make_url(&self.api_url))) } pub(crate) fn http_post(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder { self.http_req(|| self.http.post(make_url(&self.api_url))) } pub(crate) fn http_put(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder { self.http_req(|| self.http.put(make_url(&self.api_url))) } pub(crate) fn http_delete( &self, make_url: impl FnOnce(&str) -> U, ) -> RequestBuilder { self.http_req(|| self.http.delete(make_url(&self.api_url))) } pub async fn health(&self) -> reqwest::Result<()> { self.http_get(|base| format!("{base}/api/health")) .send() .await? .error_for_status()?; Ok(()) } } pub trait RequestExt: Sized { #[must_use] fn transfer_auth(self, client: &Client) -> Self; fn send_mural( self, ) -> impl Future> + Send + Sync; } const HEADER_TRANSFER_API_KEY: &str = "transfer-api-key"; impl RequestExt for reqwest::RequestBuilder { fn transfer_auth(self, client: &Client) -> Self { self.header( HEADER_TRANSFER_API_KEY, client.transfer_api_key.expose_secret(), ) } async fn send_mural(self) -> crate::Result { let resp = self.send().await?; let status = resp.status(); if status.is_client_error() || status.is_server_error() { let json = resp.bytes().await?; let err = serde_json::from_slice::(&json) .map_err(|source| MuralError::DecodeError { source, json })?; Err(MuralError::Api(err)) } else { let json = resp.bytes().await?; let t = serde_json::from_slice::(&json) .map_err(|source| MuralError::Decode { source, json })?; Ok(t) } } }