cryptologo 0.1.2

Rust client for crypto-logo.com — 413 cryptocurrency logos in SVG, PNG, WebP, ICO
Documentation
use crate::types::{Coin, Error, LogoFormat, LogoOptions};
use reqwest::Client;
use std::time::Duration;

const DEFAULT_BASE_URL: &str = "https://crypto-logo.com";
const DEFAULT_CDN_DOMAIN: &str = "cdn.crypto-logo.com";
const USER_AGENT: &str = "cryptologo-rs/0.1.0";

/// Builder for configuring a [`CryptoLogo`] client.
pub struct CryptoLogoBuilder {
    base_url: String,
    timeout: Duration,
}

impl Default for CryptoLogoBuilder {
    fn default() -> Self {
        Self {
            base_url: DEFAULT_BASE_URL.to_string(),
            timeout: Duration::from_secs(30),
        }
    }
}

impl CryptoLogoBuilder {
    /// Set a custom API base URL.
    pub fn base_url(mut self, url: &str) -> Self {
        self.base_url = url.trim_end_matches('/').to_string();
        self
    }

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

    /// Build the client.
    pub fn build(self) -> CryptoLogo {
        let client = Client::builder()
            .timeout(self.timeout)
            .user_agent(USER_AGENT)
            .build()
            .expect("Failed to build HTTP client");

        CryptoLogo {
            base_url: self.base_url,
            client,
        }
    }
}

/// Client for the crypto-logo.com API.
///
/// Provides async methods to list coins, fetch logos, and generate URLs.
///
/// # Example
///
/// ```rust,no_run
/// use cryptologo::CryptoLogo;
///
/// #[tokio::main]
/// async fn main() -> Result<(), cryptologo::Error> {
///     let client = CryptoLogo::new();
///     let coins = client.list_coins().await?;
///     println!("Found {} coins", coins.len());
///     Ok(())
/// }
/// ```
pub struct CryptoLogo {
    base_url: String,
    client: Client,
}

impl CryptoLogo {
    /// Create a new client with default settings.
    pub fn new() -> Self {
        CryptoLogoBuilder::default().build()
    }

    /// Create a builder for custom configuration.
    pub fn builder() -> CryptoLogoBuilder {
        CryptoLogoBuilder::default()
    }

    /// Fetch all active coins from the API.
    ///
    /// Returns a Vec of [`Coin`] sorted by market cap rank.
    pub async fn list_coins(&self) -> Result<Vec<Coin>, Error> {
        let url = format!("{}/api/coins.json", self.base_url);
        let resp = self.client.get(&url).send().await?;

        if resp.status().as_u16() == 404 {
            return Err(Error::NotFound(url));
        }
        if !resp.status().is_success() {
            return Err(Error::Status {
                status: resp.status().as_u16(),
                url,
            });
        }

        let coins: Vec<Coin> = resp.json().await?;
        Ok(coins)
    }

    /// Fetch logo image bytes.
    ///
    /// For SVG, no options are needed. For raster formats, provide width in options.
    pub async fn get_logo(
        &self,
        slug: &str,
        format: LogoFormat,
        options: Option<LogoOptions>,
    ) -> Result<Vec<u8>, Error> {
        let url = self.get_logo_url(slug, format, options.as_ref());
        let resp = self.client.get(&url).send().await?;

        if resp.status().as_u16() == 404 {
            return Err(Error::NotFound(url));
        }
        if !resp.status().is_success() {
            return Err(Error::Status {
                status: resp.status().as_u16(),
                url,
            });
        }

        let bytes = resp.bytes().await?;
        Ok(bytes.to_vec())
    }

    /// Build the API URL for a logo.
    pub fn get_logo_url(
        &self,
        slug: &str,
        format: LogoFormat,
        options: Option<&LogoOptions>,
    ) -> String {
        let mut url = format!("{}/api/logo/{}.{}", self.base_url, slug, format);

        if let Some(opts) = options {
            let mut params = Vec::new();
            if let Some(w) = opts.width {
                params.push(format!("w={}", w));
            }
            let h = opts.height.or(opts.width);
            if let Some(h) = h {
                params.push(format!("h={}", h));
            }
            if let Some(ref bg) = opts.bg {
                params.push(format!("bg={}", bg));
            }
            if !params.is_empty() {
                url.push('?');
                url.push_str(&params.join("&"));
            }
        }
        url
    }

    /// Build a CDN URL for direct image embedding.
    ///
    /// The CDN serves pre-generated derivatives from Cloudflare R2.
    pub fn get_cdn_url(&self, slug: &str, format: LogoFormat, width: u32) -> String {
        if format == LogoFormat::Svg {
            return format!(
                "https://{}/logos/{}/vector.svg",
                DEFAULT_CDN_DOMAIN, slug
            );
        }
        format!(
            "https://{}/logos/{}/{}x{}/transparent.{}",
            DEFAULT_CDN_DOMAIN, slug, width, width, format
        )
    }

    /// Download a logo and save to a file.
    pub async fn download_logo(
        &self,
        slug: &str,
        format: LogoFormat,
        options: Option<LogoOptions>,
        dest: &str,
    ) -> Result<(), Error> {
        let data = self.get_logo(slug, format, options).await?;
        std::fs::write(dest, &data)?;
        Ok(())
    }

    /// Fetch a variant or historical logo file.
    pub async fn get_asset(&self, file_path: &str) -> Result<Vec<u8>, Error> {
        let url = format!("{}/api/asset/{}", self.base_url, file_path);
        let resp = self.client.get(&url).send().await?;

        if resp.status().as_u16() == 404 {
            return Err(Error::NotFound(url));
        }
        if !resp.status().is_success() {
            return Err(Error::Status {
                status: resp.status().as_u16(),
                url,
            });
        }

        let bytes = resp.bytes().await?;
        Ok(bytes.to_vec())
    }
}

impl Default for CryptoLogo {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_logo_url_svg() {
        let client = CryptoLogo::new();
        let url = client.get_logo_url("bitcoin-btc", LogoFormat::Svg, None);
        assert_eq!(url, "https://crypto-logo.com/api/logo/bitcoin-btc.svg");
    }

    #[test]
    fn test_get_logo_url_png_with_size() {
        let client = CryptoLogo::new();
        let opts = LogoOptions {
            width: Some(256),
            ..Default::default()
        };
        let url = client.get_logo_url("bitcoin-btc", LogoFormat::Png, Some(&opts));
        assert_eq!(
            url,
            "https://crypto-logo.com/api/logo/bitcoin-btc.png?w=256&h=256"
        );
    }

    #[test]
    fn test_get_cdn_url_png() {
        let client = CryptoLogo::new();
        let url = client.get_cdn_url("bitcoin-btc", LogoFormat::Png, 128);
        assert_eq!(
            url,
            "https://cdn.crypto-logo.com/logos/bitcoin-btc/128x128/transparent.png"
        );
    }

    #[test]
    fn test_get_cdn_url_svg() {
        let client = CryptoLogo::new();
        let url = client.get_cdn_url("bitcoin-btc", LogoFormat::Svg, 0);
        assert_eq!(
            url,
            "https://cdn.crypto-logo.com/logos/bitcoin-btc/vector.svg"
        );
    }

    #[test]
    fn test_format_display() {
        assert_eq!(LogoFormat::Svg.to_string(), "svg");
        assert_eq!(LogoFormat::Png.to_string(), "png");
        assert_eq!(LogoFormat::WebP.to_string(), "webp");
    }
}