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";
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 {
pub fn base_url(mut self, url: &str) -> Self {
self.base_url = url.trim_end_matches('/').to_string();
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
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,
}
}
}
pub struct CryptoLogo {
base_url: String,
client: Client,
}
impl CryptoLogo {
pub fn new() -> Self {
CryptoLogoBuilder::default().build()
}
pub fn builder() -> CryptoLogoBuilder {
CryptoLogoBuilder::default()
}
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)
}
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())
}
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(¶ms.join("&"));
}
}
url
}
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
)
}
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(())
}
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");
}
}