use super::path_pattern::PathPattern;
use blazingly_json::Value;
use std::collections::BTreeMap;
pub(super) struct Target {
segments: Vec<Segment>,
max_ref: usize,
}
enum Segment {
Literal(String),
Reference(usize),
}
impl Target {
pub(super) fn parse(pattern: &str) -> Self {
let mut segments = Vec::new();
let mut literal = String::new();
let mut max_ref = 0;
let mut characters = pattern.chars().peekable();
while let Some(value) = characters.next() {
let reference = (value == '$')
.then(|| characters.peek().and_then(|next| "123456789".find(*next)))
.flatten();
let Some(index) = reference else {
literal.push(value);
continue;
};
characters.next();
if !literal.is_empty() {
segments.push(Segment::Literal(std::mem::take(&mut literal)));
}
max_ref = max_ref.max(index + 1);
segments.push(Segment::Reference(index));
}
if !literal.is_empty() {
segments.push(Segment::Literal(literal));
}
Self { segments, max_ref }
}
pub(super) fn max_ref(&self) -> usize {
self.max_ref
}
pub(super) fn instantiate(&self, captures: &[Option<String>]) -> String {
self.segments
.iter()
.map(|segment| match segment {
Segment::Literal(text) => text.clone(),
Segment::Reference(index) => captures
.get(*index)
.and_then(Option::as_ref)
.map(|text| escaped(text))
.unwrap_or_default(),
})
.collect()
}
}
fn escaped(text: &str) -> String {
let mut output = String::with_capacity(text.len());
for value in text.chars() {
if !value.is_ascii_alphanumeric() {
output.push('\\');
}
output.push(value);
}
output
}
pub(super) type TargetCache = BTreeMap<String, Option<PathPattern>>;
pub(super) struct TargetSelector {
include: Option<TargetPattern>,
exclude: Option<TargetPattern>,
}
enum TargetPattern {
Fixed(PathPattern),
Templated(Target),
}
impl TargetSelector {
pub(super) fn compile(rule: &Value) -> Option<Self> {
Some(Self {
include: target_pattern(rule, "toPath").ok()?,
exclude: target_pattern(rule, "toPathNot").ok()?,
})
}
pub(super) fn selects(
&self,
path: &str,
captures: &[Option<String>],
cache: &mut TargetCache,
) -> bool {
let included = match &self.include {
None => true,
Some(pattern) => matched(pattern, path, captures, cache),
};
let excluded = match &self.exclude {
None => false,
Some(pattern) => matched(pattern, path, captures, cache),
};
included && !excluded
}
}
fn matched(
pattern: &TargetPattern,
path: &str,
captures: &[Option<String>],
cache: &mut TargetCache,
) -> bool {
match pattern {
TargetPattern::Fixed(compiled) => compiled.matches(path).is_some(),
TargetPattern::Templated(template) => cache
.entry(template.instantiate(captures))
.or_insert_with_key(|key| PathPattern::compile(key).ok())
.as_ref()
.is_some_and(|compiled| compiled.matches(path).is_some()),
}
}
fn target_pattern(rule: &Value, key: &str) -> Result<Option<TargetPattern>, ()> {
let Some(raw) = rule.get(key) else {
return Ok(None);
};
let Some(raw) = raw.as_str() else {
return Err(());
};
let template = Target::parse(raw);
if template.max_ref() == 0 {
return match PathPattern::compile(raw) {
Ok(compiled) => Ok(Some(TargetPattern::Fixed(compiled))),
Err(_) => Err(()),
};
}
Ok(Some(TargetPattern::Templated(template)))
}