Files
AstralRinth/packages/muralpay/src/client/mod.rs
aecsocket 9aa06fbc26 Fix Mural payout status syncing (#4853)
* Fix Mural payout status syncing

* Make Mural payout code more resilient

* prepare sqlx

* fix test
2025-12-08 20:34:41 +00:00

123 lines
3.8 KiB
Rust

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<arc_swap::ArcSwapOption<mock::MuralPayMock>>,
}
impl Client {
pub fn new(
api_url: impl Into<String>,
api_key: impl Into<SecretString>,
transfer_api_key: impl Into<SecretString>,
) -> 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<U: IntoUrl>(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder {
self.http_req(|| self.http.get(make_url(&self.api_url)))
}
pub(crate) fn http_post<U: IntoUrl>(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder {
self.http_req(|| self.http.post(make_url(&self.api_url)))
}
pub(crate) fn http_put<U: IntoUrl>(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder {
self.http_req(|| self.http.put(make_url(&self.api_url)))
}
pub(crate) fn http_delete<U: IntoUrl>(
&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<T: DeserializeOwned>(
self,
) -> impl Future<Output = crate::Result<T>> + 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<T: DeserializeOwned>(self) -> crate::Result<T> {
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::<ApiError>(&json)
.map_err(|source| MuralError::DecodeError { source, json })?;
Err(MuralError::Api(err))
} else {
let json = resp.bytes().await?;
let t = serde_json::from_slice::<T>(&json)
.map_err(|source| MuralError::Decode { source, json })?;
Ok(t)
}
}
}