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, PluginOutput, sort_directives,
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    fn process(&self, input: PluginInput) -> PluginOutput {
22        use std::collections::{HashMap, HashSet};
23
24        let mut opened_accounts: HashSet<String> = HashSet::new();
25        let mut account_first_use: HashMap<String, String> = HashMap::new(); // account -> earliest date
26
27        // First pass: find all open directives and EARLIEST use of each account
28        // (directives may not be in date order in the input)
29        for wrapper in &input.directives {
30            match &wrapper.data {
31                DirectiveData::Open(data) => {
32                    opened_accounts.insert(data.account.clone());
33                }
34                DirectiveData::Transaction(txn) => {
35                    for posting in &txn.postings {
36                        account_first_use
37                            .entry(posting.account.clone())
38                            .and_modify(|existing| {
39                                if wrapper.date < *existing {
40                                    existing.clone_from(&wrapper.date);
41                                }
42                            })
43                            .or_insert_with(|| wrapper.date.clone());
44                    }
45                }
46                DirectiveData::Balance(data) => {
47                    account_first_use
48                        .entry(data.account.clone())
49                        .and_modify(|existing| {
50                            if wrapper.date < *existing {
51                                existing.clone_from(&wrapper.date);
52                            }
53                        })
54                        .or_insert_with(|| wrapper.date.clone());
55                }
56                DirectiveData::Pad(data) => {
57                    account_first_use
58                        .entry(data.account.clone())
59                        .and_modify(|existing| {
60                            if wrapper.date < *existing {
61                                existing.clone_from(&wrapper.date);
62                            }
63                        })
64                        .or_insert_with(|| wrapper.date.clone());
65                    account_first_use
66                        .entry(data.source_account.clone())
67                        .and_modify(|existing| {
68                            if wrapper.date < *existing {
69                                existing.clone_from(&wrapper.date);
70                            }
71                        })
72                        .or_insert_with(|| wrapper.date.clone());
73                }
74                _ => {}
75            }
76        }
77
78        // Generate open directives for accounts without explicit open
79        // Sort accounts for deterministic ordering (matches Python beancount behavior)
80        let mut accounts_to_open: Vec<_> = account_first_use
81            .iter()
82            .filter(|(account, _)| !opened_accounts.contains(*account))
83            .collect();
84        accounts_to_open.sort_by_key(|(account, _)| *account);
85
86        let mut new_directives: Vec<DirectiveWrapper> = Vec::new();
87        for (index, (account, date)) in accounts_to_open.into_iter().enumerate() {
88            new_directives.push(DirectiveWrapper {
89                directive_type: "open".to_string(),
90                date: date.clone(),
91                filename: Some("<auto_accounts>".to_string()),
92                lineno: Some(index as u32), // Use index as lineno for deterministic sorting
93                data: DirectiveData::Open(OpenData {
94                    account: account.clone(),
95                    currencies: vec![],
96                    booking: None,
97                    metadata: vec![],
98                }),
99            });
100        }
101
102        // Add existing directives
103        new_directives.extend(input.directives);
104
105        // Sort using beancount's standard ordering: date, type order, line number
106        sort_directives(&mut new_directives);
107
108        PluginOutput {
109            directives: new_directives,
110            errors: Vec::new(),
111        }
112    }
113}