symbolic_common/
sourcelinks.rs1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
15enum Pattern {
16 Exact(String),
17 Prefix(String),
18}
19
20impl Pattern {
21 fn parse(input: &str) -> Self {
22 if let Some(prefix) = input.strip_suffix('*') {
23 Pattern::Prefix(prefix.to_lowercase())
24 } else {
25 Pattern::Exact(input.to_lowercase())
26 }
27 }
28}
29
30impl Ord for Pattern {
31 fn cmp(&self, other: &Self) -> Ordering {
32 match (self, other) {
33 (Pattern::Exact(s), Pattern::Exact(t)) => s.cmp(t),
34 (Pattern::Exact(_), Pattern::Prefix(_)) => Ordering::Less,
35 (Pattern::Prefix(_), Pattern::Exact(_)) => Ordering::Greater,
36 (Pattern::Prefix(s), Pattern::Prefix(t)) => match s.len().cmp(&t.len()) {
37 Ordering::Greater => Ordering::Less,
38 Ordering::Equal => s.cmp(t),
39 Ordering::Less => Ordering::Greater,
40 },
41 }
42 }
43}
44
45impl PartialOrd for Pattern {
46 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
47 Some(self.cmp(other))
48 }
49}
50
51#[derive(Debug, Default, Clone, PartialEq, Eq)]
67pub struct SourceLinkMappings {
68 mappings: BTreeMap<Pattern, String>,
69}
70
71impl<'a> Extend<(&'a str, &'a str)> for SourceLinkMappings {
72 fn extend<T: IntoIterator<Item = (&'a str, &'a str)>>(&mut self, iter: T) {
73 self.mappings.extend(
74 iter.into_iter()
75 .map(|(k, v)| (Pattern::parse(k), v.to_string())),
76 )
77 }
78}
79
80impl SourceLinkMappings {
81 pub fn new<'a, I: IntoIterator<Item = (&'a str, &'a str)>>(iter: I) -> Self {
83 let mut res = Self::default();
84 res.extend(iter);
85 res
86 }
87 pub fn is_empty(&self) -> bool {
89 self.mappings.is_empty()
90 }
91
92 pub fn resolve(&self, path: &str) -> Option<String> {
94 let path_lower = path.to_lowercase();
98 for (pattern, target) in &self.mappings {
99 match &pattern {
100 Pattern::Exact(value) => {
101 if value == &path_lower {
102 return Some(target.clone());
103 }
104 }
105 Pattern::Prefix(value) => {
106 if path_lower.starts_with(value) {
107 let replacement = path
108 .get(value.len()..)
109 .unwrap_or_default()
110 .replace('\\', "/");
111 return Some(target.replace('*', &replacement));
112 }
113 }
114 }
115 }
116 None
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_mapping() {
126 let mappings = vec![
127 ("C:\\src\\*", "http://MyDefaultDomain.com/src/*"),
128 ("C:\\src\\fOO\\*", "http://MyFooDomain.com/src/*"),
129 (
130 "C:\\src\\foo\\specific.txt",
131 "http://MySpecificFoodDomain.com/src/specific.txt",
132 ),
133 ("C:\\src\\bar\\*", "http://MyBarDomain.com/src/*"),
134 ("C:\\src\\file.txt", "https://example.com/file.txt"),
135 ("/home/user/src/*", "https://linux.com/*"),
136 ];
137
138 let mappings = SourceLinkMappings::new(mappings);
139
140 assert_eq!(mappings.mappings.len(), 6);
141
142 assert!(mappings.resolve("c:\\other\\path").is_none());
147 assert!(mappings.resolve("/home/path").is_none());
148 assert_eq!(
149 mappings.resolve("c:\\src\\bAr\\foo\\FiLe.txt").unwrap(),
150 "http://MyBarDomain.com/src/foo/FiLe.txt"
151 );
152 assert_eq!(
153 mappings.resolve("c:\\src\\foo\\FiLe.txt").unwrap(),
154 "http://MyFooDomain.com/src/FiLe.txt"
155 );
156 assert_eq!(
157 mappings.resolve("c:\\src\\foo\\SpEcIfIc.txt").unwrap(),
158 "http://MySpecificFoodDomain.com/src/specific.txt"
159 );
160 assert_eq!(
161 mappings.resolve("c:\\src\\other\\path").unwrap(),
162 "http://MyDefaultDomain.com/src/other/path"
163 );
164 assert_eq!(
165 mappings.resolve("c:\\src\\other\\path").unwrap(),
166 "http://MyDefaultDomain.com/src/other/path"
167 );
168 assert_eq!(
169 mappings.resolve("/home/user/src/Path/TO/file.txt").unwrap(),
170 "https://linux.com/Path/TO/file.txt"
171 );
172 }
173}