use std::fmt;
use crate::grammar::is_valid_plain_chunk;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatternChunk {
Literal(String),
Var(String),
Rest(String),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PatternError {
#[error("empty pattern")]
Empty,
#[error("{{var...}} only in trailing position: {0:?}")]
RestNotTrailing(String),
#[error("bad variable name {0:?}")]
BadVarName(String),
#[error("chunk {0:?} violates RFC 03 §2")]
BadChunk(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubjectPattern {
chunks: Vec<PatternChunk>,
text: String,
}
impl SubjectPattern {
pub fn parse(pattern: &str) -> Result<Self, PatternError> {
if pattern.is_empty() {
return Err(PatternError::Empty);
}
let parts: Vec<&str> = pattern.split('/').collect();
let mut chunks = Vec::with_capacity(parts.len());
for (i, part) in parts.iter().enumerate() {
if let Some(var) = part.strip_prefix('{').and_then(|p| p.strip_suffix("...}")) {
if i != parts.len() - 1 {
return Err(PatternError::RestNotTrailing(pattern.to_string()));
}
if !is_valid_plain_chunk(var) {
return Err(PatternError::BadVarName(var.to_string()));
}
chunks.push(PatternChunk::Rest(var.to_string()));
} else if let Some(var) = part.strip_prefix('{').and_then(|p| p.strip_suffix('}')) {
if !is_valid_plain_chunk(var) {
return Err(PatternError::BadVarName(var.to_string()));
}
chunks.push(PatternChunk::Var(var.to_string()));
} else {
if !is_valid_plain_chunk(part) {
return Err(PatternError::BadChunk(part.to_string()));
}
chunks.push(PatternChunk::Literal(part.to_string()));
}
}
Ok(SubjectPattern {
chunks,
text: pattern.to_string(),
})
}
pub fn chunks(&self) -> &[PatternChunk] {
&self.chunks
}
pub fn as_str(&self) -> &str {
&self.text
}
pub fn matches(&self, tail: &[&str]) -> Option<Vec<(&str, String)>> {
let has_rest = matches!(self.chunks.last(), Some(PatternChunk::Rest(_)));
let fixed = if has_rest {
self.chunks.len() - 1
} else {
self.chunks.len()
};
if has_rest {
if tail.len() <= fixed {
return None;
}
} else if tail.len() != fixed {
return None;
}
let mut binds = Vec::new();
for (i, c) in self.chunks.iter().enumerate() {
match c {
PatternChunk::Literal(l) => {
if tail[i] != l {
return None;
}
}
PatternChunk::Var(v) => binds.push((v.as_str(), tail[i].to_string())),
PatternChunk::Rest(v) => binds.push((v.as_str(), tail[i..].join("/"))),
}
}
Some(binds)
}
fn rank(chunk: &PatternChunk) -> u8 {
match chunk {
PatternChunk::Literal(_) => 0,
PatternChunk::Var(_) => 1,
PatternChunk::Rest(_) => 2,
}
}
pub fn precedence_cmp(&self, other: &Self) -> std::cmp::Ordering {
self.chunks
.iter()
.map(Self::rank)
.cmp(other.chunks.iter().map(Self::rank))
.then_with(|| self.text.as_str().cmp(other.text.as_str()))
}
pub fn selector_tail(&self) -> String {
let parts: Vec<&str> = self
.chunks
.iter()
.map(|c| match c {
PatternChunk::Literal(l) => l.as_str(),
PatternChunk::Var(_) => "*",
PatternChunk::Rest(_) => "**",
})
.collect();
parts.join("/")
}
pub fn ke_format_spec(&self) -> Option<String> {
let mut out = String::new();
for (i, c) in self.chunks.iter().enumerate() {
if i > 0 {
out.push('/');
}
match c {
PatternChunk::Literal(l) => out.push_str(l),
PatternChunk::Var(v) => {
out.push_str("${");
out.push_str(v);
out.push_str(":*}");
}
PatternChunk::Rest(_) => return None,
}
}
Some(out)
}
}
impl fmt::Display for SubjectPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.text)
}
}
pub fn best_match<'p>(
patterns: &'p [SubjectPattern],
tail: &[&str],
) -> Option<(usize, Vec<(&'p str, String)>)> {
let mut order: Vec<usize> = (0..patterns.len()).collect();
order.sort_by(|&a, &b| patterns[a].precedence_cmp(&patterns[b]));
for idx in order {
if let Some(binds) = patterns[idx].matches(tail) {
return Some((idx, binds));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn p(s: &str) -> SubjectPattern {
SubjectPattern::parse(s).unwrap()
}
#[test]
fn precedence_cmp_agrees_with_the_key_it_replaced() {
fn key(p: &SubjectPattern) -> (Vec<u8>, &str) {
let ranks = p
.chunks()
.iter()
.map(|c| match c {
PatternChunk::Literal(_) => 0u8,
PatternChunk::Var(_) => 1,
PatternChunk::Rest(_) => 2,
})
.collect();
(ranks, p.as_str())
}
let pats: Vec<SubjectPattern> = [
"flow/red/count",
"flow/{quantile}/count",
"flow/{rest...}",
"{device}/count",
"{device}/{metric...}",
"a",
"a/b",
"flow/{a}/count",
"flow/{b}/count",
]
.iter()
.map(|p| SubjectPattern::parse(p).unwrap())
.collect();
for a in &pats {
for b in &pats {
assert_eq!(
a.precedence_cmp(b),
key(a).cmp(&key(b)),
"{} vs {}",
a.as_str(),
b.as_str()
);
}
}
let mut order: Vec<&SubjectPattern> = pats.iter().collect();
order.sort_by(|a, b| a.precedence_cmp(b));
assert_eq!(order[0].as_str(), "a");
assert_eq!(order.last().unwrap().as_str(), "{device}/{metric...}");
}
#[test]
fn parse_rules() {
assert!(SubjectPattern::parse("").is_err());
assert!(matches!(
SubjectPattern::parse("{rest...}/x"),
Err(PatternError::RestNotTrailing(_))
));
assert!(matches!(
SubjectPattern::parse("{Bad Var}"),
Err(PatternError::BadVarName(_))
));
assert!(matches!(
SubjectPattern::parse("UPPER/x"),
Err(PatternError::BadChunk(_))
));
assert_eq!(
p("flow/red/{quantile}").chunks(),
&[
PatternChunk::Literal("flow".into()),
PatternChunk::Literal("red".into()),
PatternChunk::Var("quantile".into()),
]
);
}
#[test]
fn matching_binds_named_vars() {
assert_eq!(
p("flow/red/{quantile}").matches(&["flow", "red", "p95_ms"]),
Some(vec![("quantile", "p95_ms".to_string())])
);
assert_eq!(p("flow/red/{quantile}").matches(&["flow", "red"]), None);
assert_eq!(p("health").matches(&["health"]), Some(vec![]));
let dev = p("{device}/{metric...}");
assert_eq!(
dev.matches(&["sw1", "if", "eth0", "rx"]),
Some(vec![
("device", "sw1".to_string()),
("metric", "if/eth0/rx".to_string()),
])
);
assert_eq!(dev.matches(&["sw1"]), None, "rest requires >= 1 chunk");
}
#[test]
fn precedence_literal_beats_var_beats_rest() {
let patterns = [p("{device}/{metric...}"), p("health"), p("{var}")];
let (idx, _) = best_match(&patterns, &["health"]).unwrap();
assert_eq!(patterns[idx].as_str(), "health");
let (idx, binds) = best_match(&patterns, &["other"]).unwrap();
assert_eq!(patterns[idx].as_str(), "{var}");
assert_eq!(binds, vec![("var", "other".to_string())]);
let (idx, _) = best_match(&patterns, &["sw1", "x"]).unwrap();
assert_eq!(patterns[idx].as_str(), "{device}/{metric...}");
}
#[test]
fn selector_tails() {
assert_eq!(p("flow/red/{quantile}").selector_tail(), "flow/red/*");
assert_eq!(p("{device}/{metric...}").selector_tail(), "*/**");
assert_eq!(p("health").selector_tail(), "health");
}
#[test]
fn ke_format_bridge_parity_on_expressible_patterns() {
use zenoh_keyexpr::format::KeFormat;
use zenoh_keyexpr::keyexpr;
let pat = p("flow/red/{quantile}");
let spec = pat.ke_format_spec().unwrap();
let format = KeFormat::new(&spec).unwrap();
let parsed = format
.parse(keyexpr::new("flow/red/p95_ms").unwrap())
.unwrap();
let ke_bound: &str = parsed.get("quantile").unwrap();
let hand_bound = pat.matches(&["flow", "red", "p95_ms"]).unwrap();
assert_eq!(hand_bound, vec![("quantile", ke_bound.to_string())]);
assert_eq!(p("{device}/{metric...}").ke_format_spec(), None);
}
}