bitculator 1.1.0

Official Rust SDK for the Bitculator Data API
Documentation
//! Official Rust SDK for the [Bitculator Data API](https://bitculator.com).
//!
//! ```no_run
//! use bitculator::{Client, Params};
//! use serde_json::Value;
//!
//! # fn main() -> Result<(), bitculator::Error> {
//! let client = Client::new("your-api-key");
//! let bitcoin: Value = client.coins().get("bitcoin", Params::new())?;
//! println!("{}", bitcoin["price"]); // "63520.780763913" — a decimal string
//! # Ok(())
//! # }
//! ```

// `Error::Api` carries `ApiError` by value and the struct tops clippy's 128-byte
// threshold. Boxing the variant would break the published 1.0 API surface, and
// the Err path is cold — the by-value trade-off is deliberate.
#![allow(clippy::result_large_err)]

mod error;
mod http;
mod pagination;
mod params;
mod resources;

use std::sync::Arc;
use std::time::Duration;

pub use error::{ApiError, Error};
pub use http::Quota;
pub use pagination::{AutoPager, Page, PageLinks, PageMeta};
pub use params::{Params, QueryValue};
pub use resources::{
    Alarms, Calculators, Coins, Conversion, Editorial, Exchanges, GlobalMarket, Indicators,
    Liquidations, Markets, Meta, NewAlarm, NewWebhook, Prices, Sentiment, Wallets, Webhooks,
};

use http::HttpClient;

/// SDK version, sent in the User-Agent header. Single-sourced from Cargo.toml.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

const DEFAULT_BASE_URL: &str = "https://bitculator.com";

/// The Bitculator Data API client. Cheap to clone (shares one HTTP client).
#[derive(Clone)]
pub struct Client {
    http: Arc<HttpClient>,
}

impl Client {
    /// Create a client with default configuration.
    ///
    /// Panics if `api_key` is empty — see [`ClientBuilder::build`].
    pub fn new(api_key: impl Into<String>) -> Self {
        Self::builder(api_key).build()
    }

    /// Start configuring a client.
    pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
        ClientBuilder::new(api_key.into())
    }

    /// Quota (X-Quota-*) from the most recent response, or `None` before the first call.
    pub fn quota(&self) -> Option<Quota> {
        self.http.quota()
    }

    pub fn coins(&self) -> Coins {
        Coins::new(self.http.clone())
    }
    pub fn prices(&self) -> Prices {
        Prices::new(self.http.clone())
    }
    pub fn markets(&self) -> Markets {
        Markets::new(self.http.clone())
    }
    pub fn exchanges(&self) -> Exchanges {
        Exchanges::new(self.http.clone())
    }
    pub fn wallets(&self) -> Wallets {
        Wallets::new(self.http.clone())
    }
    pub fn global_market(&self) -> GlobalMarket {
        GlobalMarket::new(self.http.clone())
    }
    pub fn sentiment(&self) -> Sentiment {
        Sentiment::new(self.http.clone())
    }
    pub fn indicators(&self) -> Indicators {
        Indicators::new(self.http.clone())
    }
    pub fn liquidations(&self) -> Liquidations {
        Liquidations::new(self.http.clone())
    }
    pub fn conversion(&self) -> Conversion {
        Conversion::new(self.http.clone())
    }
    pub fn calculators(&self) -> Calculators {
        Calculators::new(self.http.clone())
    }
    pub fn editorial(&self) -> Editorial {
        Editorial::new(self.http.clone())
    }
    pub fn alarms(&self) -> Alarms {
        Alarms::new(self.http.clone())
    }
    pub fn webhooks(&self) -> Webhooks {
        Webhooks::new(self.http.clone())
    }
    pub fn meta(&self) -> Meta {
        Meta::new(self.http.clone())
    }
}

/// Builder for [`Client`].
pub struct ClientBuilder {
    api_key: String,
    base_url: String,
    timeout: Duration,
    max_retries: u32,
    user_agent: String,
}

impl ClientBuilder {
    fn new(api_key: String) -> Self {
        ClientBuilder {
            api_key,
            base_url: DEFAULT_BASE_URL.to_string(),
            timeout: Duration::from_secs(30),
            max_retries: 2,
            user_agent: format!("bitculator-rust/{VERSION}"),
        }
    }

    /// Override the API origin (default `https://bitculator.com`).
    pub fn base_url(mut self, url: impl Into<String>) -> Self {
        self.base_url = url.into().trim_end_matches('/').to_string();
        self
    }

    /// Set the per-request timeout (default 30s).
    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    /// Set the number of automatic retries for 429/5xx/network errors (default 2).
    pub fn max_retries(mut self, retries: u32) -> Self {
        self.max_retries = retries;
        self
    }

    /// Override the User-Agent header.
    pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
        self.user_agent = ua.into();
        self
    }

    /// Build the client.
    ///
    /// # Panics
    ///
    /// Panics if the API key is empty — a misconfigured key should fail at
    /// construction, not on the first request.
    pub fn build(self) -> Client {
        if self.api_key.is_empty() {
            panic!("bitculator: a Data API key is required");
        }
        let client = reqwest::blocking::Client::builder()
            .timeout(self.timeout)
            .build()
            .expect("failed to build reqwest client");
        Client {
            http: Arc::new(HttpClient::new(
                self.api_key,
                self.base_url,
                self.user_agent,
                self.max_retries,
                client,
            )),
        }
    }
}