use super::matchable::Matchable;
use super::signal::Signal;
pub trait Transformable: Matchable {
fn set_field(&mut self, field: &<Self::Signal as Signal>::FieldSelector, value: &str);
fn delete_field(&mut self, field: &<Self::Signal as Signal>::FieldSelector) -> bool;
fn move_field(
&mut self,
from: &<Self::Signal as Signal>::FieldSelector,
to: &<Self::Signal as Signal>::FieldSelector,
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::signal::LogSignal;
use crate::field::LogFieldSelector;
use crate::proto::tero::policy::v1::LogField;
use std::borrow::Cow;
use std::collections::HashMap;
struct TestLog {
body: Option<String>,
severity: Option<String>,
attributes: HashMap<String, String>,
}
impl TestLog {
fn new() -> Self {
Self {
body: None,
severity: None,
attributes: HashMap::new(),
}
}
fn with_body(mut self, body: &str) -> Self {
self.body = Some(body.to_string());
self
}
fn with_attr(mut self, key: &str, value: &str) -> Self {
self.attributes.insert(key.to_string(), value.to_string());
self
}
}
impl Matchable for TestLog {
type Signal = LogSignal;
fn get_field(&self, field: &LogFieldSelector) -> Option<Cow<'_, str>> {
match field {
LogFieldSelector::Simple(LogField::Body) => self.body.as_deref().map(Cow::Borrowed),
LogFieldSelector::Simple(LogField::SeverityText) => {
self.severity.as_deref().map(Cow::Borrowed)
}
LogFieldSelector::LogAttribute(path) => path
.first()
.and_then(|key| self.attributes.get(key))
.map(|s| Cow::Borrowed(s.as_str())),
_ => None,
}
}
}
impl Transformable for TestLog {
fn set_field(&mut self, field: &LogFieldSelector, value: &str) {
match field {
LogFieldSelector::Simple(LogField::Body) => {
self.body = Some(value.to_string());
}
LogFieldSelector::Simple(LogField::SeverityText) => {
self.severity = Some(value.to_string());
}
LogFieldSelector::LogAttribute(path) => {
if let Some(key) = path.first() {
self.attributes.insert(key.clone(), value.to_string());
}
}
_ => {}
}
}
fn delete_field(&mut self, field: &LogFieldSelector) -> bool {
match field {
LogFieldSelector::Simple(LogField::Body) => self.body.take().is_some(),
LogFieldSelector::Simple(LogField::SeverityText) => self.severity.take().is_some(),
LogFieldSelector::LogAttribute(path) => path
.first()
.and_then(|key| self.attributes.remove(key))
.is_some(),
_ => false,
}
}
fn move_field(&mut self, from: &LogFieldSelector, to: &LogFieldSelector) {
let value = match from {
LogFieldSelector::Simple(LogField::Body) => self.body.take(),
LogFieldSelector::Simple(LogField::SeverityText) => self.severity.take(),
LogFieldSelector::LogAttribute(path) => {
path.first().and_then(|key| self.attributes.remove(key))
}
_ => None,
};
let target_key = match to {
LogFieldSelector::LogAttribute(path) => path.first().cloned(),
_ => None,
};
if let (Some(v), Some(k)) = (value, target_key) {
self.attributes.insert(k, v);
}
}
}
#[test]
fn set_simple_field_overwrites() {
let mut log = TestLog::new().with_body("original");
log.set_field(&LogFieldSelector::Simple(LogField::Body), "replaced");
assert_eq!(log.body, Some("replaced".to_string()));
}
#[test]
fn set_attribute_creates_when_absent() {
let mut log = TestLog::new();
log.set_field(
&LogFieldSelector::LogAttribute(vec!["new".to_string()]),
"value",
);
assert_eq!(log.attributes.get("new"), Some(&"value".to_string()));
}
#[test]
fn delete_returns_true_when_present() {
let mut log = TestLog::new().with_body("x");
assert!(log.delete_field(&LogFieldSelector::Simple(LogField::Body)));
assert!(log.body.is_none());
}
#[test]
fn delete_returns_false_when_absent() {
let mut log = TestLog::new();
assert!(!log.delete_field(&LogFieldSelector::Simple(LogField::Body)));
}
#[test]
fn move_field_relocates_value() {
let mut log = TestLog::new().with_attr("old", "value");
log.move_field(
&LogFieldSelector::LogAttribute(vec!["old".to_string()]),
&LogFieldSelector::LogAttribute(vec!["new".to_string()]),
);
assert!(!log.attributes.contains_key("old"));
assert_eq!(log.attributes.get("new"), Some(&"value".to_string()));
}
}