dioxus_iconify/
api.rs

1use anyhow::{Context, Result, anyhow};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5const API_BASE_URL: &str = "https://api.iconify.design";
6
7/// Icon data returned from the Iconify API
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct IconifyIcon {
10    pub body: String,
11    #[serde(default)]
12    pub width: Option<u32>,
13    #[serde(default)]
14    pub height: Option<u32>,
15    #[serde(default, rename = "viewBox")]
16    pub view_box: Option<String>,
17}
18
19/// API response structure for icon data
20#[derive(Debug, Deserialize)]
21struct IconifyApiResponse {
22    // prefix: String,
23    icons: HashMap<String, IconifyIcon>,
24    #[serde(default)]
25    width: Option<u32>,
26    #[serde(default)]
27    height: Option<u32>,
28}
29
30/// Iconify API client
31pub struct IconifyClient {
32    client: reqwest::Client,
33    base_url: String,
34}
35
36impl IconifyClient {
37    /// Create a new Iconify API client
38    pub fn new() -> Result<Self> {
39        let client = reqwest::Client::builder()
40            .timeout(std::time::Duration::from_secs(30))
41            .build()
42            .context("Failed to create HTTP client")?;
43
44        Ok(Self {
45            client,
46            base_url: API_BASE_URL.to_string(),
47        })
48    }
49
50    /// Fetch a single icon from the Iconify API
51    pub async fn fetch_icon(&self, collection: &str, icon_name: &str) -> Result<IconifyIcon> {
52        let url = format!("{}/{}.json?icons={}", self.base_url, collection, icon_name);
53
54        let response = self
55            .client
56            .get(&url)
57            .send()
58            .await
59            .context(format!("Failed to fetch icon {}:{}", collection, icon_name))?;
60
61        if !response.status().is_success() {
62            let status = response.status();
63            let text = response.text().await.unwrap_or_default();
64            return Err(anyhow!(
65                "API request failed with status {}: {}",
66                status,
67                text
68            ));
69        }
70
71        let api_response: IconifyApiResponse = response
72            .json()
73            .await
74            .context("Failed to parse API response")?;
75
76        let icon = api_response
77            .icons
78            .get(icon_name)
79            .ok_or_else(|| {
80                anyhow!(
81                    "Icon '{}' not found in collection '{}'",
82                    icon_name,
83                    collection
84                )
85            })?
86            .clone();
87
88        // Use icon-specific dimensions or fall back to collection defaults
89        let width = icon.width.or(api_response.width).unwrap_or(24);
90        let height = icon.height.or(api_response.height).unwrap_or(24);
91
92        // Generate viewBox if not provided
93        let view_box = icon
94            .view_box
95            .clone()
96            .unwrap_or_else(|| format!("0 0 {} {}", width, height));
97
98        Ok(IconifyIcon {
99            body: icon.body,
100            width: Some(width),
101            height: Some(height),
102            view_box: Some(view_box),
103        })
104    }
105
106    // /// Fetch multiple icons from the same collection
107    // pub fn fetch_icons(
108    //     &self,
109    //     collection: &str,
110    //     icon_names: &[String],
111    // ) -> Result<HashMap<String, IconifyIcon>> {
112    //     if icon_names.is_empty() {
113    //         return Ok(HashMap::new());
114    //     }
115
116    //     let icons_param = icon_names.join(",");
117    //     let url = format!(
118    //         "{}/{}.json?icons={}",
119    //         self.base_url, collection, icons_param
120    //     );
121
122    //     let response = self.client.get(&url).send().context(format!(
123    //         "Failed to fetch icons from collection '{}'",
124    //         collection
125    //     ))?;
126
127    //     if !response.status().is_success() {
128    //         return Err(anyhow!(
129    //             "API request failed with status {}: {}",
130    //             response.status(),
131    //             response.text().unwrap_or_default()
132    //         ));
133    //     }
134
135    //     let api_response: IconifyApiResponse =
136    //         response.json().context("Failed to parse API response")?;
137
138    //     let default_width = api_response.width.unwrap_or(24);
139    //     let default_height = api_response.height.unwrap_or(24);
140
141    //     // Process each icon and ensure they have dimensions
142    //     let mut result = HashMap::new();
143    //     for (name, mut icon) in api_response.icons {
144    //         let width = icon.width.unwrap_or(default_width);
145    //         let height = icon.height.unwrap_or(default_height);
146
147    //         icon.width = Some(width);
148    //         icon.height = Some(height);
149    //         icon.view_box = Some(
150    //             icon.view_box
151    //                 .clone()
152    //                 .unwrap_or_else(|| format!("0 0 {} {}", width, height)),
153    //         );
154
155    //         result.insert(name, icon);
156    //     }
157
158    //     Ok(result)
159    // }
160}
161
162impl Default for IconifyClient {
163    fn default() -> Self {
164        Self::new().expect("Failed to create Iconify API client")
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use rstest::rstest;
172
173    #[rstest]
174    #[case("mdi", "home")]
175    #[case("heroicons", "arrow-left")]
176    #[case("lucide", "settings")]
177    #[ignore] // Requires internet connection
178    #[tokio::test]
179    async fn test_fetch_icon(#[case] collection: &str, #[case] icon_name: &str) {
180        let client = IconifyClient::new().unwrap();
181        let icon = client.fetch_icon(collection, icon_name).await.unwrap();
182
183        assert!(!icon.body.is_empty());
184        assert!(icon.width.is_some());
185        assert!(icon.height.is_some());
186        assert!(icon.view_box.is_some());
187    }
188
189    #[tokio::test]
190    #[ignore] // Requires internet connection
191    async fn test_fetch_nonexistent_icon() {
192        let client = IconifyClient::new().unwrap();
193        let result = client
194            .fetch_icon("mdi", "this-icon-does-not-exist-12345")
195            .await;
196
197        assert!(result.is_err());
198    }
199}