use anyhow::{Result, anyhow};
use heck::ToPascalCase;
#[derive(Debug, Clone)]
pub struct IconIdentifier {
pub collection: String,
pub icon_name: String,
pub full_name: String,
}
impl IconIdentifier {
pub fn parse(input: &str) -> Result<Self> {
let parts: Vec<&str> = input.split(':').collect();
if parts.len() != 2 {
return Err(anyhow!(
"Invalid icon identifier format. Expected 'collection:icon-name', got '{}'",
input
));
}
let collection = parts[0].trim().to_string();
let icon_name = parts[1].trim().to_string();
if collection.is_empty() || icon_name.is_empty() {
return Err(anyhow!(
"Both collection and icon name must be non-empty in '{}'",
input
));
}
Ok(Self {
collection,
icon_name,
full_name: input.to_string(),
})
}
pub fn module_name(&self) -> String {
self.collection.replace('-', "_")
}
pub fn to_const_name(&self) -> String {
let mut const_name = self.icon_name.to_pascal_case();
if const_name.chars().next().is_some_and(|c| c.is_numeric()) {
const_name = format!("_{}", const_name);
}
if is_rust_keyword(&const_name) {
const_name.push_str("Icon");
}
const_name
}
}
fn is_rust_keyword(name: &str) -> bool {
matches!(
name.to_lowercase().as_str(),
"as" | "break"
| "const"
| "continue"
| "crate"
| "else"
| "enum"
| "extern"
| "false"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "match"
| "mod"
| "move"
| "mut"
| "pub"
| "ref"
| "return"
| "self"
| "Self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "unsafe"
| "use"
| "where"
| "while"
| "async"
| "await"
| "dyn"
)
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[test]
fn test_parse_valid_identifier() {
let id = IconIdentifier::parse("mdi:home").unwrap();
assert_eq!(id.collection, "mdi");
assert_eq!(id.icon_name, "home");
assert_eq!(id.full_name, "mdi:home");
}
#[rstest]
#[case("invalid")]
#[case("too:many:colons")]
#[case(":empty-collection")]
#[case("empty-name:")]
fn test_parse_invalid_identifier(#[case] input: &str) {
assert!(IconIdentifier::parse(input).is_err());
}
#[rstest]
#[case("mdi:home", "mdi")]
#[case("simple-icons:github", "simple_icons")]
#[case("heroicons-outline:arrow", "heroicons_outline")]
fn test_module_name(#[case] input: &str, #[case] expected: &str) {
let id = IconIdentifier::parse(input).unwrap();
assert_eq!(id.module_name(), expected);
}
#[rstest]
#[case("mdi:home", "Home")]
#[case("heroicons:arrow-left", "ArrowLeft")]
#[case("lucide:shopping-cart", "ShoppingCart")]
#[case("mdi:numeric-1-box", "Numeric1Box")]
#[case("mdi:1password", "_1password")] #[case("mdi:type", "TypeIcon")] fn test_to_const_name(#[case] input: &str, #[case] expected: &str) {
let id = IconIdentifier::parse(input).unwrap();
assert_eq!(id.to_const_name(), expected);
}
}