#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod config;
pub mod csv_importer;
pub mod ofx_importer;
pub mod registry;
use anyhow::Result;
use rustledger_core::Directive;
use std::path::Path;
pub use config::ImporterConfig;
pub use ofx_importer::OfxImporter;
pub use registry::ImporterRegistry;
#[derive(Debug, Clone)]
pub struct ImportResult {
pub directives: Vec<Directive>,
pub warnings: Vec<String>,
}
impl ImportResult {
pub const fn new(directives: Vec<Directive>) -> Self {
Self {
directives,
warnings: Vec::new(),
}
}
pub const fn empty() -> Self {
Self {
directives: Vec::new(),
warnings: Vec::new(),
}
}
pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
self.warnings.push(warning.into());
self
}
}
pub trait Importer: Send + Sync {
fn name(&self) -> &str;
fn identify(&self, path: &Path) -> bool;
fn extract(&self, path: &Path) -> Result<ImportResult>;
fn description(&self) -> &str {
self.name()
}
}
pub fn extract_from_file(path: &Path, config: &ImporterConfig) -> Result<ImportResult> {
config.extract(path)
}
pub fn extract_from_string(content: &str, config: &ImporterConfig) -> Result<ImportResult> {
config.extract_from_string(content)
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::Decimal;
use rustledger_core::{Amount, Posting, Transaction};
use std::str::FromStr;
#[test]
fn test_import_result_new() {
let directives = vec![];
let result = ImportResult::new(directives);
assert!(result.directives.is_empty());
assert!(result.warnings.is_empty());
}
#[test]
fn test_import_result_empty() {
let result = ImportResult::empty();
assert!(result.directives.is_empty());
assert!(result.warnings.is_empty());
}
#[test]
fn test_import_result_with_warning() {
let result = ImportResult::empty().with_warning("Test warning");
assert_eq!(result.warnings.len(), 1);
assert_eq!(result.warnings[0], "Test warning");
}
#[test]
fn test_import_result_multiple_warnings() {
let result = ImportResult::empty()
.with_warning("Warning 1")
.with_warning("Warning 2");
assert_eq!(result.warnings.len(), 2);
assert_eq!(result.warnings[0], "Warning 1");
assert_eq!(result.warnings[1], "Warning 2");
}
#[test]
fn test_import_result_with_directives() {
let date = chrono::NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
let txn = Transaction::new(date, "Test transaction")
.with_posting(Posting::new(
"Assets:Bank",
Amount::new(Decimal::from_str("100").unwrap(), "USD"),
))
.with_posting(Posting::new(
"Expenses:Food",
Amount::new(Decimal::from_str("-100").unwrap(), "USD"),
));
let directives = vec![Directive::Transaction(txn)];
let result = ImportResult::new(directives);
assert_eq!(result.directives.len(), 1);
}
#[test]
fn test_extract_from_string_csv() {
let config = ImporterConfig::csv()
.account("Assets:Bank:Checking")
.currency("USD")
.date_column("Date")
.narration_column("Description")
.amount_column("Amount")
.build();
let csv_content = "Date,Description,Amount\n2024-01-15,Coffee,-5.00\n";
let result = extract_from_string(csv_content, &config).unwrap();
assert_eq!(result.directives.len(), 1);
}
#[test]
fn test_extract_from_string_empty_csv() {
let config = ImporterConfig::csv()
.account("Assets:Bank:Checking")
.currency("USD")
.date_column("Date")
.narration_column("Description")
.amount_column("Amount")
.build();
let csv_content = "Date,Description,Amount\n";
let result = extract_from_string(csv_content, &config).unwrap();
assert!(result.directives.is_empty());
}
#[test]
fn test_import_result_debug() {
let result = ImportResult::empty();
let debug_str = format!("{result:?}");
assert!(debug_str.contains("ImportResult"));
}
#[test]
fn test_import_result_clone() {
let result = ImportResult::empty().with_warning("Test");
let cloned = result.clone();
assert_eq!(result.warnings.len(), 1);
assert_eq!(cloned.warnings.len(), 1);
}
}