use std::collections::BTreeMap;
pub(crate) fn normalize_path(path: &str) -> String {
path.trim_matches('.').to_owned()
}
pub(crate) fn join_path(parent: &str, child: &str) -> String {
if parent.is_empty() {
child.to_owned()
} else {
format!("{parent}.{child}")
}
}
pub(crate) fn path_matches_pattern(path: &str, pattern: &str) -> bool {
let actual_segments = path_segments(path);
let pattern_segments = path_segments(pattern);
actual_segments.len() == pattern_segments.len()
&& actual_segments
.iter()
.zip(pattern_segments.iter())
.all(|(actual, expected)| *expected == "*" || actual == expected)
}
pub(crate) fn path_starts_with_pattern(path: &str, pattern: &str) -> bool {
let actual_segments = path_segments(path);
let pattern_segments = path_segments(pattern);
actual_segments.len() >= pattern_segments.len()
&& actual_segments
.iter()
.zip(pattern_segments.iter())
.all(|(actual, expected)| *expected == "*" || actual == expected)
}
pub(crate) fn path_overlaps_pattern(path: &str, pattern: &str) -> bool {
let actual_segments = path_segments(path);
let pattern_segments = path_segments(pattern);
let shared = actual_segments.len().min(pattern_segments.len());
actual_segments
.iter()
.take(shared)
.zip(pattern_segments.iter().take(shared))
.all(|(actual, expected)| *expected == "*" || *actual == "*" || actual == expected)
}
pub(crate) fn path_is_at_or_below(path: &str, parent: &str) -> bool {
path_child_segments(path, parent).is_some()
}
pub(crate) fn path_child_segments<'a>(path: &'a str, parent: &str) -> Option<Vec<&'a str>> {
let child_segments = path_segments(path);
let parent_segments = path_segments(parent);
if child_segments.len() < parent_segments.len()
|| !child_segments
.iter()
.zip(parent_segments.iter())
.all(|(actual, expected)| actual == expected)
{
return None;
}
Some(child_segments[parent_segments.len()..].to_vec())
}
pub(crate) fn direct_child_array_index(container_path: &str, entry_path: &str) -> Option<usize> {
path_child_segments(entry_path, container_path)?
.first()
.and_then(|segment| segment.parse::<usize>().ok())
}
pub(crate) fn concrete_paths_overlap(left: &str, right: &str) -> bool {
path_is_at_or_below(left, right) || path_is_at_or_below(right, left)
}
pub(crate) fn canonicalize_path_with_aliases(
path: &str,
aliases: &BTreeMap<String, String>,
) -> String {
let normalized = normalize_path(path);
if normalized.is_empty() || aliases.is_empty() {
return normalized;
}
let path_segments = normalized.split('.').collect::<Vec<_>>();
let mut best = None::<(usize, usize, String)>;
for (alias, canonical) in aliases {
let alias_segments = alias.split('.').collect::<Vec<_>>();
if alias_segments.len() > path_segments.len() {
continue;
}
let matched = alias_segments
.iter()
.zip(path_segments.iter())
.all(|(expected, actual)| *expected == "*" || expected == actual);
if !matched {
continue;
}
let specificity = alias_segments
.iter()
.filter(|segment| **segment != "*")
.count();
let candidate = rewrite_alias_path(&path_segments, &alias_segments, canonical);
match &mut best {
Some((best_len, best_specificity, best_candidate))
if alias_segments.len() > *best_len
|| (alias_segments.len() == *best_len && specificity > *best_specificity) =>
{
*best_len = alias_segments.len();
*best_specificity = specificity;
*best_candidate = candidate;
}
None => best = Some((alias_segments.len(), specificity, candidate)),
_ => {}
}
}
best.map_or(normalized, |(_, _, candidate)| candidate)
}
fn rewrite_alias_path(path_segments: &[&str], alias_segments: &[&str], canonical: &str) -> String {
let canonical_segments = canonical
.split('.')
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>();
let mut rewritten = canonical_segments
.iter()
.enumerate()
.map(|(index, segment)| {
if *segment == "*" && alias_segments.get(index) == Some(&"*") {
path_segments[index].to_owned()
} else {
(*segment).to_owned()
}
})
.collect::<Vec<_>>();
rewritten.extend(
path_segments[alias_segments.len()..]
.iter()
.map(|segment| (*segment).to_owned()),
);
normalize_path(&rewritten.join("."))
}
pub(crate) fn path_segments(path: &str) -> Vec<&str> {
path.split('.')
.filter(|segment| !segment.is_empty())
.collect()
}
#[cfg(test)]
mod tests {
use super::{
concrete_paths_overlap, direct_child_array_index, path_child_segments, path_is_at_or_below,
};
#[test]
fn concrete_path_prefix_checks_are_segment_aware() {
assert!(path_is_at_or_below("db.token", "db"));
assert!(path_is_at_or_below("db", "db"));
assert!(path_is_at_or_below("db", ""));
assert!(!path_is_at_or_below("database.token", "db"));
assert!(!path_is_at_or_below("db", "db.token"));
}
#[test]
fn concrete_path_overlap_checks_are_segment_aware() {
assert!(concrete_paths_overlap("db", "db.token"));
assert!(concrete_paths_overlap("db.token", "db"));
assert!(concrete_paths_overlap("db.token", "db.token"));
assert!(!concrete_paths_overlap("db", "database"));
assert!(!concrete_paths_overlap("db.token", "db_token"));
}
#[test]
fn child_path_segments_return_descendant_tail() {
assert_eq!(
path_child_segments("users.0.name", "users"),
Some(vec!["0", "name"])
);
assert_eq!(
path_child_segments("users.0.name", ""),
Some(vec!["users", "0", "name"])
);
assert_eq!(path_child_segments("users", "users"), Some(Vec::new()));
assert_eq!(path_child_segments("users_profile.0", "users"), None);
}
#[test]
fn direct_child_array_index_is_segment_aware() {
assert_eq!(direct_child_array_index("users", "users.0.name"), Some(0));
assert_eq!(direct_child_array_index("", "0.name"), Some(0));
assert_eq!(direct_child_array_index("users", "users_profile.0"), None);
assert_eq!(direct_child_array_index("users.0", "users.0.name"), None);
}
}