use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterOp {
Equals,
NotEquals,
Contains,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilterExpr {
path: Vec<String>,
op: FilterOp,
value: String,
}
const SEPARATORS: &[(&str, FilterOp)] = &[
("!=", FilterOp::NotEquals),
("==", FilterOp::Equals),
("~", FilterOp::Contains),
("=", FilterOp::Equals),
];
impl FilterExpr {
pub fn parse(raw: &str) -> Result<Self, String> {
let mut best: Option<(usize, usize, FilterOp)> = None;
for (token, op) in SEPARATORS {
if let Some(idx) = raw.find(token) {
let better = match best {
None => true,
Some((cur_idx, cur_len, _)) => {
idx < cur_idx || (idx == cur_idx && token.len() > cur_len)
}
};
if better {
best = Some((idx, token.len(), *op));
}
}
}
let Some((idx, len, op)) = best else {
return Err(crate::i18n::validation::agent_surface_filter_invalid(raw));
};
let key = raw[..idx].trim();
if key.is_empty() {
return Err(crate::i18n::validation::agent_surface_filter_empty_key(raw));
}
let value = &raw[idx + len..];
Ok(Self {
path: key.split('.').map(str::to_string).collect(),
op,
value: value.to_string(),
})
}
pub fn key(&self) -> String {
self.path.join(".")
}
pub fn path(&self) -> &[String] {
&self.path
}
pub fn matches(&self, element: &Value, command: Option<&str>) -> bool {
let scalar = lookup(element, &self.path, command).and_then(scalar_text);
match (self.op, scalar) {
(FilterOp::Equals, Some(text)) => text == self.value,
(FilterOp::Equals, None) => false,
(FilterOp::NotEquals, Some(text)) => text != self.value,
(FilterOp::NotEquals, None) => true,
(FilterOp::Contains, Some(text)) => {
text.to_lowercase().contains(&self.value.to_lowercase())
}
(FilterOp::Contains, None) => false,
}
}
}
pub fn lookup<'a>(value: &'a Value, path: &[String], command: Option<&str>) -> Option<&'a Value> {
if let Some(found) = walk(value, path.iter().map(String::as_str)) {
return Some(found);
}
let (last, prefix) = path.split_last()?;
for spelling in synonyms_of(last, command) {
if let Some(found) = walk(
value,
prefix
.iter()
.map(String::as_str)
.chain(std::iter::once(spelling)),
) {
return Some(found);
}
}
None
}
fn walk<'a, 'b>(value: &'a Value, path: impl Iterator<Item = &'b str>) -> Option<&'a Value> {
let mut cursor = value;
for segment in path {
cursor = cursor.as_object()?.get(segment)?;
}
Some(cursor)
}
fn synonyms_of(leaf: &str, command: Option<&str>) -> Vec<&'static str> {
let mut out: Vec<&'static str> = Vec::new();
for group in crate::constants::agent_surface_field_synonym_groups(command) {
if !group.contains(&leaf) {
continue;
}
for spelling in group {
if *spelling != leaf && !out.contains(spelling) {
out.push(spelling);
}
}
}
out
}
pub fn resolve<'a>(value: &'a Value, key: &str) -> Option<&'a Value> {
walk(value, key.split('.'))
}
pub fn scalar_text(value: &Value) -> Option<String> {
match value {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
Value::Bool(b) => Some(b.to_string()),
Value::Null => Some(String::new()),
Value::Array(_) | Value::Object(_) => None,
}
}
pub fn matches_all(filters: &[FilterExpr], element: &Value, command: Option<&str>) -> bool {
filters.iter().all(|f| f.matches(element, command))
}