alpaca-data 0.9.1

High-performance Rust client for Alpaca Market Data API
Documentation
use crate::{
    Error,
    auth::Auth,
    transport::{endpoint::Endpoint, rate_limit::RateLimiter, retry::RetryPolicy},
};

#[derive(Clone, Debug)]
pub(crate) struct HttpClient {
    client: reqwest::Client,
    retry_policy: RetryPolicy,
    rate_limiter: RateLimiter,
}

impl HttpClient {
    pub(crate) fn new(
        timeout: std::time::Duration,
        retry_policy: RetryPolicy,
        rate_limiter: RateLimiter,
    ) -> Result<Self, Error> {
        let client = reqwest::Client::builder()
            .timeout(timeout)
            .build()
            .map_err(Error::from_reqwest)?;

        Ok(Self {
            client,
            retry_policy,
            rate_limiter,
        })
    }

    pub(crate) async fn get_json<T>(
        &self,
        base_url: &str,
        endpoint: Endpoint,
        auth: &Auth,
        query: Vec<(String, String)>,
    ) -> Result<T, Error>
    where
        T: serde::de::DeserializeOwned,
    {
        let _permit = self.rate_limiter.acquire().await?;
        let path = endpoint.path();
        let url = format!("{}{}", base_url.trim_end_matches('/'), path.as_ref());
        let mut attempt = 0;

        loop {
            let request = self.client.get(&url).query(&query);
            let request = auth.apply(request, endpoint.requires_auth())?;
            let response = request.send().await.map_err(Error::from_reqwest)?;
            let status = response.status();
            let retry_after = parse_retry_after(response.headers());
            let body = response.text().await.map_err(Error::from_reqwest)?;

            if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
                return Err(Error::RateLimited {
                    retry_after,
                    body: Some(body),
                });
            }

            if self.retry_policy.should_retry(status, attempt) {
                attempt += 1;
                tokio::time::sleep(self.retry_policy.backoff(attempt)).await;
                continue;
            }

            if !status.is_success() {
                return Err(Error::HttpStatus {
                    status: status.as_u16(),
                    body: Some(body),
                });
            }

            return serde_json::from_str(&body)
                .map_err(|error| Error::Deserialize(error.to_string()));
        }
    }
}

fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<u64> {
    headers
        .get(reqwest::header::RETRY_AFTER)
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.parse::<u64>().ok())
}