use anyhow::{anyhow, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PredicateOp {
Eq,
Ne,
Exists,
NotExists,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Predicate {
pub(crate) field: String,
pub(crate) op: PredicateOp,
pub(crate) value: String,
}
pub(crate) fn parse_predicates(mut rest: &str) -> Result<Vec<Predicate>> {
let mut predicates = Vec::new();
while !rest.is_empty() {
if !rest.starts_with('[') {
return Err(anyhow!("unexpected text after predicate: '{rest}'"));
}
let Some(end) = rest.find(']') else {
return Err(anyhow!("unterminated '[' in selector"));
};
predicates.push(parse_predicate(&rest[1..end])?);
rest = &rest[end + 1..];
}
Ok(predicates)
}
fn parse_predicate(inner: &str) -> Result<Predicate> {
let Some(eq) = inner.find('=') else {
let (op, field) = match inner.strip_prefix('!') {
Some(rest) => (PredicateOp::NotExists, rest.trim()),
None => (PredicateOp::Exists, inner.trim()),
};
if field.is_empty() {
return Err(anyhow!("predicate '{inner}' has an empty field name"));
}
return Ok(Predicate {
field: field.to_string(),
op,
value: String::new(),
});
};
let (op, field_end) = if eq > 0 && inner.as_bytes()[eq - 1] == b'!' {
(PredicateOp::Ne, eq - 1)
} else {
(PredicateOp::Eq, eq)
};
let field = inner[..field_end].trim();
if field.is_empty() {
return Err(anyhow!("predicate '{inner}' has an empty field name"));
}
Ok(Predicate {
field: field.to_string(),
op,
value: inner[eq + 1..].to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn predicate(field: &str, op: PredicateOp, value: &str) -> Predicate {
Predicate {
field: field.to_string(),
op,
value: value.to_string(),
}
}
#[test]
fn parses_predicate_forms() {
assert_eq!(
parse_predicates("[role=leaf]").unwrap(),
vec![predicate("role", PredicateOp::Eq, "leaf")],
);
assert_eq!(
parse_predicates("[a=x][b=y]").unwrap(),
vec![
predicate("a", PredicateOp::Eq, "x"),
predicate("b", PredicateOp::Eq, "y"),
],
);
assert_eq!(
parse_predicates("[role!=leaf]").unwrap(),
vec![predicate("role", PredicateOp::Ne, "leaf")],
);
assert_eq!(
parse_predicates("[primary_ip]").unwrap(),
vec![predicate("primary_ip", PredicateOp::Exists, "")],
);
assert_eq!(
parse_predicates("[!primary_ip]").unwrap(),
vec![predicate("primary_ip", PredicateOp::NotExists, "")],
);
assert_eq!(
parse_predicates("[prefix=10.0.0.0/24]").unwrap(),
vec![predicate("prefix", PredicateOp::Eq, "10.0.0.0/24")],
);
}
#[test]
fn rejects_malformed_predicates() {
assert!(parse_predicates("[role=leaf")
.unwrap_err()
.to_string()
.contains("unterminated"));
assert!(parse_predicates("[=leaf]")
.unwrap_err()
.to_string()
.contains("empty field name"));
assert!(parse_predicates("[]")
.unwrap_err()
.to_string()
.contains("empty field name"));
assert!(parse_predicates("[!]")
.unwrap_err()
.to_string()
.contains("empty field name"));
}
}