use anyhow::{Context, Result, anyhow};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
const API_BASE_URL: &str = "https://api.iconify.design";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IconifyIcon {
pub body: String,
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
#[serde(default, rename = "viewBox")]
pub view_box: Option<String>,
}
#[derive(Debug, Deserialize)]
struct IconifyApiResponse {
icons: HashMap<String, IconifyIcon>,
#[serde(default)]
width: Option<u32>,
#[serde(default)]
height: Option<u32>,
}
pub struct IconifyClient {
client: reqwest::Client,
base_url: String,
}
impl IconifyClient {
pub fn new() -> Result<Self> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.context("Failed to create HTTP client")?;
Ok(Self {
client,
base_url: API_BASE_URL.to_string(),
})
}
pub async fn fetch_icon(&self, collection: &str, icon_name: &str) -> Result<IconifyIcon> {
let url = format!("{}/{}.json?icons={}", self.base_url, collection, icon_name);
let response = self
.client
.get(&url)
.send()
.await
.context(format!("Failed to fetch icon {}:{}", collection, icon_name))?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
return Err(anyhow!(
"API request failed with status {}: {}",
status,
text
));
}
let api_response: IconifyApiResponse = response
.json()
.await
.context("Failed to parse API response")?;
let icon = api_response
.icons
.get(icon_name)
.ok_or_else(|| {
anyhow!(
"Icon '{}' not found in collection '{}'",
icon_name,
collection
)
})?
.clone();
let width = icon.width.or(api_response.width).unwrap_or(24);
let height = icon.height.or(api_response.height).unwrap_or(24);
let view_box = icon
.view_box
.clone()
.unwrap_or_else(|| format!("0 0 {} {}", width, height));
Ok(IconifyIcon {
body: icon.body,
width: Some(width),
height: Some(height),
view_box: Some(view_box),
})
}
}
impl Default for IconifyClient {
fn default() -> Self {
Self::new().expect("Failed to create Iconify API client")
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case("mdi", "home")]
#[case("heroicons", "arrow-left")]
#[case("lucide", "settings")]
#[ignore] #[tokio::test]
async fn test_fetch_icon(#[case] collection: &str, #[case] icon_name: &str) {
let client = IconifyClient::new().unwrap();
let icon = client.fetch_icon(collection, icon_name).await.unwrap();
assert!(!icon.body.is_empty());
assert!(icon.width.is_some());
assert!(icon.height.is_some());
assert!(icon.view_box.is_some());
}
#[tokio::test]
#[ignore] async fn test_fetch_nonexistent_icon() {
let client = IconifyClient::new().unwrap();
let result = client
.fetch_icon("mdi", "this-icon-does-not-exist-12345")
.await;
assert!(result.is_err());
}
}