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)
}
pub fn precedence(&self) -> (Vec<u8>, &str) {
let ranks = self
.chunks
.iter()
.map(|c| match c {
PatternChunk::Literal(_) => 0u8,
PatternChunk::Var(_) => 1,
PatternChunk::Rest(_) => 2,
})
.collect();
(ranks, &self.text)
}
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].precedence()));
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 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);
}
}