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