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, RVec},
17};
18
19use crate::types::{Diagnostic, 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    /// Deep validation of the plugin's `[plugins.params]` subtree.
97    ///
98    /// Called by the hub before `init()` (production mode) and by the
99    /// validator sidecar in `--validate-socket` mode. Returns a list
100    /// of `Diagnostic` findings; an empty list means "the plugin's
101    /// configuration is valid as far as this plugin can tell."
102    ///
103    /// # Purity contract
104    ///
105    /// `validate()` MUST be pure: no tokio tasks, no goroutines, no
106    /// network I/O, no file I/O beyond what the `PluginContext`
107    /// carries, no global state mutation. Plugins MAY use
108    /// process-internal caches scoped to plugin-instance lifetime.
109    /// Side effects in validation would surface in the validator
110    /// sidecar, which has no business contacting external services.
111    ///
112    /// # Default behavior
113    ///
114    /// The default impl returns an empty `RVec`, so existing plugins
115    /// built against the prior plugin-api version continue to work
116    /// unchanged. Plugins that want to surface line/column-precise
117    /// errors at admission time (and in the production hub's startup
118    /// logs) override this method.
119    ///
120    /// # `Diagnostic.path`
121    ///
122    /// Plugins MUST leave the `path` field of returned diagnostics
123    /// empty. The hub fills it post-hoc with the file identity it
124    /// knows about. See [`Diagnostic`] for details.
125    ///
126    /// See `specs/004-validate-mode/` for the full design.
127    fn validate(&self, _: &PluginContext) -> RVec<Diagnostic> {
128        RVec::new()
129    }
130}
131
132/// Root module exported by every plugin shared library.
133///
134/// Contains a factory function to create new plugin instances.
135#[repr(C)]
136#[derive(StableAbi)]
137#[sabi(kind(Prefix(prefix_ref = PluginMod_Ref)))]
138#[sabi(missing_field(panic))]
139pub struct PluginMod {
140    /// Factory function: create a new plugin instance.
141    #[sabi(last_prefix_field)]
142    pub new: extern "C" fn() -> RResult<PluginBox, RBoxError>,
143}
144
145impl RootModule for PluginMod_Ref {
146    declare_root_module_statics! {PluginMod_Ref}
147    const BASE_NAME: &'static str = "spoa_plugin";
148    const NAME: &'static str = "spoa_plugin";
149    const VERSION_STRINGS: VersionStrings = package_version_strings!();
150}