envelope_cli/export/
yaml.rs

1//! YAML Export functionality
2//!
3//! Exports the complete database to YAML format for human-readable backup.
4
5use crate::error::EnvelopeResult;
6use crate::export::json::FullExport;
7use crate::storage::Storage;
8use std::io::Write;
9
10/// Export the full database to YAML format
11pub fn export_full_yaml<W: Write>(storage: &Storage, writer: &mut W) -> EnvelopeResult<()> {
12    let export = FullExport::from_storage(storage)?;
13
14    // Add a header comment
15    writeln!(writer, "# EnvelopeCLI Full Database Export")
16        .map_err(|e| crate::error::EnvelopeError::Export(e.to_string()))?;
17    writeln!(writer, "# Generated: {}", export.exported_at)
18        .map_err(|e| crate::error::EnvelopeError::Export(e.to_string()))?;
19    writeln!(writer, "# App Version: {}", export.app_version)
20        .map_err(|e| crate::error::EnvelopeError::Export(e.to_string()))?;
21    writeln!(writer, "#").map_err(|e| crate::error::EnvelopeError::Export(e.to_string()))?;
22    writeln!(
23        writer,
24        "# This file can be used to restore your budget data."
25    )
26    .map_err(|e| crate::error::EnvelopeError::Export(e.to_string()))?;
27    writeln!(
28        writer,
29        "# Keep it secure - it contains all your financial data."
30    )
31    .map_err(|e| crate::error::EnvelopeError::Export(e.to_string()))?;
32    writeln!(writer).map_err(|e| crate::error::EnvelopeError::Export(e.to_string()))?;
33
34    // Serialize to YAML
35    serde_yaml::to_writer(writer, &export)
36        .map_err(|e| crate::error::EnvelopeError::Export(e.to_string()))?;
37
38    Ok(())
39}
40
41/// Import from a YAML export
42pub fn import_from_yaml(yaml_str: &str) -> EnvelopeResult<FullExport> {
43    let export: FullExport = serde_yaml::from_str(yaml_str)
44        .map_err(|e| crate::error::EnvelopeError::Import(e.to_string()))?;
45
46    // Validate the import
47    export
48        .validate()
49        .map_err(crate::error::EnvelopeError::Import)?;
50
51    Ok(export)
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use crate::config::paths::EnvelopePaths;
58    use crate::models::{Account, AccountType, Category, CategoryGroup};
59    use tempfile::TempDir;
60
61    fn create_test_storage() -> (TempDir, Storage) {
62        let temp_dir = TempDir::new().unwrap();
63        let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
64        let mut storage = Storage::new(paths).unwrap();
65        storage.load_all().unwrap();
66        (temp_dir, storage)
67    }
68
69    #[test]
70    fn test_yaml_export() {
71        let (_temp_dir, storage) = create_test_storage();
72
73        // Create test data
74        let account = Account::new("Checking", AccountType::Checking);
75        storage.accounts.upsert(account).unwrap();
76        storage.accounts.save().unwrap();
77
78        let group = CategoryGroup::new("Test");
79        storage.categories.upsert_group(group.clone()).unwrap();
80        let cat = Category::new("Groceries", group.id);
81        storage.categories.upsert_category(cat).unwrap();
82        storage.categories.save().unwrap();
83
84        // Export to YAML
85        let mut yaml_output = Vec::new();
86        export_full_yaml(&storage, &mut yaml_output).unwrap();
87
88        let yaml_string = String::from_utf8(yaml_output).unwrap();
89
90        // Verify header comments
91        assert!(yaml_string.contains("# EnvelopeCLI Full Database Export"));
92
93        // Verify data
94        assert!(yaml_string.contains("Checking"));
95        assert!(yaml_string.contains("Groceries"));
96    }
97
98    #[test]
99    fn test_yaml_roundtrip() {
100        let (_temp_dir, storage) = create_test_storage();
101
102        // Create test data
103        let account = Account::new("Checking", AccountType::Checking);
104        storage.accounts.upsert(account).unwrap();
105        storage.accounts.save().unwrap();
106
107        // Export to YAML
108        let mut yaml_output = Vec::new();
109        export_full_yaml(&storage, &mut yaml_output).unwrap();
110
111        let yaml_string = String::from_utf8(yaml_output).unwrap();
112
113        // Skip the comment lines for parsing
114        let yaml_content: String = yaml_string
115            .lines()
116            .filter(|line| !line.starts_with('#'))
117            .collect::<Vec<_>>()
118            .join("\n");
119
120        // Import back
121        let imported = import_from_yaml(&yaml_content).unwrap();
122
123        assert_eq!(imported.accounts.len(), 1);
124        assert_eq!(imported.accounts[0].name, "Checking");
125    }
126}