use crate::sdf::Path;
#[derive(Debug, Clone, PartialEq)]
enum PathMap {
Empty,
Single((Path, Path)),
Multi(Vec<(Path, Path)>),
}
impl PathMap {
fn as_slice(&self) -> &[(Path, Path)] {
match self {
PathMap::Empty => &[],
PathMap::Single(pair) => std::slice::from_ref(pair),
PathMap::Multi(pairs) => pairs,
}
}
fn len(&self) -> usize {
match self {
PathMap::Empty => 0,
PathMap::Single(_) => 1,
PathMap::Multi(pairs) => pairs.len(),
}
}
fn is_empty(&self) -> bool {
matches!(self, PathMap::Empty)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MapFunction {
path_map: PathMap,
}
impl MapFunction {
pub fn identity() -> Self {
Self {
path_map: PathMap::Single((Path::abs_root(), Path::abs_root())),
}
}
pub fn null() -> Self {
Self {
path_map: PathMap::Empty,
}
}
pub fn new(pairs: impl Into<Vec<(Path, Path)>>) -> Self {
let mut pairs: Vec<(Path, Path)> = pairs.into();
match pairs.len() {
0 => Self::null(),
1 => Self {
path_map: PathMap::Single(pairs.remove(0)),
},
_ => {
pairs.sort_by(|a, b| b.0.as_str().len().cmp(&a.0.as_str().len()));
Self {
path_map: PathMap::Multi(pairs),
}
}
}
}
pub fn from_pair(source: Path, target: Path) -> Self {
Self {
path_map: PathMap::Single((source, target)),
}
}
pub fn from_pair_identity(source: Path, target: Path) -> Self {
Self::new(vec![(source, target), (Path::abs_root(), Path::abs_root())])
}
pub fn is_identity(&self) -> bool {
self.path_map.len() == 1
&& self.path_map.as_slice()[0].0.as_str() == "/"
&& self.path_map.as_slice()[0].1.as_str() == "/"
}
pub fn is_noop(&self) -> bool {
!self.path_map.is_empty() && self.path_map.as_slice().iter().all(|(s, t)| s == t)
}
pub fn is_null(&self) -> bool {
self.path_map.is_empty()
}
pub fn path_pairs(&self) -> &[(Path, Path)] {
self.path_map.as_slice()
}
pub fn map_source_to_target(&self, path: &Path) -> Option<Path> {
for (source, target) in self.path_map.as_slice() {
if let Some(mapped) = path.replace_prefix(source, target) {
return Some(mapped);
}
}
None
}
pub fn map_target_to_source(&self, path: &Path) -> Option<Path> {
let mut best: Option<Path> = None;
let mut best_len = 0;
for (source, target) in self.path_map.as_slice() {
let tgt_len = target.as_str().len();
if tgt_len > best_len {
if let Some(mapped) = path.replace_prefix(target, source) {
best = Some(mapped);
best_len = tgt_len;
}
}
}
best
}
pub fn compose(&self, inner: &MapFunction) -> MapFunction {
if self.is_identity() {
return inner.clone();
}
if inner.is_identity() {
return self.clone();
}
let mut pairs = Vec::new();
for (inner_src, inner_tgt) in inner.path_map.as_slice() {
if let Some(composed_tgt) = self.map_source_to_target(inner_tgt) {
pairs.push((inner_src.clone(), composed_tgt));
}
}
MapFunction::new(pairs)
}
pub fn inverse(&self) -> MapFunction {
let pairs: Vec<_> = self
.path_map
.as_slice()
.iter()
.map(|(s, t)| (t.clone(), s.clone()))
.collect();
MapFunction::new(pairs)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(s: &str) -> Path {
Path::from(s.to_string())
}
#[test]
fn identity() {
let m = MapFunction::identity();
assert!(m.is_identity());
assert!(!m.is_null());
assert_eq!(m.map_source_to_target(&p("/Foo/Bar")), Some(p("/Foo/Bar")));
assert_eq!(m.map_target_to_source(&p("/Foo/Bar")), Some(p("/Foo/Bar")));
}
#[test]
fn null() {
let m = MapFunction::null();
assert!(m.is_null());
assert!(!m.is_identity());
assert_eq!(m.map_source_to_target(&p("/Foo")), None);
assert_eq!(m.map_target_to_source(&p("/Foo")), None);
}
#[test]
fn single_pair_mapping() {
let m = MapFunction::new(vec![(p("/RefPrim"), p("/MyPrim"))]);
assert_eq!(m.map_source_to_target(&p("/RefPrim")), Some(p("/MyPrim")));
assert_eq!(m.map_source_to_target(&p("/RefPrim/Child")), Some(p("/MyPrim/Child")));
assert_eq!(m.map_source_to_target(&p("/Other")), None);
assert_eq!(m.map_target_to_source(&p("/MyPrim")), Some(p("/RefPrim")));
assert_eq!(m.map_target_to_source(&p("/MyPrim/Child")), Some(p("/RefPrim/Child")));
assert_eq!(m.map_target_to_source(&p("/Other")), None);
}
#[test]
fn longest_prefix_wins() {
let m = MapFunction::new(vec![(p("/A"), p("/X")), (p("/A/B"), p("/Y"))]);
assert_eq!(m.map_source_to_target(&p("/A/B/C")), Some(p("/Y/C")));
assert_eq!(m.map_source_to_target(&p("/A/C")), Some(p("/X/C")));
}
#[test]
fn compose_simple() {
let inner = MapFunction::new(vec![(p("/Ref"), p("/Class"))]);
let outer = MapFunction::new(vec![(p("/Class"), p("/Prim"))]);
let composed = outer.compose(&inner);
assert_eq!(composed.map_source_to_target(&p("/Ref")), Some(p("/Prim")));
assert_eq!(composed.map_source_to_target(&p("/Ref/Child")), Some(p("/Prim/Child")));
}
#[test]
fn compose_with_identity() {
let m = MapFunction::new(vec![(p("/A"), p("/B"))]);
let id = MapFunction::identity();
let c1 = id.compose(&m);
assert_eq!(c1.map_source_to_target(&p("/A")), Some(p("/B")));
let c2 = m.compose(&id);
assert_eq!(c2.map_source_to_target(&p("/A")), Some(p("/B")));
}
#[test]
fn inverse_roundtrip() {
let m = MapFunction::new(vec![(p("/Src"), p("/Tgt"))]);
let inv = m.inverse();
assert_eq!(inv.map_source_to_target(&p("/Tgt")), Some(p("/Src")));
assert_eq!(inv.map_source_to_target(&p("/Tgt/Child")), Some(p("/Src/Child")));
let m2 = inv.inverse();
assert_eq!(m2.map_source_to_target(&p("/Src")), Some(p("/Tgt")));
}
#[test]
fn compose_chain() {
let f1 = MapFunction::new(vec![(p("/A"), p("/B"))]);
let f2 = MapFunction::new(vec![(p("/B"), p("/C"))]);
let composed = f2.compose(&f1);
assert_eq!(composed.map_source_to_target(&p("/A")), Some(p("/C")));
assert_eq!(composed.map_source_to_target(&p("/A/D")), Some(p("/C/D")));
}
#[test]
fn compose_drops_unmappable() {
let inner = MapFunction::new(vec![(p("/A"), p("/B"))]);
let outer = MapFunction::new(vec![(p("/C"), p("/D"))]);
let composed = outer.compose(&inner);
assert_eq!(composed.map_source_to_target(&p("/A")), None);
}
#[test]
fn from_pair_is_single() {
let m = MapFunction::from_pair(p("/A"), p("/B"));
assert!(matches!(m.path_map, PathMap::Single(_)));
assert_eq!(m.map_source_to_target(&p("/A")), Some(p("/B")));
assert_eq!(m.map_source_to_target(&p("/A/C")), Some(p("/B/C")));
assert_eq!(m.map_source_to_target(&p("/Other")), None);
assert_eq!(m.map_target_to_source(&p("/B")), Some(p("/A")));
}
#[test]
fn from_pair_identity_includes_catch_all() {
let m = MapFunction::from_pair_identity(p("/A"), p("/B"));
assert_eq!(m.map_source_to_target(&p("/A")), Some(p("/B")));
assert_eq!(m.map_source_to_target(&p("/A/C")), Some(p("/B/C")));
assert_eq!(m.map_source_to_target(&p("/Other")), Some(p("/Other")));
assert_eq!(m.map_source_to_target(&p("/Other/D")), Some(p("/Other/D")));
assert_eq!(m.map_target_to_source(&p("/B")), Some(p("/A")));
assert_eq!(m.map_target_to_source(&p("/Other")), Some(p("/Other")));
}
#[test]
fn from_pair_identity_is_not_identity() {
let m = MapFunction::from_pair_identity(p("/A"), p("/B"));
assert!(!m.is_identity());
assert!(!m.is_noop());
assert!(!m.is_null());
}
#[test]
fn from_pair_identity_noop_when_same_source_target() {
let m = MapFunction::from_pair_identity(p("/A"), p("/A"));
assert!(!m.is_identity());
assert!(m.is_noop());
assert_eq!(m.map_source_to_target(&p("/A")), Some(p("/A")));
assert_eq!(m.map_source_to_target(&p("/Other")), Some(p("/Other")));
}
#[test]
fn compose_with_identity_pairs() {
let inner = MapFunction::from_pair_identity(p("/B"), p("/A"));
let outer = MapFunction::from_pair_identity(p("/A"), p("/C"));
let composed = outer.compose(&inner);
assert_eq!(composed.map_source_to_target(&p("/B")), Some(p("/C")));
assert_eq!(composed.map_source_to_target(&p("/B/X")), Some(p("/C/X")));
assert_eq!(composed.map_source_to_target(&p("/Other")), Some(p("/Other")));
}
#[test]
fn inverse_preserves_identity_pair() {
let m = MapFunction::from_pair_identity(p("/Src"), p("/Tgt"));
let inv = m.inverse();
assert_eq!(inv.map_source_to_target(&p("/Tgt")), Some(p("/Src")));
assert_eq!(inv.map_source_to_target(&p("/Tgt/Child")), Some(p("/Src/Child")));
assert_eq!(inv.map_source_to_target(&p("/Other")), Some(p("/Other")));
}
#[test]
fn new_single_pair_uses_single_variant() {
let m = MapFunction::new(vec![(p("/X"), p("/Y"))]);
assert!(matches!(m.path_map, PathMap::Single(_)));
}
}