mod plugins;
pub use plugins::*;
use std::sync::LazyLock;
use crate::types::PluginInput;
use crate::types::PluginOutput;
pub trait NativePlugin: Send + Sync {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn process(&self, input: PluginInput) -> PluginOutput;
}
pub trait SynthPlugin: NativePlugin {}
pub trait RegularPlugin: NativePlugin {}
pub struct NativePluginRegistry {
synth: Vec<Box<dyn SynthPlugin>>,
regular: Vec<Box<dyn RegularPlugin>>,
}
#[inline]
fn plugin_short_name(name: &str) -> &str {
name.rsplit('.').next().unwrap_or(name)
}
fn build_global_registry() -> NativePluginRegistry {
let synth: Vec<Box<dyn SynthPlugin>> = vec![
Box::new(AutoAccountsPlugin),
Box::new(DocumentDiscoveryPlugin),
];
let regular: Vec<Box<dyn RegularPlugin>> = vec![
Box::new(ImplicitPricesPlugin),
Box::new(CheckCommodityPlugin),
Box::new(AutoTagPlugin::new()),
Box::new(LeafOnlyPlugin),
Box::new(NoDuplicatesPlugin),
Box::new(OneCommodityPlugin),
Box::new(UniquePricesPlugin),
Box::new(CheckClosingPlugin),
Box::new(CloseTreePlugin),
Box::new(CoherentCostPlugin),
Box::new(ForecastPlugin),
Box::new(SellGainsPlugin),
Box::new(PedanticPlugin),
Box::new(RxTxnPlugin),
Box::new(SplitExpensesPlugin),
Box::new(UnrealizedPlugin::new()),
Box::new(NoUnusedPlugin),
Box::new(CheckDrainedPlugin),
Box::new(CommodityAttrPlugin::new()),
Box::new(CheckAverageCostPlugin::new()),
Box::new(CurrencyAccountsPlugin::new()),
Box::new(ZerosumPlugin),
Box::new(EffectiveDatePlugin),
Box::new(GenerateBaseCcyPricesPlugin),
Box::new(RenameAccountsPlugin),
Box::new(ValuationPlugin),
Box::new(CapitalGainsLongShortPlugin),
Box::new(CapitalGainsGainLossPlugin),
Box::new(BoxAccrualPlugin),
];
NativePluginRegistry { synth, regular }
}
static GLOBAL_REGISTRY: LazyLock<NativePluginRegistry> = LazyLock::new(build_global_registry);
impl NativePluginRegistry {
#[must_use]
pub fn global() -> &'static Self {
&GLOBAL_REGISTRY
}
pub fn find_synth(&self, name: &str) -> Option<&dyn SynthPlugin> {
let short_name = plugin_short_name(name);
self.synth
.iter()
.find(|p| p.name() == short_name)
.map(std::convert::AsRef::as_ref)
}
pub fn find_regular(&self, name: &str) -> Option<&dyn RegularPlugin> {
let short_name = plugin_short_name(name);
self.regular
.iter()
.find(|p| p.name() == short_name)
.map(std::convert::AsRef::as_ref)
}
pub fn iter(&self) -> impl Iterator<Item = &dyn NativePlugin> {
self.synth
.iter()
.map(|p| p.as_ref() as &dyn NativePlugin)
.chain(self.regular.iter().map(|p| p.as_ref() as &dyn NativePlugin))
}
#[must_use]
pub fn has(&self, name: &str) -> bool {
let short_name = plugin_short_name(name);
self.synth.iter().any(|p| p.name() == short_name)
|| self.regular.iter().any(|p| p.name() == short_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_short_name_bare() {
assert_eq!(plugin_short_name("zerosum"), "zerosum");
assert_eq!(plugin_short_name("implicit_prices"), "implicit_prices");
}
#[test]
fn test_plugin_short_name_beancount_plugins() {
assert_eq!(
plugin_short_name("beancount.plugins.implicit_prices"),
"implicit_prices"
);
assert_eq!(
plugin_short_name("beancount.plugins.check_commodity"),
"check_commodity"
);
}
#[test]
fn test_plugin_short_name_beanahead() {
assert_eq!(
plugin_short_name("beanahead.plugins.rx_txn_plugin"),
"rx_txn_plugin"
);
}
#[test]
fn test_plugin_short_name_reds_plugins() {
assert_eq!(
plugin_short_name("beancount_reds_plugins.zerosum.zerosum"),
"zerosum"
);
assert_eq!(
plugin_short_name("beancount_reds_plugins.capital_gains_classifier.gain_loss"),
"gain_loss"
);
assert_eq!(
plugin_short_name("beancount_reds_plugins.effective_date.effective_date"),
"effective_date"
);
}
#[test]
fn test_plugin_short_name_tarioch() {
assert_eq!(
plugin_short_name("tariochbctools.plugins.generate_base_ccy_prices"),
"generate_base_ccy_prices"
);
}
#[test]
fn test_plugin_short_name_empty() {
assert_eq!(plugin_short_name(""), "");
}
#[test]
fn test_registry_find_regular_short_name() {
let registry = NativePluginRegistry::global();
assert!(registry.find_regular("implicit_prices").is_some());
assert!(registry.find_regular("zerosum").is_some());
assert!(registry.find_regular("nonexistent").is_none());
}
#[test]
fn test_registry_find_regular_qualified_name() {
let registry = NativePluginRegistry::global();
assert!(
registry
.find_regular("beancount.plugins.implicit_prices")
.is_some()
);
assert!(
registry
.find_regular("beanahead.plugins.rx_txn_plugin")
.is_some()
);
assert!(
registry
.find_regular("beancount_reds_plugins.zerosum.zerosum")
.is_some()
);
assert!(
registry
.find_regular("beancount_reds_plugins.capital_gains_classifier.gain_loss")
.is_some()
);
}
#[test]
fn test_registry_synth_and_regular_are_disjoint() {
let registry = NativePluginRegistry::global();
for plugin in registry.iter() {
let name = plugin.name();
let in_synth = registry.find_synth(name).is_some();
let in_regular = registry.find_regular(name).is_some();
assert!(
in_synth ^ in_regular,
"plugin {name:?} must live in exactly one pass Vec (synth={in_synth}, regular={in_regular})",
);
assert!(
registry.has(name),
"list() yielded {name:?} but has() disagrees"
);
}
assert!(!registry.has("nonexistent"));
assert!(registry.find_synth("nonexistent").is_none());
assert!(registry.find_regular("nonexistent").is_none());
assert!(registry.has("beancount.plugins.implicit_prices"));
assert!(registry.has("beanahead.plugins.rx_txn_plugin"));
assert!(registry.has("beancount_reds_plugins.zerosum.zerosum"));
assert!(!registry.has("some.random.nonexistent"));
}
}