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
25/// Registry of built-in native plugins.
26pub struct NativePluginRegistry {
27    plugins: Vec<Box<dyn NativePlugin>>,
28}
29
30/// Extract the short plugin name from a potentially qualified module path.
31///
32/// Examples:
33/// - `"zerosum"` → `"zerosum"`
34/// - `"beancount.plugins.implicit_prices"` → `"implicit_prices"`
35/// - `"beancount_reds_plugins.zerosum.zerosum"` → `"zerosum"`
36#[inline]
37fn plugin_short_name(name: &str) -> &str {
38    name.rsplit('.').next().unwrap_or(name)
39}
40
41impl NativePluginRegistry {
42    /// Create a new registry with all built-in plugins.
43    pub fn new() -> Self {
44        Self {
45            plugins: vec![
46                Box::new(ImplicitPricesPlugin),
47                Box::new(CheckCommodityPlugin),
48                Box::new(AutoTagPlugin::new()),
49                Box::new(AutoAccountsPlugin),
50                Box::new(LeafOnlyPlugin),
51                Box::new(NoDuplicatesPlugin),
52                Box::new(OneCommodityPlugin),
53                Box::new(UniquePricesPlugin),
54                Box::new(CheckClosingPlugin),
55                Box::new(CloseTreePlugin),
56                Box::new(CoherentCostPlugin),
57                Box::new(ForecastPlugin),
58                Box::new(SellGainsPlugin),
59                Box::new(PedanticPlugin),
60                Box::new(RxTxnPlugin),
61                Box::new(SplitExpensesPlugin),
62                Box::new(UnrealizedPlugin::new()),
63                Box::new(NoUnusedPlugin),
64                Box::new(CheckDrainedPlugin),
65                Box::new(CommodityAttrPlugin::new()),
66                Box::new(CheckAverageCostPlugin::new()),
67                Box::new(CurrencyAccountsPlugin::new()),
68                Box::new(ZerosumPlugin),
69                Box::new(EffectiveDatePlugin),
70                Box::new(GenerateBaseCcyPricesPlugin),
71                Box::new(RenameAccountsPlugin),
72                Box::new(ValuationPlugin),
73                Box::new(CapitalGainsLongShortPlugin),
74                Box::new(CapitalGainsGainLossPlugin),
75                Box::new(BoxAccrualPlugin),
76            ],
77        }
78    }
79
80    /// Find a plugin by name.
81    ///
82    /// Accepts both short names (`"implicit_prices"`) and fully qualified
83    /// module paths (`"beancount.plugins.implicit_prices"`).
84    pub fn find(&self, name: &str) -> Option<&dyn NativePlugin> {
85        let short_name = plugin_short_name(name);
86        self.plugins
87            .iter()
88            .find(|p| p.name() == short_name)
89            .map(std::convert::AsRef::as_ref)
90    }
91
92    /// List all available plugins.
93    pub fn list(&self) -> Vec<&dyn NativePlugin> {
94        self.plugins.iter().map(AsRef::as_ref).collect()
95    }
96
97    /// Check if a name refers to a built-in plugin.
98    ///
99    /// Accepts both short names and fully qualified module paths.
100    pub fn is_builtin(name: &str) -> bool {
101        let short_name = plugin_short_name(name);
102        // Check against registered plugin names
103        let registry = Self::new();
104        registry.plugins.iter().any(|p| p.name() == short_name)
105    }
106}
107
108impl Default for NativePluginRegistry {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_plugin_short_name_bare() {
120        assert_eq!(plugin_short_name("zerosum"), "zerosum");
121        assert_eq!(plugin_short_name("implicit_prices"), "implicit_prices");
122    }
123
124    #[test]
125    fn test_plugin_short_name_beancount_plugins() {
126        assert_eq!(
127            plugin_short_name("beancount.plugins.implicit_prices"),
128            "implicit_prices"
129        );
130        assert_eq!(
131            plugin_short_name("beancount.plugins.check_commodity"),
132            "check_commodity"
133        );
134    }
135
136    #[test]
137    fn test_plugin_short_name_beanahead() {
138        assert_eq!(
139            plugin_short_name("beanahead.plugins.rx_txn_plugin"),
140            "rx_txn_plugin"
141        );
142    }
143
144    #[test]
145    fn test_plugin_short_name_reds_plugins() {
146        assert_eq!(
147            plugin_short_name("beancount_reds_plugins.zerosum.zerosum"),
148            "zerosum"
149        );
150        assert_eq!(
151            plugin_short_name("beancount_reds_plugins.capital_gains_classifier.gain_loss"),
152            "gain_loss"
153        );
154        assert_eq!(
155            plugin_short_name("beancount_reds_plugins.effective_date.effective_date"),
156            "effective_date"
157        );
158    }
159
160    #[test]
161    fn test_plugin_short_name_tarioch() {
162        assert_eq!(
163            plugin_short_name("tariochbctools.plugins.generate_base_ccy_prices"),
164            "generate_base_ccy_prices"
165        );
166    }
167
168    #[test]
169    fn test_plugin_short_name_empty() {
170        assert_eq!(plugin_short_name(""), "");
171    }
172
173    #[test]
174    fn test_registry_find_short_name() {
175        let registry = NativePluginRegistry::new();
176        assert!(registry.find("implicit_prices").is_some());
177        assert!(registry.find("zerosum").is_some());
178        assert!(registry.find("nonexistent").is_none());
179    }
180
181    #[test]
182    fn test_registry_find_qualified_name() {
183        let registry = NativePluginRegistry::new();
184        assert!(registry.find("beancount.plugins.implicit_prices").is_some());
185        assert!(registry.find("beanahead.plugins.rx_txn_plugin").is_some());
186        assert!(
187            registry
188                .find("beancount_reds_plugins.zerosum.zerosum")
189                .is_some()
190        );
191        assert!(
192            registry
193                .find("beancount_reds_plugins.capital_gains_classifier.gain_loss")
194                .is_some()
195        );
196    }
197
198    #[test]
199    fn test_is_builtin_short_name() {
200        assert!(NativePluginRegistry::is_builtin("implicit_prices"));
201        assert!(NativePluginRegistry::is_builtin("zerosum"));
202        assert!(!NativePluginRegistry::is_builtin("nonexistent"));
203    }
204
205    #[test]
206    fn test_is_builtin_qualified_name() {
207        assert!(NativePluginRegistry::is_builtin(
208            "beancount.plugins.implicit_prices"
209        ));
210        assert!(NativePluginRegistry::is_builtin(
211            "beanahead.plugins.rx_txn_plugin"
212        ));
213        assert!(NativePluginRegistry::is_builtin(
214            "beancount_reds_plugins.zerosum.zerosum"
215        ));
216        assert!(!NativePluginRegistry::is_builtin("some.random.nonexistent"));
217    }
218}