pub mod api;
pub mod error;
pub mod key;
mod policy;
pub mod request;
pub mod types;
pub mod venue;
pub mod weight;
#[cfg(feature = "usdm-ws")]
pub mod ws;
use polyoxide_core::{ClientConfig, HttpClient};
pub use error::UsdmError;
pub use key::key;
pub use weight::WeightBudget;
use crate::usdm::{
api::{exchange::ExchangeApi, health::Health, market::MarketApi},
policy::UsdmRetryPolicy,
};
pub const DEFAULT_BASE_URL: &str = "https://fapi.binance.com";
pub const DEFAULT_MAX_CONCURRENT: usize = 4;
#[derive(Debug, Clone)]
pub struct Usdm {
http: HttpClient,
budget: WeightBudget,
}
impl Usdm {
pub fn new() -> Result<Self, UsdmError> {
Self::builder().build()
}
pub fn builder() -> UsdmBuilder {
UsdmBuilder::new()
}
polyoxide_core::namespaces! { http;
health: Health,
exchange: ExchangeApi,
market: MarketApi,
}
pub fn weight_budget(&self) -> &WeightBudget {
&self.budget
}
}
#[derive(Debug)]
pub struct UsdmBuilder {
config: ClientConfig,
budget: Option<WeightBudget>,
}
impl UsdmBuilder {
fn new() -> Self {
Self {
config: ClientConfig::new(DEFAULT_BASE_URL, DEFAULT_MAX_CONCURRENT),
budget: None,
}
}
polyoxide_core::client_config_setters!(config);
pub fn weight_budget(mut self, budget: WeightBudget) -> Self {
self.budget = Some(budget);
self
}
pub fn build(self) -> Result<Usdm, UsdmError> {
let budget = self.budget.unwrap_or_default();
let http = self
.config
.http_builder()
.with_throttle(budget.clone())
.with_retry_policy(UsdmRetryPolicy {
budget: budget.clone(),
})
.build()?;
Ok(Usdm { http, budget })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_builder_targets_the_production_host() {
let usdm = Usdm::new().expect("client builds");
assert_eq!(usdm.http.base_url.as_str(), "https://fapi.binance.com/");
}
#[test]
fn a_bad_base_url_is_a_url_error() {
let err = Usdm::builder().base_url("not a url").build().unwrap_err();
assert!(matches!(
err,
UsdmError::Api(polyoxide_core::ApiError::Url(_))
));
}
#[test]
fn clients_given_one_budget_share_it() {
let budget = WeightBudget::new();
let a = Usdm::builder()
.weight_budget(budget.clone())
.build()
.unwrap();
let b = Usdm::builder().weight_budget(budget).build().unwrap();
assert!(a.weight_budget().is_shared_with(b.weight_budget()));
let c = Usdm::new().unwrap();
assert!(!a.weight_budget().is_shared_with(c.weight_budget()));
}
#[tokio::test]
async fn test_default_concurrency_limit_is_4() {
let usdm = Usdm::new().unwrap();
let mut permits = Vec::new();
for _ in 0..4 {
permits.push(usdm.http.acquire_concurrency().await);
}
assert!(permits.iter().all(|p| p.is_some()));
let result = tokio::time::timeout(
std::time::Duration::from_millis(50),
usdm.http.acquire_concurrency(),
)
.await;
assert!(
result.is_err(),
"5th permit should block with default limit of 4"
);
}
}