Skip to main content

haproxy_spoa_hub_plugin_api/
lib.rs

1// abi_stable's proc macros generate code that triggers these warnings
2#![allow(non_camel_case_types, non_local_definitions)]
3
4pub mod plugin_trait;
5pub mod types;
6
7pub use abi_stable;
8pub use abi_stable::std_types::{RHashMap, RString, RVec, Tuple2};
9pub use plugin_trait::{PluginBox, PluginMod, PluginMod_Ref, SpoePlugin, SpoePlugin_TO};
10pub use types::{
11    ConfigValue, Diagnostic, DiagnosticSeverity, PluginContext, ProcessingResult, SpoeMessage,
12    SpoeValue, TxnVariable, VarScope,
13};
14
15/// Define a plugin with automatic panic safety and module export.
16///
17/// This macro generates the `#[export_root_module]` entry point and
18/// wraps the `process` implementation in `std::panic::catch_unwind`
19/// to prevent plugin panics from aborting the hub process.
20///
21/// # Usage
22///
23/// ```rust,ignore
24/// use haproxy_spoa_hub_plugin_api::*;
25///
26/// #[derive(Debug)]
27/// struct MyPlugin;
28///
29/// define_plugin!(MyPlugin, {
30///     fn new() -> Self {
31///         MyPlugin
32///     }
33///
34///     fn init(&mut self, _context: &PluginContext)
35///         -> Result<(), Box<dyn std::error::Error + Send + Sync>>
36///     {
37///         Ok(())
38///     }
39///
40///     fn name(&self) -> &str {
41///         "my-plugin"
42///     }
43///
44///     fn version(&self) -> &str {
45///         "0.1.0"
46///     }
47///
48///     fn process(
49///         &self,
50///         message: &SpoeMessage,
51///     ) -> Result<ProcessingResult, Box<dyn std::error::Error + Send + Sync>> {
52///         Ok(ProcessingResult {
53///             variables: vec![].into(),
54///         })
55///     }
56/// });
57/// ```
58/// Internal-only token-burner used by `define_plugin!` to reference a
59/// captured `$()?` metavariable inside the expansion (so macro_rules!
60/// can resolve which conditional branch to emit) without producing
61/// any Rust code itself. Accepts arbitrary token streams. Hidden from
62/// rustdoc.
63#[doc(hidden)]
64#[macro_export]
65macro_rules! __plugin_present {
66    ($($tt:tt)*) => {};
67}
68
69#[macro_export]
70macro_rules! define_plugin {
71    // Single arm — `config_schema` and `validate` are both optional and
72    // independently provided. Plugins that only define the required
73    // methods (new, init, name, version, process) work as today; plugins
74    // that override `config_schema`, `validate`, or both add them in
75    // that order at the end of the block. Backward-compatible.
76    ($plugin_ty:ty, {
77        fn new() -> Self $new_body:block
78
79        fn init(&mut self, $ctx_param:ident : &PluginContext $(,)?) -> Result<(), Box<dyn std::error::Error + Send + Sync>> $init_body:block
80
81        fn name(&self) -> &str $name_body:block
82
83        fn version(&self) -> &str $version_body:block
84
85        fn process(
86            &self,
87            $msg_param:ident : &SpoeMessage $(,)?
88        ) -> Result<ProcessingResult, Box<dyn std::error::Error + Send + Sync>> $process_body:block
89
90        $(fn config_schema(&self) -> Option<&str> $schema_body:block)?
91
92        $(fn validate(&self, $vctx_param:ident : &PluginContext $(,)?) -> Vec<Diagnostic> $validate_body:block)?
93    }) => {
94        mod __plugin_impl {
95            use super::*;
96            use $crate::abi_stable::std_types::{
97                RBoxError, ROption, RResult, RStr, RString, RVec,
98            };
99            use $crate::abi_stable::sabi_trait::prelude::TD_Opaque;
100
101            impl $plugin_ty {
102                pub fn __new() -> Self $new_body
103                #[allow(clippy::unnecessary_wraps)]
104                pub fn __init(
105                    &mut self,
106                    $ctx_param: &$crate::PluginContext,
107                ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> $init_body
108                pub fn __name(&self) -> &str $name_body
109                pub fn __version(&self) -> &str $version_body
110                #[allow(clippy::unnecessary_wraps)]
111                pub fn __process(
112                    &self,
113                    $msg_param: &$crate::SpoeMessage,
114                ) -> Result<$crate::ProcessingResult, Box<dyn std::error::Error + Send + Sync>> $process_body
115
116                $(
117                    #[allow(clippy::unnecessary_wraps)]
118                    pub fn __config_schema(&self) -> Option<&str> $schema_body
119                )?
120
121                $(
122                    #[allow(clippy::unnecessary_wraps)]
123                    pub fn __validate(
124                        &self,
125                        $vctx_param: &$crate::PluginContext,
126                    ) -> Vec<$crate::Diagnostic> $validate_body
127                )?
128            }
129
130            impl $crate::SpoePlugin for $plugin_ty {
131                fn init(
132                    &mut self,
133                    context: &$crate::PluginContext,
134                ) -> RResult<(), RBoxError> {
135                    self.__init(context).map_err(RBoxError::from_box).into()
136                }
137
138                fn process(
139                    &self,
140                    message: &$crate::SpoeMessage,
141                ) -> RResult<$crate::ProcessingResult, RBoxError> {
142                    match std::panic::catch_unwind(
143                        std::panic::AssertUnwindSafe(|| self.__process(message)),
144                    ) {
145                        Ok(res) => res.map_err(RBoxError::from_box).into(),
146                        Err(_) => {
147                            RResult::RErr(RBoxError::new(
148                                $crate::PluginPanicError,
149                            ))
150                        }
151                    }
152                }
153
154                fn name(&self) -> RStr<'_> {
155                    self.__name().into()
156                }
157
158                fn version(&self) -> RStr<'_> {
159                    self.__version().into()
160                }
161
162                fn shutdown(&self) {}
163
164                $(
165                    fn config_schema(&self) -> ROption<RString> {
166                        $crate::__plugin_present!($schema_body);
167                        self.__config_schema().map(RString::from).into()
168                    }
169                )?
170
171                $(
172                    fn validate(
173                        &self,
174                        context: &$crate::PluginContext,
175                    ) -> RVec<$crate::Diagnostic> {
176                        $crate::__plugin_present!($validate_body);
177                        self.__validate(context).into()
178                    }
179                )?
180            }
181
182            #[$crate::abi_stable::export_root_module]
183            pub fn get_root_module() -> $crate::PluginMod_Ref {
184                use $crate::abi_stable::prefix_type::PrefixTypeTrait;
185                $crate::PluginMod {
186                    new: __create_plugin,
187                }
188                .leak_into_prefix()
189            }
190
191            extern "C" fn __create_plugin() -> RResult<$crate::PluginBox, RBoxError> {
192                let plugin = <$plugin_ty>::__new();
193                RResult::ROk($crate::SpoePlugin_TO::from_value(plugin, TD_Opaque))
194            }
195        }
196    };
197}
198
199/// Error type for plugin panics caught by the `define_plugin!` macro.
200#[derive(Debug)]
201pub struct PluginPanicError;
202
203impl std::fmt::Display for PluginPanicError {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        write!(f, "plugin panicked during message processing")
206    }
207}
208
209impl std::error::Error for PluginPanicError {}