Skip to main content

rustledger_plugin/native/plugins/
auto_accounts.rs

1//! Auto-generate Open directives for accounts used without explicit open.
2
3use crate::types::{
4    DirectiveData, DirectiveWrapper, OpenData, PluginInput, PluginOp, PluginOutput,
5};
6
7use super::super::NativePlugin;
8
9/// Plugin that auto-generates Open directives for accounts used without explicit open.
10pub struct AutoAccountsPlugin;
11
12impl NativePlugin for AutoAccountsPlugin {
13    fn name(&self) -> &'static str {
14        "auto_accounts"
15    }
16
17    fn description(&self) -> &'static str {
18        "Auto-generate Open directives for used accounts"
19    }
20
21    /// Synthesizes `Open` directives the early validator needs to
22    /// see — must run pre-booking to suppress spurious E1001 errors
23    /// on accounts the plugin will auto-create.
24    fn is_synth(&self) -> bool {
25        true
26    }
27
28    fn process(&self, input: PluginInput) -> PluginOutput {
29        use std::collections::{HashMap, HashSet};
30
31        let mut opened_accounts: HashSet<String> = HashSet::new();
32        let mut account_first_use: HashMap<String, String> = HashMap::new(); // account -> earliest date
33
34        // First pass: find all open directives and EARLIEST use of each account
35        // (directives may not be in date order in the input)
36        for wrapper in &input.directives {
37            match &wrapper.data {
38                DirectiveData::Open(data) => {
39                    opened_accounts.insert(data.account.clone());
40                }
41                DirectiveData::Transaction(txn) => {
42                    for posting in &txn.postings {
43                        account_first_use
44                            .entry(posting.account.clone())
45                            .and_modify(|existing| {
46                                if wrapper.date < *existing {
47                                    existing.clone_from(&wrapper.date);
48                                }
49                            })
50                            .or_insert_with(|| wrapper.date.clone());
51                    }
52                }
53                DirectiveData::Balance(data) => {
54                    account_first_use
55                        .entry(data.account.clone())
56                        .and_modify(|existing| {
57                            if wrapper.date < *existing {
58                                existing.clone_from(&wrapper.date);
59                            }
60                        })
61                        .or_insert_with(|| wrapper.date.clone());
62                }
63                DirectiveData::Pad(data) => {
64                    account_first_use
65                        .entry(data.account.clone())
66                        .and_modify(|existing| {
67                            if wrapper.date < *existing {
68                                existing.clone_from(&wrapper.date);
69                            }
70                        })
71                        .or_insert_with(|| wrapper.date.clone());
72                    account_first_use
73                        .entry(data.source_account.clone())
74                        .and_modify(|existing| {
75                            if wrapper.date < *existing {
76                                existing.clone_from(&wrapper.date);
77                            }
78                        })
79                        .or_insert_with(|| wrapper.date.clone());
80                }
81                _ => {}
82            }
83        }
84
85        // Generate open directives for accounts without explicit open
86        // Sort accounts for deterministic ordering (matches Python beancount behavior)
87        let mut accounts_to_open: Vec<_> = account_first_use
88            .iter()
89            .filter(|(account, _)| !opened_accounts.contains(*account))
90            .collect();
91        accounts_to_open.sort_by_key(|(account, _)| *account);
92
93        // Start with Keep ops for every input directive (preserves spans).
94        let mut ops: Vec<PluginOp> = (0..input.directives.len()).map(PluginOp::Keep).collect();
95
96        // Insert synthesized Open directives for accounts without explicit open.
97        for (index, (account, date)) in accounts_to_open.into_iter().enumerate() {
98            ops.push(PluginOp::Insert(DirectiveWrapper {
99                directive_type: "open".to_string(),
100                date: date.clone(),
101                filename: Some("<auto_accounts>".to_string()),
102                lineno: Some(index as u32), // Use index as lineno for deterministic sorting
103                data: DirectiveData::Open(OpenData {
104                    account: account.clone(),
105                    currencies: vec![],
106                    booking: None,
107                    metadata: vec![],
108                }),
109            }));
110        }
111
112        // Final ordering is the loader's responsibility — it re-sorts
113        // directives after the plugin pass.
114        PluginOutput {
115            ops,
116            errors: Vec::new(),
117        }
118    }
119}