use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TypeInfo {
Bool,
UInt,
Number,
String,
Json,
Yaml,
BoolOrNull,
StringOrNull,
JsonOrNull,
YamlOrNull,
Uuid,
Email,
Url,
Ip,
Any,
}
impl TypeInfo {
pub fn is_truthy(&self) -> bool {
matches!(self, TypeInfo::Bool | TypeInfo::BoolOrNull)
}
pub fn is_numeric(&self) -> bool {
matches!(self, TypeInfo::UInt | TypeInfo::Number)
}
pub fn is_stringy(&self) -> bool {
matches!(
self,
TypeInfo::String
| TypeInfo::StringOrNull
| TypeInfo::Uuid
| TypeInfo::Email
| TypeInfo::Url
| TypeInfo::Ip
)
}
pub fn is_nullable(&self) -> bool {
matches!(
self,
TypeInfo::BoolOrNull
| TypeInfo::StringOrNull
| TypeInfo::JsonOrNull
| TypeInfo::YamlOrNull
| TypeInfo::Any
)
}
pub fn base_type(&self) -> TypeInfo {
match self {
TypeInfo::BoolOrNull => TypeInfo::Bool,
TypeInfo::StringOrNull => TypeInfo::String,
TypeInfo::JsonOrNull => TypeInfo::Json,
TypeInfo::YamlOrNull => TypeInfo::Yaml,
TypeInfo::Uuid | TypeInfo::Email | TypeInfo::Url | TypeInfo::Ip => TypeInfo::String,
other => *other,
}
}
pub fn display_name(&self) -> &'static str {
match self {
TypeInfo::Bool => "bool",
TypeInfo::UInt => "non-negative integer",
TypeInfo::Number => "number",
TypeInfo::String => "string",
TypeInfo::Json => "json",
TypeInfo::Yaml => "yaml",
TypeInfo::Any => "any",
TypeInfo::BoolOrNull => "bool | null",
TypeInfo::StringOrNull => "string | null",
TypeInfo::JsonOrNull => "json | null",
TypeInfo::YamlOrNull => "yaml | null",
TypeInfo::Uuid => "uuid (string)",
TypeInfo::Email => "email (string)",
TypeInfo::Url => "url (string)",
TypeInfo::Ip => "ip address (string)",
}
}
pub fn supports_operator(&self, op: &str) -> (bool, Option<&'static str>) {
match op {
"==" | "!=" => (true, None), ">" | "<" | ">=" | "<=" => {
if self.is_numeric() {
(true, None)
} else if matches!(self, TypeInfo::Bool | TypeInfo::BoolOrNull) {
(
false,
Some("boolean values cannot be compared with <, >, <=, >="),
)
} else if self.is_stringy() {
(
false,
Some(
"use 'contains', 'startsWith', or 'endsWith' for string comparison, not <, >, <=, >=",
),
)
} else {
(
false,
Some("this type does not support ordering comparisons"),
)
}
}
"contains" | "startsWith" | "endsWith" | "matches" => {
if self.is_stringy() {
(true, None)
} else if self.is_numeric() {
(
false,
Some(
"numeric values do not support string operators — use == or != instead",
),
)
} else if matches!(self, TypeInfo::Bool | TypeInfo::BoolOrNull) {
(
false,
Some("boolean values do not support string operators"),
)
} else {
(false, Some("this type does not support string operators"))
}
}
_ => (false, Some("unknown operator")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgTypeInfo {
pub expected: TypeInfo,
pub required: bool,
pub default: Option<&'static str>,
}
#[derive(Debug, Clone)]
pub struct TypedPluginSignature {
pub return_type: TypeInfo,
pub arg_types: &'static [ArgTypeInfo],
pub purity: crate::plugins::PluginPurity,
pub deterministic: bool,
pub idempotent: bool,
pub safe_for_rewrite: bool,
}
impl TypedPluginSignature {
pub fn valid_arg_count(&self, count: usize) -> bool {
let required = self.arg_types.iter().filter(|a| a.required).count();
let total = self.arg_types.len();
count >= required && count <= total
}
pub fn arg_type(&self, idx: usize) -> Option<TypeInfo> {
self.arg_types.get(idx).map(|a| a.expected)
}
}
pub fn typed_plugin_signatures() -> std::collections::HashMap<String, TypedPluginSignature> {
use crate::plugins::PluginPurity;
let mut map = std::collections::HashMap::new();
macro_rules! plugin_sig {
($name:expr, $return_type:expr, purity: $purity:expr, deterministic: $det:expr, idempotent: $idem:expr, rewrite: $rewrite:expr, args: [$($arg:expr),* $(,)?]) => {
map.insert(
$name.to_string(),
TypedPluginSignature {
return_type: $return_type,
arg_types: &[$($arg),*],
purity: $purity,
deterministic: $det,
idempotent: $idem,
safe_for_rewrite: $rewrite,
},
);
};
}
plugin_sig!("@uuid", TypeInfo::Bool,
purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
plugin_sig!("@email", TypeInfo::Bool,
purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
plugin_sig!("@ip", TypeInfo::Bool,
purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
plugin_sig!("@url", TypeInfo::Bool,
purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
plugin_sig!("@timestamp", TypeInfo::Bool,
purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
args: [ArgTypeInfo { expected: TypeInfo::Any, required: true, default: None }]);
plugin_sig!("@regex", TypeInfo::Bool,
purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: false,
args: [
ArgTypeInfo { expected: TypeInfo::String, required: true, default: None },
ArgTypeInfo { expected: TypeInfo::String, required: true, default: None },
]);
plugin_sig!("@len", TypeInfo::UInt,
purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
args: [ArgTypeInfo { expected: TypeInfo::Any, required: true, default: None }]);
plugin_sig!("@empty", TypeInfo::Bool,
purity: PluginPurity::Pure, deterministic: true, idempotent: true, rewrite: true,
args: [ArgTypeInfo { expected: TypeInfo::Any, required: true, default: None }]);
plugin_sig!("@header", TypeInfo::StringOrNull,
purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
plugin_sig!("@has_header", TypeInfo::Bool,
purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: true,
args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
plugin_sig!("@trailer", TypeInfo::StringOrNull,
purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
plugin_sig!("@has_trailer", TypeInfo::Bool,
purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: true,
args: [ArgTypeInfo { expected: TypeInfo::String, required: true, default: None }]);
plugin_sig!("@env", TypeInfo::StringOrNull,
purity: PluginPurity::Impure, deterministic: false, idempotent: false, rewrite: false,
args: [
ArgTypeInfo { expected: TypeInfo::String, required: true, default: None },
ArgTypeInfo { expected: TypeInfo::String, required: false, default: None },
]);
plugin_sig!("@elapsed_ms", TypeInfo::UInt,
purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
args: []);
plugin_sig!("@total_elapsed_ms", TypeInfo::UInt,
purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
args: []);
plugin_sig!("@scope_message_count", TypeInfo::UInt,
purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
args: []);
plugin_sig!("@scope_index", TypeInfo::UInt,
purity: PluginPurity::ContextDependent, deterministic: false, idempotent: true, rewrite: false,
args: []);
map
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_type_info_is_truthy() {
assert!(TypeInfo::Bool.is_truthy());
assert!(TypeInfo::BoolOrNull.is_truthy());
assert!(!TypeInfo::String.is_truthy());
assert!(!TypeInfo::UInt.is_truthy());
}
#[test]
fn test_type_info_is_numeric() {
assert!(TypeInfo::UInt.is_numeric());
assert!(TypeInfo::Number.is_numeric());
assert!(!TypeInfo::String.is_numeric());
}
#[test]
fn test_type_info_is_stringy() {
assert!(TypeInfo::String.is_stringy());
assert!(TypeInfo::StringOrNull.is_stringy());
assert!(TypeInfo::Uuid.is_stringy());
assert!(TypeInfo::Email.is_stringy());
assert!(TypeInfo::Url.is_stringy());
assert!(TypeInfo::Ip.is_stringy());
assert!(!TypeInfo::Bool.is_stringy());
}
#[test]
fn test_type_info_is_nullable() {
assert!(TypeInfo::BoolOrNull.is_nullable());
assert!(TypeInfo::StringOrNull.is_nullable());
assert!(TypeInfo::JsonOrNull.is_nullable());
assert!(TypeInfo::YamlOrNull.is_nullable());
assert!(TypeInfo::Any.is_nullable());
assert!(!TypeInfo::Bool.is_nullable());
}
#[test]
fn test_type_info_base_type() {
assert_eq!(TypeInfo::BoolOrNull.base_type(), TypeInfo::Bool);
assert_eq!(TypeInfo::StringOrNull.base_type(), TypeInfo::String);
assert_eq!(TypeInfo::Uuid.base_type(), TypeInfo::String);
assert_eq!(TypeInfo::Email.base_type(), TypeInfo::String);
assert_eq!(TypeInfo::Url.base_type(), TypeInfo::String);
assert_eq!(TypeInfo::Ip.base_type(), TypeInfo::String);
assert_eq!(TypeInfo::Bool.base_type(), TypeInfo::Bool);
}
#[test]
fn test_type_info_display_name() {
assert_eq!(TypeInfo::Bool.display_name(), "bool");
assert_eq!(TypeInfo::UInt.display_name(), "non-negative integer");
assert_eq!(TypeInfo::Number.display_name(), "number");
assert_eq!(TypeInfo::String.display_name(), "string");
assert_eq!(TypeInfo::Json.display_name(), "json");
assert_eq!(TypeInfo::Yaml.display_name(), "yaml");
assert_eq!(TypeInfo::Any.display_name(), "any");
assert_eq!(TypeInfo::BoolOrNull.display_name(), "bool | null");
assert_eq!(TypeInfo::Uuid.display_name(), "uuid (string)");
assert_eq!(TypeInfo::Email.display_name(), "email (string)");
assert_eq!(TypeInfo::Url.display_name(), "url (string)");
assert_eq!(TypeInfo::Ip.display_name(), "ip address (string)");
}
#[test]
fn test_type_info_supports_operator() {
let (valid, _) = TypeInfo::Bool.supports_operator("==");
assert!(valid);
let (valid, reason) = TypeInfo::Bool.supports_operator("<");
assert!(!valid);
assert!(reason.is_some());
let (valid, reason) = TypeInfo::String.supports_operator("<");
assert!(!valid);
assert!(reason.is_some());
let (valid, _) = TypeInfo::String.supports_operator("contains");
assert!(valid);
let (valid, _) = TypeInfo::Number.supports_operator("contains");
assert!(!valid);
let (valid, _) = TypeInfo::Uuid.supports_operator("==");
assert!(valid);
let (valid, reason) = TypeInfo::Uuid.supports_operator(">");
assert!(!valid);
assert!(reason.is_some());
let (valid, _reason) = TypeInfo::Uuid.supports_operator("contains");
assert!(valid);
let (valid, _) = TypeInfo::Any.supports_operator("==");
assert!(valid);
}
#[test]
fn test_typed_plugin_signature_valid_arg_count() {
let sig = TypedPluginSignature {
return_type: TypeInfo::Bool,
arg_types: &[
ArgTypeInfo {
expected: TypeInfo::String,
required: true,
default: None,
},
ArgTypeInfo {
expected: TypeInfo::String,
required: false,
default: Some("default"),
},
],
purity: crate::plugins::PluginPurity::Pure,
deterministic: true,
idempotent: true,
safe_for_rewrite: true,
};
assert!(sig.valid_arg_count(1));
assert!(sig.valid_arg_count(2));
assert!(!sig.valid_arg_count(0));
assert!(!sig.valid_arg_count(3));
}
#[test]
fn test_typed_plugin_signature_count() {
let sig = TypedPluginSignature {
return_type: TypeInfo::Bool,
arg_types: &[
ArgTypeInfo {
expected: TypeInfo::String,
required: true,
default: None,
},
ArgTypeInfo {
expected: TypeInfo::String,
required: false,
default: Some("default"),
},
],
purity: crate::plugins::PluginPurity::Pure,
deterministic: true,
idempotent: true,
safe_for_rewrite: true,
};
assert_eq!(sig.arg_type(0), Some(TypeInfo::String));
assert_eq!(sig.arg_type(1), Some(TypeInfo::String));
assert_eq!(sig.arg_type(2), None);
}
#[test]
fn test_typed_plugin_signatures() {
let sigs = typed_plugin_signatures();
assert!(sigs.contains_key("@uuid"));
assert!(sigs.contains_key("@email"));
assert!(sigs.contains_key("@ip"));
assert!(sigs.contains_key("@url"));
assert!(sigs.contains_key("@len"));
assert!(sigs.contains_key("@empty"));
assert!(sigs.contains_key("@header"));
assert!(sigs.contains_key("@trailer"));
assert!(sigs.contains_key("@env"));
assert!(sigs.contains_key("@elapsed_ms"));
}
}