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}