use crate::{ImportResult, Importer};
use anyhow::{Context, Result};
use std::path::Path;
use std::sync::Arc;
pub struct ImporterRegistry {
importers: Vec<Arc<dyn Importer>>,
}
impl ImporterRegistry {
pub fn new() -> Self {
Self {
importers: Vec::new(),
}
}
pub fn register(&mut self, importer: impl Importer + 'static) {
self.importers.push(Arc::new(importer));
}
pub fn identify(&self, path: &Path) -> Option<Arc<dyn Importer>> {
for importer in &self.importers {
if importer.identify(path) {
return Some(Arc::clone(importer));
}
}
None
}
pub fn extract(&self, path: &Path) -> Result<ImportResult> {
let importer = self
.identify(path)
.with_context(|| format!("No importer found for file: {}", path.display()))?;
importer
.extract(path)
.with_context(|| format!("Failed to extract from: {}", path.display()))
}
pub fn list_importers(&self) -> Vec<(&str, &str)> {
self.importers
.iter()
.map(|i| (i.name(), i.description()))
.collect()
}
pub fn len(&self) -> usize {
self.importers.len()
}
pub fn is_empty(&self) -> bool {
self.importers.is_empty()
}
}
impl Default for ImporterRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockImporter {
name: &'static str,
extension: &'static str,
}
impl Importer for MockImporter {
fn name(&self) -> &str {
self.name
}
fn identify(&self, path: &Path) -> bool {
path.extension().is_some_and(|ext| ext == self.extension)
}
fn extract(&self, _path: &Path) -> Result<ImportResult> {
Ok(ImportResult::empty())
}
fn description(&self) -> &'static str {
"Mock importer for testing"
}
}
#[test]
fn test_registry_basic() {
let mut registry = ImporterRegistry::new();
assert!(registry.is_empty());
registry.register(MockImporter {
name: "CSV",
extension: "csv",
});
registry.register(MockImporter {
name: "OFX",
extension: "ofx",
});
assert_eq!(registry.len(), 2);
assert!(!registry.is_empty());
}
#[test]
fn test_registry_identify() {
let mut registry = ImporterRegistry::new();
registry.register(MockImporter {
name: "CSV",
extension: "csv",
});
registry.register(MockImporter {
name: "OFX",
extension: "ofx",
});
let csv_path = Path::new("transactions.csv");
let ofx_path = Path::new("statement.ofx");
let unknown_path = Path::new("document.pdf");
assert!(registry.identify(csv_path).is_some());
assert_eq!(registry.identify(csv_path).unwrap().name(), "CSV");
assert!(registry.identify(ofx_path).is_some());
assert_eq!(registry.identify(ofx_path).unwrap().name(), "OFX");
assert!(registry.identify(unknown_path).is_none());
}
#[test]
fn test_registry_default() {
let registry = ImporterRegistry::default();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
}
#[test]
fn test_registry_list_importers() {
let mut registry = ImporterRegistry::new();
registry.register(MockImporter {
name: "CSV",
extension: "csv",
});
registry.register(MockImporter {
name: "OFX",
extension: "ofx",
});
let list = registry.list_importers();
assert_eq!(list.len(), 2);
assert!(list.iter().any(|(name, _)| *name == "CSV"));
assert!(list.iter().any(|(name, _)| *name == "OFX"));
for (_, desc) in &list {
assert_eq!(*desc, "Mock importer for testing");
}
}
#[test]
fn test_registry_extract_unknown_file() {
let registry = ImporterRegistry::new();
let unknown_path = Path::new("document.pdf");
let result = registry.extract(unknown_path);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No importer found"));
}
#[test]
fn test_registry_identify_returns_first_match() {
let mut registry = ImporterRegistry::new();
registry.register(MockImporter {
name: "CSV1",
extension: "csv",
});
registry.register(MockImporter {
name: "CSV2",
extension: "csv",
});
let csv_path = Path::new("transactions.csv");
let importer = registry.identify(csv_path).unwrap();
assert_eq!(importer.name(), "CSV1");
}
#[test]
fn test_registry_empty_list_importers() {
let registry = ImporterRegistry::new();
let list = registry.list_importers();
assert!(list.is_empty());
}
}