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