1use anyhow::{Result, anyhow};
2use heck::ToPascalCase;
3
4#[derive(Debug, Clone)]
6pub struct IconIdentifier {
7 pub collection: String,
8 pub icon_name: String,
9 pub full_name: String,
10}
11
12impl IconIdentifier {
13 pub fn parse(input: &str) -> Result<Self> {
15 let parts: Vec<&str> = input.split(':').collect();
16 if parts.len() != 2 {
17 return Err(anyhow!(
18 "Invalid icon identifier format. Expected 'collection:icon-name', got '{}'",
19 input
20 ));
21 }
22
23 let collection = parts[0].trim().to_string();
24 let icon_name = parts[1].trim().to_string();
25
26 if collection.is_empty() || icon_name.is_empty() {
27 return Err(anyhow!(
28 "Both collection and icon name must be non-empty in '{}'",
29 input
30 ));
31 }
32
33 Ok(Self {
34 collection,
35 icon_name,
36 full_name: input.to_string(),
37 })
38 }
39
40 pub fn module_name(&self) -> String {
42 self.collection.replace('-', "_")
43 }
44
45 pub fn to_const_name(&self) -> String {
47 let mut const_name = self.icon_name.to_pascal_case();
49
50 if const_name.chars().next().is_some_and(|c| c.is_numeric()) {
52 const_name = format!("_{}", const_name);
53 }
54
55 if is_rust_keyword(&const_name) {
57 const_name.push_str("Icon");
58 }
59
60 const_name
61 }
62}
63
64fn is_rust_keyword(name: &str) -> bool {
66 matches!(
67 name.to_lowercase().as_str(),
68 "as" | "break"
69 | "const"
70 | "continue"
71 | "crate"
72 | "else"
73 | "enum"
74 | "extern"
75 | "false"
76 | "fn"
77 | "for"
78 | "if"
79 | "impl"
80 | "in"
81 | "let"
82 | "loop"
83 | "match"
84 | "mod"
85 | "move"
86 | "mut"
87 | "pub"
88 | "ref"
89 | "return"
90 | "self"
91 | "Self"
92 | "static"
93 | "struct"
94 | "super"
95 | "trait"
96 | "true"
97 | "type"
98 | "unsafe"
99 | "use"
100 | "where"
101 | "while"
102 | "async"
103 | "await"
104 | "dyn"
105 )
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use rstest::rstest;
112
113 #[test]
114 fn test_parse_valid_identifier() {
115 let id = IconIdentifier::parse("mdi:home").unwrap();
116 assert_eq!(id.collection, "mdi");
117 assert_eq!(id.icon_name, "home");
118 assert_eq!(id.full_name, "mdi:home");
119 }
120
121 #[rstest]
122 #[case("invalid")]
123 #[case("too:many:colons")]
124 #[case(":empty-collection")]
125 #[case("empty-name:")]
126 fn test_parse_invalid_identifier(#[case] input: &str) {
127 assert!(IconIdentifier::parse(input).is_err());
128 }
129
130 #[rstest]
131 #[case("mdi:home", "mdi")]
132 #[case("simple-icons:github", "simple_icons")]
133 #[case("heroicons-outline:arrow", "heroicons_outline")]
134 fn test_module_name(#[case] input: &str, #[case] expected: &str) {
135 let id = IconIdentifier::parse(input).unwrap();
136 assert_eq!(id.module_name(), expected);
137 }
138
139 #[rstest]
140 #[case("mdi:home", "Home")]
141 #[case("heroicons:arrow-left", "ArrowLeft")]
142 #[case("lucide:shopping-cart", "ShoppingCart")]
143 #[case("mdi:numeric-1-box", "Numeric1Box")]
144 #[case("mdi:1password", "_1password")] #[case("mdi:type", "TypeIcon")] fn test_to_const_name(#[case] input: &str, #[case] expected: &str) {
147 let id = IconIdentifier::parse(input).unwrap();
148 assert_eq!(id.to_const_name(), expected);
149 }
150}