Skip to main content

rustledger_plugin/native/
mod.rs

1//! Native (non-WASM) plugin support.
2//!
3//! These plugins run as native Rust code for maximum performance.
4//! They implement the same interface as WASM plugins.
5
6mod plugins;
7
8pub use plugins::*;
9
10use crate::types::PluginInput;
11use crate::types::PluginOutput;
12
13/// Trait for native plugins.
14pub trait NativePlugin: Send + Sync {
15    /// Plugin name.
16    fn name(&self) -> &'static str;
17
18    /// Plugin description.
19    fn description(&self) -> &'static str;
20
21    /// Process directives and return modified directives + errors.
22    fn process(&self, input: PluginInput) -> PluginOutput;
23
24    /// Whether this plugin synthesizes directives the loader's
25    /// `Phase::Early` validation depends on (e.g. injecting `Open`
26    /// directives so account-presence checks see them).
27    ///
28    /// Plugins returning `true` run in the loader's pre-booking pass;
29    /// plugins returning `false` (the default — transformations on
30    /// already-parsed directives) run post-booking so they see
31    /// filled-in `cost.number_per` values from the booker.
32    ///
33    /// This is the trait-level analogue of the loader's `PluginPass`
34    /// enum; the loader consults this method to classify each plugin
35    /// at scheduling time, avoiding a hardcoded list of synthesizer
36    /// names.
37    fn is_synth(&self) -> bool {
38        false
39    }
40}
41
42/// Registry of built-in native plugins.
43pub struct NativePluginRegistry {
44    plugins: Vec<Box<dyn NativePlugin>>,
45}
46
47/// Extract the short plugin name from a potentially qualified module path.
48///
49/// Examples:
50/// - `"zerosum"` → `"zerosum"`
51/// - `"beancount.plugins.implicit_prices"` → `"implicit_prices"`
52/// - `"beancount_reds_plugins.zerosum.zerosum"` → `"zerosum"`
53#[inline]
54fn plugin_short_name(name: &str) -> &str {
55    name.rsplit('.').next().unwrap_or(name)
56}
57
58impl NativePluginRegistry {
59    /// Create a new registry with all built-in plugins.
60    pub fn new() -> Self {
61        Self {
62            plugins: vec![
63                Box::new(ImplicitPricesPlugin),
64                Box::new(CheckCommodityPlugin),
65                Box::new(AutoTagPlugin::new()),
66                Box::new(AutoAccountsPlugin),
67                Box::new(LeafOnlyPlugin),
68                Box::new(NoDuplicatesPlugin),
69                Box::new(OneCommodityPlugin),
70                Box::new(UniquePricesPlugin),
71                Box::new(CheckClosingPlugin),
72                Box::new(CloseTreePlugin),
73                Box::new(CoherentCostPlugin),
74                Box::new(ForecastPlugin),
75                Box::new(SellGainsPlugin),
76                Box::new(PedanticPlugin),
77                Box::new(RxTxnPlugin),
78                Box::new(SplitExpensesPlugin),
79                Box::new(UnrealizedPlugin::new()),
80                Box::new(NoUnusedPlugin),
81                Box::new(CheckDrainedPlugin),
82                Box::new(CommodityAttrPlugin::new()),
83                Box::new(CheckAverageCostPlugin::new()),
84                Box::new(CurrencyAccountsPlugin::new()),
85                Box::new(ZerosumPlugin),
86                Box::new(EffectiveDatePlugin),
87                Box::new(GenerateBaseCcyPricesPlugin),
88                Box::new(RenameAccountsPlugin),
89                Box::new(ValuationPlugin),
90                Box::new(CapitalGainsLongShortPlugin),
91                Box::new(CapitalGainsGainLossPlugin),
92                Box::new(BoxAccrualPlugin),
93            ],
94        }
95    }
96
97    /// Find a plugin by name.
98    ///
99    /// Accepts both short names (`"implicit_prices"`) and fully qualified
100    /// module paths (`"beancount.plugins.implicit_prices"`).
101    pub fn find(&self, name: &str) -> Option<&dyn NativePlugin> {
102        let short_name = plugin_short_name(name);
103        self.plugins
104            .iter()
105            .find(|p| p.name() == short_name)
106            .map(std::convert::AsRef::as_ref)
107    }
108
109    /// List all available plugins.
110    pub fn list(&self) -> Vec<&dyn NativePlugin> {
111        self.plugins.iter().map(AsRef::as_ref).collect()
112    }
113
114    /// Check if a name refers to a built-in plugin.
115    ///
116    /// Accepts both short names and fully qualified module paths.
117    pub fn is_builtin(name: &str) -> bool {
118        let short_name = plugin_short_name(name);
119        // Check against registered plugin names
120        let registry = Self::new();
121        registry.plugins.iter().any(|p| p.name() == short_name)
122    }
123}
124
125impl Default for NativePluginRegistry {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_plugin_short_name_bare() {
137        assert_eq!(plugin_short_name("zerosum"), "zerosum");
138        assert_eq!(plugin_short_name("implicit_prices"), "implicit_prices");
139    }
140
141    #[test]
142    fn test_plugin_short_name_beancount_plugins() {
143        assert_eq!(
144            plugin_short_name("beancount.plugins.implicit_prices"),
145            "implicit_prices"
146        );
147        assert_eq!(
148            plugin_short_name("beancount.plugins.check_commodity"),
149            "check_commodity"
150        );
151    }
152
153    #[test]
154    fn test_plugin_short_name_beanahead() {
155        assert_eq!(
156            plugin_short_name("beanahead.plugins.rx_txn_plugin"),
157            "rx_txn_plugin"
158        );
159    }
160
161    #[test]
162    fn test_plugin_short_name_reds_plugins() {
163        assert_eq!(
164            plugin_short_name("beancount_reds_plugins.zerosum.zerosum"),
165            "zerosum"
166        );
167        assert_eq!(
168            plugin_short_name("beancount_reds_plugins.capital_gains_classifier.gain_loss"),
169            "gain_loss"
170        );
171        assert_eq!(
172            plugin_short_name("beancount_reds_plugins.effective_date.effective_date"),
173            "effective_date"
174        );
175    }
176
177    #[test]
178    fn test_plugin_short_name_tarioch() {
179        assert_eq!(
180            plugin_short_name("tariochbctools.plugins.generate_base_ccy_prices"),
181            "generate_base_ccy_prices"
182        );
183    }
184
185    #[test]
186    fn test_plugin_short_name_empty() {
187        assert_eq!(plugin_short_name(""), "");
188    }
189
190    #[test]
191    fn test_registry_find_short_name() {
192        let registry = NativePluginRegistry::new();
193        assert!(registry.find("implicit_prices").is_some());
194        assert!(registry.find("zerosum").is_some());
195        assert!(registry.find("nonexistent").is_none());
196    }
197
198    #[test]
199    fn test_registry_find_qualified_name() {
200        let registry = NativePluginRegistry::new();
201        assert!(registry.find("beancount.plugins.implicit_prices").is_some());
202        assert!(registry.find("beanahead.plugins.rx_txn_plugin").is_some());
203        assert!(
204            registry
205                .find("beancount_reds_plugins.zerosum.zerosum")
206                .is_some()
207        );
208        assert!(
209            registry
210                .find("beancount_reds_plugins.capital_gains_classifier.gain_loss")
211                .is_some()
212        );
213    }
214
215    #[test]
216    fn test_is_builtin_short_name() {
217        assert!(NativePluginRegistry::is_builtin("implicit_prices"));
218        assert!(NativePluginRegistry::is_builtin("zerosum"));
219        assert!(!NativePluginRegistry::is_builtin("nonexistent"));
220    }
221
222    #[test]
223    fn test_is_builtin_qualified_name() {
224        assert!(NativePluginRegistry::is_builtin(
225            "beancount.plugins.implicit_prices"
226        ));
227        assert!(NativePluginRegistry::is_builtin(
228            "beanahead.plugins.rx_txn_plugin"
229        ));
230        assert!(NativePluginRegistry::is_builtin(
231            "beancount_reds_plugins.zerosum.zerosum"
232        ));
233        assert!(!NativePluginRegistry::is_builtin("some.random.nonexistent"));
234    }
235}