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::blocking::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::blocking::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 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.client.get(&url).send().context(format!(
55            "Failed to fetch icon {
56
57}:{}",
58            collection, icon_name
59        ))?;
60
61        if !response.status().is_success() {
62            return Err(anyhow!(
63                "API request failed with status {}: {}",
64                response.status(),
65                response.text().unwrap_or_default()
66            ));
67        }
68
69        let api_response: IconifyApiResponse =
70            response.json().context("Failed to parse API response")?;
71
72        let icon = api_response
73            .icons
74            .get(icon_name)
75            .ok_or_else(|| {
76                anyhow!(
77                    "Icon '{}' not found in collection '{}'",
78                    icon_name,
79                    collection
80                )
81            })?
82            .clone();
83
84        // Use icon-specific dimensions or fall back to collection defaults
85        let width = icon.width.or(api_response.width).unwrap_or(24);
86        let height = icon.height.or(api_response.height).unwrap_or(24);
87
88        // Generate viewBox if not provided
89        let view_box = icon
90            .view_box
91            .clone()
92            .unwrap_or_else(|| format!("0 0 {} {}", width, height));
93
94        Ok(IconifyIcon {
95            body: icon.body,
96            width: Some(width),
97            height: Some(height),
98            view_box: Some(view_box),
99        })
100    }
101
102    // /// Fetch multiple icons from the same collection
103    // pub fn fetch_icons(
104    //     &self,
105    //     collection: &str,
106    //     icon_names: &[String],
107    // ) -> Result<HashMap<String, IconifyIcon>> {
108    //     if icon_names.is_empty() {
109    //         return Ok(HashMap::new());
110    //     }
111
112    //     let icons_param = icon_names.join(",");
113    //     let url = format!(
114    //         "{}/{}.json?icons={}",
115    //         self.base_url, collection, icons_param
116    //     );
117
118    //     let response = self.client.get(&url).send().context(format!(
119    //         "Failed to fetch icons from collection '{}'",
120    //         collection
121    //     ))?;
122
123    //     if !response.status().is_success() {
124    //         return Err(anyhow!(
125    //             "API request failed with status {}: {}",
126    //             response.status(),
127    //             response.text().unwrap_or_default()
128    //         ));
129    //     }
130
131    //     let api_response: IconifyApiResponse =
132    //         response.json().context("Failed to parse API response")?;
133
134    //     let default_width = api_response.width.unwrap_or(24);
135    //     let default_height = api_response.height.unwrap_or(24);
136
137    //     // Process each icon and ensure they have dimensions
138    //     let mut result = HashMap::new();
139    //     for (name, mut icon) in api_response.icons {
140    //         let width = icon.width.unwrap_or(default_width);
141    //         let height = icon.height.unwrap_or(default_height);
142
143    //         icon.width = Some(width);
144    //         icon.height = Some(height);
145    //         icon.view_box = Some(
146    //             icon.view_box
147    //                 .clone()
148    //                 .unwrap_or_else(|| format!("0 0 {} {}", width, height)),
149    //         );
150
151    //         result.insert(name, icon);
152    //     }
153
154    //     Ok(result)
155    // }
156}
157
158impl Default for IconifyClient {
159    fn default() -> Self {
160        Self::new().expect("Failed to create Iconify API client")
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use rstest::rstest;
168
169    #[rstest]
170    #[case("mdi", "home")]
171    #[case("heroicons", "arrow-left")]
172    #[case("lucide", "settings")]
173    #[ignore] // Requires internet connection
174    fn test_fetch_icon(#[case] collection: &str, #[case] icon_name: &str) {
175        let client = IconifyClient::new().unwrap();
176        let icon = client.fetch_icon(collection, icon_name).unwrap();
177
178        assert!(!icon.body.is_empty());
179        assert!(icon.width.is_some());
180        assert!(icon.height.is_some());
181        assert!(icon.view_box.is_some());
182    }
183
184    #[test]
185    #[ignore] // Requires internet connection
186    fn test_fetch_nonexistent_icon() {
187        let client = IconifyClient::new().unwrap();
188        let result = client.fetch_icon("mdi", "this-icon-does-not-exist-12345");
189
190        assert!(result.is_err());
191    }
192}