use abi_stable::{
StableAbi,
std_types::{RHashMap, RString, RVec, Tuple2},
};
#[repr(u8)]
#[derive(Debug, Clone, StableAbi)]
pub enum ConfigValue {
String(RString),
Integer(i64),
Float(f64),
Bool(bool),
Array(RVec<ConfigValue>),
Table(RHashMap<RString, ConfigValue>),
#[doc(hidden)]
__Other(u8, RVec<u8>),
}
impl ConfigValue {
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) => Some(s.as_str()),
_ => None,
}
}
#[must_use]
pub fn as_integer(&self) -> Option<i64> {
match self {
Self::Integer(i) => Some(*i),
_ => None,
}
}
#[must_use]
pub fn as_float(&self) -> Option<f64> {
match self {
Self::Float(f) => Some(*f),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(b) => Some(*b),
_ => None,
}
}
#[must_use]
pub fn as_array(&self) -> Option<&RVec<ConfigValue>> {
match self {
Self::Array(a) => Some(a),
_ => None,
}
}
#[must_use]
pub fn as_table(&self) -> Option<&RHashMap<RString, ConfigValue>> {
match self {
Self::Table(t) => Some(t),
_ => None,
}
}
}
impl PartialEq for ConfigValue {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::String(a), Self::String(b)) => a == b,
(Self::Integer(a), Self::Integer(b)) => a == b,
(Self::Float(a), Self::Float(b)) => a == b,
(Self::Bool(a), Self::Bool(b)) => a == b,
(Self::Array(a), Self::Array(b)) => a == b,
(Self::Table(a), Self::Table(b)) => {
a.len() == b.len()
&& a.iter()
.all(|Tuple2(k, v)| b.get(k).is_some_and(|bv| v == bv))
}
(Self::__Other(t1, d1), Self::__Other(t2, d2)) => t1 == t2 && d1 == d2,
_ => false,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, StableAbi)]
pub struct PluginContext {
pub name: RString,
pub config: RHashMap<RString, ConfigValue>,
}
#[repr(u8)]
#[derive(Debug, Clone, StableAbi)]
pub enum SpoeValue {
Null,
Bool(bool),
Int32(i32),
Uint32(u32),
Int64(i64),
Uint64(u64),
Ipv4([u8; 4]),
Ipv6([u8; 16]),
String(RString),
Binary(RVec<u8>),
#[doc(hidden)]
__Other(u8, RVec<u8>),
}
#[repr(C)]
#[derive(Debug, Clone, StableAbi)]
pub struct SpoeMessage {
pub name: RString,
pub args: RHashMap<RString, SpoeValue>,
pub stream_id: u64,
pub frame_id: u64,
}
#[repr(C)]
#[derive(Debug, Clone, StableAbi)]
pub struct ProcessingResult {
pub variables: RVec<TxnVariable>,
}
#[repr(C)]
#[derive(Debug, Clone, StableAbi)]
pub struct TxnVariable {
pub scope: VarScope,
pub name: RString,
pub value: SpoeValue,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, StableAbi)]
pub enum VarScope {
Process = 0,
Session = 1,
Transaction = 2,
Request = 3,
Response = 4,
#[doc(hidden)]
__Other(u8),
}
impl PluginContext {
#[must_use]
pub fn get_config(&self, key: &str) -> Option<&ConfigValue> {
self.config.get(key)
}
}
impl SpoeMessage {
#[must_use]
pub fn get(&self, name: &str) -> Option<&SpoeValue> {
self.args.get(name)
}
#[must_use]
pub fn get_string(&self, name: &str) -> Option<&str> {
match self.get(name)? {
SpoeValue::String(s) => Some(s.as_str()),
_ => None,
}
}
#[must_use]
pub fn get_int(&self, name: &str) -> Option<i64> {
match self.get(name)? {
SpoeValue::Int32(v) => Some(i64::from(*v)),
SpoeValue::Uint32(v) => Some(i64::from(*v)),
SpoeValue::Int64(v) => Some(*v),
SpoeValue::Uint64(v) => i64::try_from(*v).ok(),
_ => None,
}
}
#[must_use]
pub fn get_bool(&self, name: &str) -> Option<bool> {
match self.get(name)? {
SpoeValue::Bool(b) => Some(*b),
_ => None,
}
}
#[must_use]
pub fn get_ipv4(&self, name: &str) -> Option<[u8; 4]> {
match self.get(name)? {
SpoeValue::Ipv4(ip) => Some(*ip),
_ => None,
}
}
#[must_use]
pub fn get_ipv6(&self, name: &str) -> Option<[u8; 16]> {
match self.get(name)? {
SpoeValue::Ipv6(ip) => Some(*ip),
_ => None,
}
}
#[must_use]
pub fn get_binary(&self, name: &str) -> Option<&[u8]> {
match self.get(name)? {
SpoeValue::Binary(b) => Some(b.as_slice()),
_ => None,
}
}
}
impl TxnVariable {
#[must_use]
pub fn new(scope: VarScope, name: impl Into<RString>, value: SpoeValue) -> Self {
Self {
scope,
name: name.into(),
value,
}
}
#[must_use]
pub fn transaction(name: impl Into<RString>, value: SpoeValue) -> Self {
Self::new(VarScope::Transaction, name, value)
}
#[must_use]
pub fn session(name: impl Into<RString>, value: SpoeValue) -> Self {
Self::new(VarScope::Session, name, value)
}
}
impl ProcessingResult {
#[must_use]
pub fn empty() -> Self {
Self {
variables: RVec::new(),
}
}
#[must_use]
pub fn from_vars(vars: Vec<TxnVariable>) -> Self {
Self {
variables: vars.into(),
}
}
#[must_use]
pub fn single(var: TxnVariable) -> Self {
Self {
variables: vec![var].into(),
}
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, StableAbi)]
pub enum DiagnosticSeverity {
Warning = 0,
Error = 1,
}
#[repr(C)]
#[derive(Debug, Clone, StableAbi)]
pub struct Diagnostic {
pub severity: DiagnosticSeverity,
pub path: RString,
pub line: u32,
pub column: u32,
pub message: RString,
}
impl Diagnostic {
#[must_use]
pub fn error(line: u32, column: u32, message: impl Into<RString>) -> Self {
Self {
severity: DiagnosticSeverity::Error,
path: RString::new(),
line,
column,
message: message.into(),
}
}
#[must_use]
pub fn warning(line: u32, column: u32, message: impl Into<RString>) -> Self {
Self {
severity: DiagnosticSeverity::Warning,
path: RString::new(),
line,
column,
message: message.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_value_as_str() {
let v = ConfigValue::String("hello".into());
assert_eq!(v.as_str(), Some("hello"));
assert_eq!(ConfigValue::Integer(1).as_str(), None);
}
#[test]
fn config_value_as_integer() {
let v = ConfigValue::Integer(42);
assert_eq!(v.as_integer(), Some(42));
assert_eq!(ConfigValue::String("x".into()).as_integer(), None);
}
#[test]
fn config_value_as_float() {
let v = ConfigValue::Float(2.72);
assert_eq!(v.as_float(), Some(2.72));
assert_eq!(ConfigValue::Integer(1).as_float(), None);
}
#[test]
fn config_value_as_bool() {
let v = ConfigValue::Bool(true);
assert_eq!(v.as_bool(), Some(true));
assert_eq!(ConfigValue::String("true".into()).as_bool(), None);
}
#[test]
fn config_value_as_array() {
let arr: RVec<ConfigValue> = vec![ConfigValue::Integer(1), ConfigValue::Integer(2)].into();
let v = ConfigValue::Array(arr.clone());
assert_eq!(v.as_array(), Some(&arr));
assert_eq!(ConfigValue::Integer(1).as_array(), None);
}
#[test]
fn config_value_as_table() {
let mut map = RHashMap::new();
map.insert(RString::from("key"), ConfigValue::Bool(true));
let v = ConfigValue::Table(map.clone());
assert!(v.as_table().is_some());
assert_eq!(ConfigValue::Integer(1).as_table(), None);
}
fn make_message(args: Vec<(&str, SpoeValue)>) -> SpoeMessage {
let mut map = RHashMap::new();
for (k, v) in args {
map.insert(RString::from(k), v);
}
SpoeMessage {
name: RString::from("test"),
args: map,
stream_id: 1,
frame_id: 1,
}
}
#[test]
fn spoe_message_get() {
let msg = make_message(vec![("key", SpoeValue::String("val".into()))]);
assert!(msg.get("key").is_some());
assert!(msg.get("missing").is_none());
}
#[test]
fn spoe_message_get_string() {
let msg = make_message(vec![
("s", SpoeValue::String("hello".into())),
("n", SpoeValue::Int32(42)),
]);
assert_eq!(msg.get_string("s"), Some("hello"));
assert_eq!(msg.get_string("n"), None);
assert_eq!(msg.get_string("missing"), None);
}
#[test]
fn spoe_message_get_int_coerces() {
let msg = make_message(vec![
("i32", SpoeValue::Int32(-1)),
("u32", SpoeValue::Uint32(100)),
("i64", SpoeValue::Int64(i64::MIN)),
("u64", SpoeValue::Uint64(999)),
("u64_overflow", SpoeValue::Uint64(u64::MAX)),
("s", SpoeValue::String("nope".into())),
]);
assert_eq!(msg.get_int("i32"), Some(-1));
assert_eq!(msg.get_int("u32"), Some(100));
assert_eq!(msg.get_int("i64"), Some(i64::MIN));
assert_eq!(msg.get_int("u64"), Some(999));
assert_eq!(msg.get_int("u64_overflow"), None);
assert_eq!(msg.get_int("s"), None);
}
#[test]
fn spoe_message_get_bool() {
let msg = make_message(vec![("b", SpoeValue::Bool(true))]);
assert_eq!(msg.get_bool("b"), Some(true));
assert_eq!(msg.get_bool("missing"), None);
}
#[test]
fn spoe_message_get_ipv4() {
let msg = make_message(vec![("ip", SpoeValue::Ipv4([10, 0, 0, 1]))]);
assert_eq!(msg.get_ipv4("ip"), Some([10, 0, 0, 1]));
assert_eq!(msg.get_ipv4("missing"), None);
}
#[test]
fn spoe_message_get_ipv6() {
let addr = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
let msg = make_message(vec![("ip", SpoeValue::Ipv6(addr))]);
assert_eq!(msg.get_ipv6("ip"), Some(addr));
}
#[test]
fn spoe_message_get_binary() {
let msg = make_message(vec![("b", SpoeValue::Binary(vec![1, 2, 3].into()))]);
assert_eq!(msg.get_binary("b"), Some([1, 2, 3].as_slice()));
assert_eq!(msg.get_binary("missing"), None);
}
#[test]
fn txn_variable_constructors() {
let t = TxnVariable::transaction("x", SpoeValue::Bool(true));
assert_eq!(t.scope, VarScope::Transaction);
assert_eq!(t.name.as_str(), "x");
let s = TxnVariable::session("y", SpoeValue::Int32(1));
assert_eq!(s.scope, VarScope::Session);
assert_eq!(s.name.as_str(), "y");
let n = TxnVariable::new(VarScope::Process, "z", SpoeValue::Null);
assert_eq!(n.scope, VarScope::Process);
}
#[test]
fn processing_result_constructors() {
let empty = ProcessingResult::empty();
assert!(empty.variables.is_empty());
let single =
ProcessingResult::single(TxnVariable::transaction("a", SpoeValue::Bool(false)));
assert_eq!(single.variables.len(), 1);
let multi = ProcessingResult::from_vars(vec![
TxnVariable::transaction("a", SpoeValue::Null),
TxnVariable::transaction("b", SpoeValue::Null),
]);
assert_eq!(multi.variables.len(), 2);
}
#[test]
fn plugin_context_get_config() {
let mut config = RHashMap::new();
config.insert(RString::from("timeout"), ConfigValue::Integer(5000));
let ctx = PluginContext {
name: RString::from("test"),
config,
};
assert_eq!(
ctx.get_config("timeout").and_then(ConfigValue::as_integer),
Some(5000)
);
assert!(ctx.get_config("missing").is_none());
}
#[test]
fn config_value_partial_eq() {
assert_eq!(
ConfigValue::String("a".into()),
ConfigValue::String("a".into())
);
assert_ne!(
ConfigValue::String("a".into()),
ConfigValue::String("b".into())
);
assert_ne!(ConfigValue::String("1".into()), ConfigValue::Integer(1));
assert_eq!(ConfigValue::Integer(42), ConfigValue::Integer(42));
assert_eq!(ConfigValue::Float(1.0), ConfigValue::Float(1.0));
assert_eq!(ConfigValue::Bool(true), ConfigValue::Bool(true));
let arr1: RVec<ConfigValue> = vec![ConfigValue::Integer(1)].into();
let arr2: RVec<ConfigValue> = vec![ConfigValue::Integer(1)].into();
assert_eq!(ConfigValue::Array(arr1), ConfigValue::Array(arr2));
let mut t1 = RHashMap::new();
t1.insert(RString::from("k"), ConfigValue::Integer(1));
let mut t2 = RHashMap::new();
t2.insert(RString::from("k"), ConfigValue::Integer(1));
assert_eq!(ConfigValue::Table(t1), ConfigValue::Table(t2));
}
#[test]
fn diagnostic_error_constructor() {
let d = Diagnostic::error(42, 7, "unknown directive");
assert_eq!(d.severity, DiagnosticSeverity::Error);
assert!(d.path.is_empty(), "plugins must leave path empty for hub");
assert_eq!(d.line, 42);
assert_eq!(d.column, 7);
assert_eq!(d.message.as_str(), "unknown directive");
}
#[test]
fn diagnostic_warning_constructor() {
let d = Diagnostic::warning(0, 0, RString::from("file-level warn"));
assert_eq!(d.severity, DiagnosticSeverity::Warning);
assert!(d.path.is_empty());
assert_eq!(d.line, 0);
assert_eq!(d.column, 0);
assert_eq!(d.message.as_str(), "file-level warn");
}
#[test]
fn diagnostic_severity_distinct() {
assert_ne!(DiagnosticSeverity::Warning, DiagnosticSeverity::Error);
assert_eq!(DiagnosticSeverity::Warning as u8, 0);
assert_eq!(DiagnosticSeverity::Error as u8, 1);
}
}