use crate::{NylError, Result};
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub enum Component {
Helm(HelmComponent),
}
#[derive(Debug, Clone)]
pub struct HelmComponent {
pub path: PathBuf,
pub api_version: String,
pub kind: String,
}
impl HelmComponent {
pub fn chart_yaml_path(&self) -> PathBuf {
self.path.join("Chart.yaml")
}
pub fn verify(&self) -> Result<()> {
let chart_path = self.chart_yaml_path();
if !chart_path.exists() {
return Err(NylError::Config(format!(
"Chart.yaml not found at: {}",
chart_path.display()
)));
}
Ok(())
}
}
pub struct ComponentRegistry {
search_paths: Vec<PathBuf>,
cache: RefCell<HashMap<(String, String), Option<Component>>>,
}
impl ComponentRegistry {
pub fn new(search_paths: Vec<PathBuf>) -> Self {
Self {
search_paths,
cache: RefCell::new(HashMap::new()),
}
}
pub fn find_component(&self, api_version: &str, kind: &str) -> Result<Option<Component>> {
let key = (api_version.to_string(), kind.to_string());
if let Some(cached) = self.cache.borrow().get(&key) {
return Ok(cached.clone());
}
let component = self.search_component(api_version, kind)?;
self.cache.borrow_mut().insert(key, component.clone());
Ok(component)
}
fn search_component(&self, api_version: &str, kind: &str) -> Result<Option<Component>> {
for search_path in &self.search_paths {
let component_path = search_path.join(api_version).join(kind);
let chart_path = component_path.join("Chart.yaml");
if chart_path.exists() {
let component = HelmComponent {
path: component_path,
api_version: api_version.to_string(),
kind: kind.to_string(),
};
component.verify()?;
return Ok(Some(Component::Helm(component)));
}
}
Ok(None)
}
pub fn clear_cache(&self) {
self.cache.borrow_mut().clear();
}
pub fn cache_size(&self) -> usize {
self.cache.borrow().len()
}
pub fn search_paths(&self) -> &[PathBuf] {
&self.search_paths
}
pub fn list_components(&self) -> Result<Vec<Component>> {
let mut components = Vec::new();
for search_path in &self.search_paths {
let components_dir = search_path;
if !components_dir.exists() {
continue;
}
if let Ok(entries) = std::fs::read_dir(components_dir) {
for api_version_entry in entries.flatten() {
let api_version_path = api_version_entry.path();
if !api_version_path.is_dir() {
continue;
}
let api_version = api_version_entry.file_name().to_string_lossy().to_string();
if let Ok(kind_entries) = std::fs::read_dir(&api_version_path) {
for kind_entry in kind_entries.flatten() {
let kind_path = kind_entry.path();
if !kind_path.is_dir() {
continue;
}
let chart_path = kind_path.join("Chart.yaml");
if chart_path.exists() {
let kind = kind_entry.file_name().to_string_lossy().to_string();
let component = HelmComponent {
path: kind_path,
api_version: api_version.clone(),
kind: kind.clone(),
};
self.cache
.borrow_mut()
.insert((api_version.clone(), kind), Some(Component::Helm(component.clone())));
components.push(Component::Helm(component));
}
}
}
}
}
}
Ok(components)
}
}
impl std::fmt::Debug for ComponentRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ComponentRegistry")
.field("search_paths", &self.search_paths)
.field("cache_size", &self.cache_size())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn create_test_component(base: &Path, api_version: &str, kind: &str) {
let component_dir = base.join(api_version).join(kind);
fs::create_dir_all(&component_dir).unwrap();
let chart_yaml = component_dir.join("Chart.yaml");
fs::write(
chart_yaml,
format!("apiVersion: v2\nname: {}\nversion: 1.0.0\n", kind.to_lowercase()),
)
.unwrap();
}
#[test]
fn test_helm_component_chart_yaml_path() {
let component = HelmComponent {
path: PathBuf::from("/test/path"),
api_version: "v1".to_string(),
kind: "Test".to_string(),
};
assert_eq!(component.chart_yaml_path(), PathBuf::from("/test/path/Chart.yaml"));
}
#[test]
fn test_component_registry_new() {
let paths = vec![PathBuf::from("/path1"), PathBuf::from("/path2")];
let registry = ComponentRegistry::new(paths.clone());
assert_eq!(registry.search_paths, paths);
assert_eq!(registry.cache_size(), 0);
}
#[test]
fn test_find_component_success() {
let temp = TempDir::new().unwrap();
create_test_component(temp.path(), "v1.example.io", "WebApp");
let registry = ComponentRegistry::new(vec![temp.path().to_path_buf()]);
let result = registry.find_component("v1.example.io", "WebApp").unwrap();
assert!(result.is_some());
match result.unwrap() {
Component::Helm(helm) => {
assert_eq!(helm.api_version, "v1.example.io");
assert_eq!(helm.kind, "WebApp");
assert!(helm.chart_yaml_path().exists());
}
}
}
#[test]
fn test_find_component_not_found() {
let temp = TempDir::new().unwrap();
let registry = ComponentRegistry::new(vec![temp.path().to_path_buf()]);
let result = registry.find_component("v1.example.io", "Missing").unwrap();
assert!(result.is_none());
}
#[test]
fn test_find_component_caching() {
let temp = TempDir::new().unwrap();
create_test_component(temp.path(), "v1.example.io", "WebApp");
let registry = ComponentRegistry::new(vec![temp.path().to_path_buf()]);
let result1 = registry.find_component("v1.example.io", "WebApp").unwrap();
assert!(result1.is_some());
assert_eq!(registry.cache_size(), 1);
let result2 = registry.find_component("v1.example.io", "WebApp").unwrap();
assert!(result2.is_some());
assert_eq!(registry.cache_size(), 1);
let result3 = registry.find_component("v1.example.io", "Missing").unwrap();
assert!(result3.is_none());
assert_eq!(registry.cache_size(), 2);
let result4 = registry.find_component("v1.example.io", "Missing").unwrap();
assert!(result4.is_none());
assert_eq!(registry.cache_size(), 2);
}
#[test]
fn test_clear_cache() {
let temp = TempDir::new().unwrap();
create_test_component(temp.path(), "v1.example.io", "WebApp");
let registry = ComponentRegistry::new(vec![temp.path().to_path_buf()]);
registry.find_component("v1.example.io", "WebApp").unwrap();
assert_eq!(registry.cache_size(), 1);
registry.clear_cache();
assert_eq!(registry.cache_size(), 0);
}
#[test]
fn test_multiple_search_paths() {
let temp1 = TempDir::new().unwrap();
let temp2 = TempDir::new().unwrap();
create_test_component(temp1.path(), "v1.example.io", "App1");
create_test_component(temp2.path(), "v2.example.io", "App2");
let registry = ComponentRegistry::new(vec![temp1.path().to_path_buf(), temp2.path().to_path_buf()]);
let app1 = registry.find_component("v1.example.io", "App1").unwrap();
assert!(app1.is_some());
let app2 = registry.find_component("v2.example.io", "App2").unwrap();
assert!(app2.is_some());
}
#[test]
fn test_list_components() {
let temp = TempDir::new().unwrap();
create_test_component(temp.path(), "v1.example.io", "WebApp");
create_test_component(temp.path(), "v1.example.io", "Database");
create_test_component(temp.path(), "v2.example.io", "Cache");
let registry = ComponentRegistry::new(vec![temp.path().to_path_buf()]);
let components = registry.list_components().unwrap();
assert_eq!(components.len(), 3);
assert_eq!(registry.cache_size(), 3);
}
#[test]
fn test_list_components_empty_dir() {
let temp = TempDir::new().unwrap();
let registry = ComponentRegistry::new(vec![temp.path().to_path_buf()]);
let components = registry.list_components().unwrap();
assert_eq!(components.len(), 0);
}
#[test]
fn test_component_verify_missing_chart() {
let temp = TempDir::new().unwrap();
let component = HelmComponent {
path: temp.path().to_path_buf(),
api_version: "v1".to_string(),
kind: "Test".to_string(),
};
let result = component.verify();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Chart.yaml not found"));
}
}