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}