Skip to main content

rustledger_plugin/native/plugins/
check_commodity.rs

1//! Plugin that checks all used commodities are declared.
2
3use std::collections::HashSet;
4
5use crate::types::{DirectiveData, PluginError, PluginInput, PluginOp, PluginOutput};
6
7use super::super::{NativePlugin, RegularPlugin};
8
9/// Plugin that checks all used commodities are declared.
10pub struct CheckCommodityPlugin;
11
12impl NativePlugin for CheckCommodityPlugin {
13    fn name(&self) -> &'static str {
14        "check_commodity"
15    }
16
17    fn description(&self) -> &'static str {
18        "Verify all commodities are declared"
19    }
20
21    fn process(&self, input: PluginInput) -> PluginOutput {
22        let mut declared_commodities: HashSet<String> = HashSet::new();
23        let mut used_commodities: HashSet<String> = HashSet::new();
24        let mut errors = Vec::new();
25
26        // First pass: collect declared commodities
27        for wrapper in &input.directives {
28            if wrapper.directive_type == "commodity"
29                && let DirectiveData::Commodity(ref comm) = wrapper.data
30            {
31                declared_commodities.insert(comm.currency.clone());
32            }
33        }
34
35        // Second pass: collect used commodities and check
36        for wrapper in &input.directives {
37            match &wrapper.data {
38                DirectiveData::Transaction(txn) => {
39                    for posting in &txn.postings {
40                        if let Some(ref units) = posting.units {
41                            used_commodities.insert(units.currency.clone());
42                        }
43                        if let Some(ref cost) = posting.cost
44                            && let Some(ref currency) = cost.currency
45                        {
46                            used_commodities.insert(currency.clone());
47                        }
48                    }
49                }
50                DirectiveData::Balance(bal) => {
51                    used_commodities.insert(bal.amount.currency.clone());
52                }
53                DirectiveData::Price(price) => {
54                    used_commodities.insert(price.currency.clone());
55                    used_commodities.insert(price.amount.currency.clone());
56                }
57                _ => {}
58            }
59        }
60
61        // Report undeclared commodities. `used_commodities` is a `HashSet`,
62        // so collect-and-sort to give a deterministic warning order
63        // (iterating the set directly leaked hash order into the output —
64        // see #1235).
65        let mut undeclared: Vec<&String> = used_commodities
66            .iter()
67            .filter(|currency| !declared_commodities.contains(*currency))
68            .collect();
69        undeclared.sort_unstable();
70        for currency in undeclared {
71            errors.push(PluginError::warning(format!(
72                "commodity '{currency}' used but not declared"
73            )));
74        }
75
76        PluginOutput {
77            ops: (0..input.directives.len()).map(PluginOp::Keep).collect(),
78            errors,
79        }
80    }
81}
82
83impl RegularPlugin for CheckCommodityPlugin {}