use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct HierarchyConfig {
pub families: Vec<Family>,
pub subcategories: Vec<Subcategory>,
compiled_patterns: Vec<CompiledPattern>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Family {
pub id: String,
pub name: String,
pub icon: String,
pub order: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortBy {
Date,
#[default]
Name,
}
#[derive(Debug, Clone)]
pub struct Subcategory {
pub id: String,
pub name: String,
pub family: String,
pub order: i32,
pub patterns: Vec<String>,
pub sort_by: SortBy,
}
#[derive(Debug, Clone)]
struct CompiledPattern {
regex: Regex,
family_id: String,
subcategory_id: String,
}
#[derive(Debug, Deserialize)]
struct HierarchyToml {
families: HashMap<String, FamilyToml>,
subcategories: HashMap<String, SubcategoryToml>,
}
#[derive(Debug, Deserialize)]
struct FamilyToml {
name: String,
icon: String,
order: i32,
}
#[derive(Debug, Deserialize)]
struct SubcategoryToml {
name: String,
family: String,
order: i32,
patterns: Vec<String>,
#[serde(default)]
sort_by: Option<String>,
}
impl HierarchyConfig {
pub fn load_embedded() -> Self {
let toml_str = include_str!("../../assets/metadata/hierarchy.toml");
Self::parse_toml(toml_str).unwrap_or_default()
}
#[allow(dead_code)]
pub fn load_from_file(path: &std::path::Path) -> Option<Self> {
let content = std::fs::read_to_string(path).ok()?;
Self::parse_toml(&content).ok()
}
fn parse_toml(content: &str) -> Result<Self, Box<dyn std::error::Error>> {
let raw: HierarchyToml = toml::from_str(content)?;
let mut families: Vec<Family> = raw.families
.into_iter()
.map(|(id, f)| Family {
id,
name: f.name,
icon: f.icon,
order: f.order,
})
.collect();
families.sort_by_key(|f| f.order);
let mut subcategories: Vec<Subcategory> = raw.subcategories
.into_iter()
.map(|(id, s)| {
let sort_by = match s.sort_by.as_deref() {
Some("date") => SortBy::Date,
Some("name") | None => SortBy::Name,
_ => SortBy::Name,
};
Subcategory {
id,
name: s.name,
family: s.family,
order: s.order,
patterns: s.patterns,
sort_by,
}
})
.collect();
subcategories.sort_by_key(|s| (s.family.clone(), s.order));
let mut compiled_patterns = Vec::new();
for subcat in &subcategories {
for pattern in &subcat.patterns {
if let Ok(regex) = Regex::new(&format!("(?i){}", pattern)) {
compiled_patterns.push(CompiledPattern {
regex,
family_id: subcat.family.clone(),
subcategory_id: subcat.id.clone(),
});
}
}
}
Ok(Self {
families,
subcategories,
compiled_patterns,
})
}
pub fn categorize(&self, vm_id: &str) -> (String, String) {
for cp in &self.compiled_patterns {
if cp.regex.is_match(vm_id) {
return (cp.family_id.clone(), cp.subcategory_id.clone());
}
}
("other".to_string(), "uncategorized".to_string())
}
#[allow(dead_code)]
pub fn get_family(&self, id: &str) -> Option<&Family> {
self.families.iter().find(|f| f.id == id)
}
pub fn get_subcategory(&self, id: &str) -> Option<&Subcategory> {
self.subcategories.iter().find(|s| s.id == id)
}
pub fn subcategories_for_family(&self, family_id: &str) -> Vec<&Subcategory> {
self.subcategories
.iter()
.filter(|s| s.family == family_id)
.collect()
}
}