use std::fmt;
use crate::sdf::Path;
use super::predicate::PredicateExpression;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct PathPattern {
prefix: Path,
components: Vec<Component>,
pred_exprs: Vec<PredicateExpression>,
is_property: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Component {
pub text: String,
pub predicate_index: Option<usize>,
pub is_literal: bool,
}
impl Component {
pub fn is_stretch(&self) -> bool {
self.text.is_empty() && self.predicate_index.is_none()
}
}
impl PathPattern {
pub fn everything() -> Self {
PathPattern {
prefix: Path::abs_root(),
components: vec![Component {
text: String::new(),
predicate_index: None,
is_literal: false,
}],
pred_exprs: Vec::new(),
is_property: false,
}
}
pub fn every_descendant() -> Self {
let mut pattern = Self::everything();
pattern.prefix = reflexive_relative();
pattern
}
pub fn nothing() -> Self {
Self::default()
}
pub fn is_nothing(&self) -> bool {
self.prefix.is_empty()
}
pub fn prefix(&self) -> &Path {
&self.prefix
}
pub fn set_prefix(&mut self, prefix: Path) {
if self.components.is_empty() {
self.is_property = prefix.is_property_path();
}
self.prefix = prefix;
}
pub fn components(&self) -> &[Component] {
&self.components
}
pub fn pred_exprs(&self) -> &[PredicateExpression] {
&self.pred_exprs
}
pub fn is_property(&self) -> bool {
self.is_property
}
pub fn has_leading_stretch(&self) -> bool {
self.prefix.is_abs_root() && self.components.first().is_some_and(Component::is_stretch)
}
pub fn has_trailing_stretch(&self) -> bool {
!self.is_property && self.components.last().is_some_and(Component::is_stretch)
}
pub fn can_append_child(&self, text: &str) -> bool {
if self.is_property {
return false;
}
!(text.is_empty() && self.components.last().is_some_and(Component::is_stretch))
}
pub fn append_child(&mut self, text: impl Into<String>, predicate: Option<PredicateExpression>) {
let text = text.into();
if self.prefix.is_empty() {
self.prefix = reflexive_relative();
}
let is_literal = Path::is_valid_identifier(&text);
if (is_literal || text == "..") && predicate.is_none() && self.components.is_empty() {
self.prefix = append_prefix_child(&self.prefix, &text);
return;
}
let predicate_index = predicate.map(|expr| {
self.pred_exprs.push(expr);
self.pred_exprs.len() - 1
});
self.components.push(Component {
text,
predicate_index,
is_literal,
});
}
pub fn append_stretch_if_possible(&mut self) {
if self.can_append_child("") {
self.append_child("", None);
}
}
pub fn append_property(&mut self, text: impl Into<String>, predicate: Option<PredicateExpression>) {
let text = text.into();
if self.has_trailing_stretch() {
self.append_child("*", None);
}
if self.prefix.is_empty() {
self.prefix = reflexive_relative();
}
let is_literal = Path::is_valid_namespace_identifier(&text);
if is_literal
&& predicate.is_none()
&& self.components.is_empty()
&& let Some(folded) = append_prefix_property(&self.prefix, &text)
{
self.prefix = folded;
self.is_property = true;
return;
}
let predicate_index = predicate.map(|expr| {
self.pred_exprs.push(expr);
self.pred_exprs.len() - 1
});
self.components.push(Component {
text,
predicate_index,
is_literal,
});
self.is_property = true;
}
}
impl fmt::Display for PathPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut result = String::new();
if self.prefix == reflexive_relative() {
if self.components.is_empty() || self.components[0].is_stretch() {
result.push('.');
}
} else {
result.push_str(self.prefix.as_str());
}
let prefix_is_abs_root = self.prefix.is_abs_root();
for (i, component) in self.components.iter().enumerate() {
if component.is_stretch() {
result.push_str(if i == 0 && prefix_is_abs_root { "/" } else { "//" });
continue;
}
if i + 1 == self.components.len() && self.is_property {
result.push('.');
} else if !result.is_empty() && !result.ends_with('/') {
result.push('/');
}
result.push_str(&component.text);
if let Some(index) = component.predicate_index {
result.push('{');
result.push_str(&self.pred_exprs[index].to_string());
result.push('}');
}
}
f.write_str(&result)
}
}
fn reflexive_relative() -> Path {
Path::new(".").expect("the reflexive relative path is valid")
}
fn append_prefix_child(prefix: &Path, name: &str) -> Path {
let base = prefix.as_str();
let combined = if base == "." {
name.to_string()
} else if base.ends_with('/') {
format!("{base}{name}")
} else {
format!("{base}/{name}")
};
Path::new(&combined).expect("a pattern prefix concatenation is a valid path string")
}
fn append_prefix_property(prefix: &Path, name: &str) -> Option<Path> {
let base = prefix.as_str();
if base == "." {
return Path::new(&format!(".{name}")).ok();
}
if base.ends_with("..") {
return None;
}
prefix.append_property(name).ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn build(prefix: &str, children: &[&str]) -> PathPattern {
let mut pattern = PathPattern::default();
if !prefix.is_empty() {
pattern.set_prefix(Path::new(prefix).unwrap());
}
for child in children {
if child.is_empty() {
pattern.append_stretch_if_possible();
} else {
pattern.append_child(*child, None);
}
}
pattern
}
#[test]
fn literal_run_folds() {
let pattern = build("/", &["foo", "bar", "baz"]);
assert_eq!(pattern.prefix().as_str(), "/foo/bar/baz");
assert!(pattern.components().is_empty());
assert_eq!(pattern.to_string(), "/foo/bar/baz");
}
#[test]
fn glob_stops_folding() {
let mut pattern = build("/", &["foo"]);
pattern.append_child("b*r", None);
pattern.append_child("baz", None);
assert_eq!(pattern.prefix().as_str(), "/foo");
assert_eq!(pattern.components().len(), 2);
assert!(!pattern.components()[0].is_literal);
assert!(pattern.components()[1].is_literal);
assert_eq!(pattern.to_string(), "/foo/b*r/baz");
}
#[test]
fn stretch_text() {
assert_eq!(PathPattern::everything().to_string(), "//");
assert_eq!(PathPattern::every_descendant().to_string(), ".//");
let pattern = build("/", &["foo", "", "bar"]);
assert_eq!(pattern.to_string(), "/foo//bar");
assert!(!pattern.has_leading_stretch());
assert!(!pattern.has_trailing_stretch());
let trailing = build("/", &["foo", ""]);
assert!(trailing.has_trailing_stretch());
assert_eq!(trailing.to_string(), "/foo//");
}
#[test]
fn no_double_stretch() {
let mut pattern = build("/", &["foo", ""]);
pattern.append_stretch_if_possible();
assert_eq!(pattern.components().len(), 1, "stretch after stretch is refused");
}
#[test]
fn property_after_stretch_gains_wildcard() {
let mut pattern = build("/", &["x", ""]);
pattern.append_property("foo", None);
assert_eq!(pattern.to_string(), "/x//*.foo");
assert!(pattern.is_property());
assert!(!pattern.has_trailing_stretch());
}
#[test]
fn literal_property_folds() {
let mut pattern = build("/", &["foo"]);
pattern.append_property("attr:ns", None);
assert_eq!(pattern.prefix().as_str(), "/foo.attr:ns");
assert!(pattern.components().is_empty());
assert!(pattern.is_property());
assert_eq!(pattern.to_string(), "/foo.attr:ns");
}
#[test]
fn relative_forms() {
let pattern = build("", &["foo", "bar"]);
assert_eq!(pattern.prefix().as_str(), "foo/bar");
assert_eq!(pattern.to_string(), "foo/bar");
let mut dotted = PathPattern::default();
dotted.append_child("..", None);
dotted.append_child("..", None);
dotted.append_child("sib", None);
assert_eq!(dotted.prefix().as_str(), "../../sib");
assert_eq!(dotted.to_string(), "../../sib");
}
#[test]
fn nothing_is_default() {
assert!(PathPattern::nothing().is_nothing());
assert!(!PathPattern::everything().is_nothing());
assert_eq!(PathPattern::nothing().to_string(), "");
}
}