Skip to main content

haproxy_spoa_hub_plugin_api/
plugin_trait.rs

1// Suppressed warnings originate from abi_stable's #[sabi_trait] and
2// #[derive(StableAbi)] macro expansions, not from hand-written code.
3#![allow(
4    clippy::used_underscore_binding,
5    clippy::needless_lifetimes,
6    clippy::expl_impl_clone_on_copy,
7    clippy::must_use_candidate,
8    clippy::cast_ptr_alignment
9)]
10
11use abi_stable::{
12    StableAbi, declare_root_module_statics,
13    library::RootModule,
14    package_version_strings, sabi_trait,
15    sabi_types::VersionStrings,
16    std_types::{RBox, RBoxError, ROption, RResult, RStr, RString},
17};
18
19use crate::types::{PluginContext, ProcessingResult, SpoeMessage};
20
21/// FFI-safe plugin trait object type.
22pub type PluginBox = SpoePlugin_TO<'static, RBox<()>>;
23
24/// The trait that all SPOA hub plugins must implement.
25///
26/// Plugins are loaded as shared libraries at runtime. The hub calls
27/// `init` once after loading, then `process` for each SPOE message
28/// that matches the plugin's configured message names.
29///
30/// # Thread Safety
31///
32/// `Send + Sync` is required because plugin instances are shared
33/// across connections via `Arc`. The `process` method takes `&self`,
34/// so plugins must use interior mutability for any mutable state.
35///
36/// # Panic Safety
37///
38/// Use the `define_plugin!` macro to implement plugins — it wraps
39/// `process` in `catch_unwind` to prevent panics from aborting the
40/// hub process.
41#[sabi_trait]
42pub trait SpoePlugin: Send + Sync + core::fmt::Debug {
43    /// Initialize the plugin. Called once after loading.
44    ///
45    /// The `context` provides the plugin name and any configuration
46    /// parameters from the `[plugins.params]` TOML table. Use this
47    /// to set up resources (database connections, lookup tables, etc.).
48    /// Return `RErr` to abort plugin registration — the hub will log
49    /// the error and skip this plugin.
50    fn init(&mut self, context: &PluginContext) -> RResult<(), RBoxError>;
51
52    /// Process an SPOE message and return transaction variables.
53    ///
54    /// Called for each SPOE message that matches this plugin's
55    /// configured message names. The `message` contains pre-parsed
56    /// typed arguments and connection metadata (stream/frame IDs).
57    ///
58    /// When this plugin depends on another, the upstream plugin's
59    /// output variables are merged into `message.args` with their
60    /// namespace-prefixed names (e.g., `"ja3.hash"`).
61    ///
62    /// Variable names in the result should be unprefixed — the hub
63    /// adds the plugin namespace automatically.
64    fn process(&self, message: &SpoeMessage) -> RResult<ProcessingResult, RBoxError>;
65
66    /// Human-readable plugin name used for logging and variable
67    /// namespace prefixing.
68    fn name(&self) -> RStr<'_>;
69
70    /// Semantic version string (e.g., `"0.1.0"`). Logged at plugin
71    /// load time.
72    fn version(&self) -> RStr<'_>;
73
74    /// Called during hub shutdown. Use this to clean up resources
75    /// (close file handles, flush buffers, disconnect from databases).
76    ///
77    /// This is the last field in the current ABI version. Methods
78    /// added in future minor versions will appear below this line
79    /// with default implementations.
80    #[sabi(last_prefix_field)]
81    fn shutdown(&self);
82
83    /// Return a JSON Schema string to validate plugin configuration.
84    ///
85    /// The hub calls this after `new()` and before `init()`. If
86    /// `RSome(schema)` is returned, the hub parses the string as
87    /// JSON Schema and validates the plugin's `[plugins.params]`
88    /// against it. Validation failure prevents `init()` from being
89    /// called.
90    ///
91    /// Return `RNone` (the default) to skip validation.
92    fn config_schema(&self) -> ROption<RString> {
93        ROption::RNone
94    }
95}
96
97/// Root module exported by every plugin shared library.
98///
99/// Contains a factory function to create new plugin instances.
100#[repr(C)]
101#[derive(StableAbi)]
102#[sabi(kind(Prefix(prefix_ref = PluginMod_Ref)))]
103#[sabi(missing_field(panic))]
104pub struct PluginMod {
105    /// Factory function: create a new plugin instance.
106    #[sabi(last_prefix_field)]
107    pub new: extern "C" fn() -> RResult<PluginBox, RBoxError>,
108}
109
110impl RootModule for PluginMod_Ref {
111    declare_root_module_statics! {PluginMod_Ref}
112    const BASE_NAME: &'static str = "spoa_plugin";
113    const NAME: &'static str = "spoa_plugin";
114    const VERSION_STRINGS: VersionStrings = package_version_strings!();
115}