coap_server/app/
path_matcher.rs1use std::collections::hash_map::Values;
2use std::collections::HashMap;
3
4#[derive(Debug)]
7pub struct PathMatcher<V> {
8 map: HashMap<Vec<String>, V>,
9}
10
11impl<V> FromIterator<(Vec<String>, V)> for PathMatcher<V> {
12 fn from_iter<I: IntoIterator<Item = (Vec<String>, V)>>(iter: I) -> Self {
13 Self {
14 map: HashMap::from_iter(iter),
15 }
16 }
17}
18
19impl<V> PathMatcher<V> {
20 pub fn new_empty() -> Self {
21 Self {
22 map: HashMap::default(),
23 }
24 }
25
26 pub fn from_path_strings(src: HashMap<String, V>) -> Self {
29 let iter = src.into_iter().map(|(k, v)| (key_from_path(&k), v));
30 Self::from_iter(iter)
31 }
32
33 pub fn lookup(&self, path: &[String]) -> Option<MatchedResult<V>> {
36 for search_depth in (0..path.len() + 1).rev() {
37 let search_path = &path[0..search_depth];
38 if let Some(value) = self.map.get(search_path) {
39 return Some(MatchedResult {
40 value,
41 matched_index: search_depth,
42 });
43 }
44 }
45 None
46 }
47
48 pub fn match_all(&self, path: &[String]) -> Vec<MatchedResult<V>> {
53 let mut result = Vec::new();
54 for search_depth in (0..path.len() + 1).rev() {
55 let search_path = &path[0..search_depth];
56 if let Some(value) = self.map.get(search_path) {
57 result.push(MatchedResult {
58 value,
59 matched_index: search_depth,
60 });
61 }
62 }
63 result
64 }
65
66 pub fn insert(&mut self, key: Vec<String>, value: V) -> Option<V> {
67 self.map.insert(key, value)
68 }
69
70 pub fn remove(&mut self, key: &[String]) -> Option<V> {
71 self.map.remove(key)
72 }
73
74 pub fn values(&self) -> Values<'_, Vec<String>, V> {
75 self.map.values()
76 }
77
78 pub fn lookup_exact(&self, path: &[String]) -> Option<&V> {
79 self.map.get(path)
80 }
81}
82
83pub fn key_from_path(path: &str) -> Vec<String> {
84 path.split('/')
85 .filter(|s| !s.is_empty())
86 .map(|s| s.to_string())
87 .collect()
88}
89
90pub struct MatchedResult<'a, V> {
95 pub value: &'a V,
96 pub matched_index: usize,
97}
98
99#[cfg(test)]
100mod tests {
101 use crate::app::path_matcher::PathMatcher;
102
103 #[test]
104 fn test_unambiguous_exact_match() {
105 let matcher = new_matcher(["/a/b/c", "/a/b/d"]);
106 assert_exact_match(&matcher, "/a/b/c");
107 assert_exact_match(&matcher, "/a/b/d");
108 }
109
110 #[test]
111 fn test_ambiguous_match() {
112 let matcher = new_matcher(["/a/b/c", "/a/b/d"]);
113 assert_no_match(&matcher, "/a/b");
114 }
115
116 #[test]
117 fn test_inexact_match() {
118 let matcher = new_matcher(["/a/b/c"]);
119 assert_prefix_match(&matcher, "/a/b/c/d", "/a/b/c", "d");
120 }
121
122 fn new_matcher<const N: usize>(paths: [&str; N]) -> PathMatcher<String> {
123 let path_kv_pairs = paths.into_iter().map(|path| {
124 let key = path_to_vec(path);
125 let value = path.to_string();
126 (key, value)
127 });
128
129 PathMatcher::from_iter(path_kv_pairs)
130 }
131
132 fn assert_no_match(matcher: &PathMatcher<String>, path: &str) {
133 let paths = path_to_vec(path);
134 assert!(matcher.lookup(&paths).is_none());
135 }
136
137 fn assert_exact_match(matcher: &PathMatcher<String>, path: &str) {
138 assert_prefix_match(matcher, path, path, "");
139 }
140
141 fn assert_prefix_match(
142 matcher: &PathMatcher<String>,
143 path: &str,
144 expected_value: &str,
145 expected_suffix: &str,
146 ) {
147 let paths = path_to_vec(path);
148
149 let result = matcher.lookup(&paths);
150 let matched = result.unwrap_or_else(|| panic!("Expected {expected_value}"));
151 assert_eq!(matched.value, expected_value);
152
153 let actual_suffix = &paths[matched.matched_index..];
154 assert_eq!(actual_suffix, &path_to_vec(expected_suffix));
155 }
156
157 fn path_to_vec(path: &str) -> Vec<String> {
158 if path.is_empty() {
159 return vec![];
160 }
161 path.split('/').map(|p| p.to_string()).collect()
162 }
163}