edfsm_kv_store/
path.rs

1use alloc::{string::String, string::ToString, vec::Vec};
2use core::{fmt::Display, ops::Div, slice::Iter, str::FromStr};
3use derive_more::{
4    derive::{Deref, IntoIterator},
5    From, TryInto,
6};
7use serde::{Deserialize, Serialize};
8use smol_str::SmolStr;
9
10/// The key to a KV store is a pathname, `Path`, and allows heirarchical grouping of values.
11/// A path can be constructed with an expression such as:
12///
13///  `Path::root().append("first_level").append(42),append("third_level")`
14///
15/// or imperatively using `path.push(item)`.
16#[derive(
17    PartialEq,
18    Eq,
19    PartialOrd,
20    Ord,
21    Clone,
22    Debug,
23    Default,
24    Serialize,
25    Deserialize,
26    Hash,
27    IntoIterator,
28    Deref,
29)]
30#[deref(forward)]
31pub struct Path(Vec<PathItem>);
32
33impl Path {
34    /// Another name for the empty path, also the default path.
35    pub fn root() -> Self {
36        Self::default()
37    }
38
39    /// Append an item to the path
40    pub fn append(mut self, item: impl Into<PathItem>) -> Self {
41        self.push(item.into());
42        self
43    }
44
45    /// Push a `PathItem` to the end of this path
46    pub fn push(&mut self, item: PathItem) {
47        self.0.push(item);
48    }
49
50    /// The length of this path.
51    pub fn len(&self) -> usize {
52        self.0.len()
53    }
54
55    /// This is the empty or root path.
56    pub fn is_empty(&self) -> bool {
57        self.0.is_empty()
58    }
59
60    pub fn iter(&self) -> Iter<'_, PathItem> {
61        self.0.iter()
62    }
63}
64
65/// Another name for the empty path, also the default path.
66pub fn root() -> Path {
67    Path::default()
68}
69
70impl<T> Div<T> for Path
71where
72    T: Into<PathItem>,
73{
74    type Output = Path;
75
76    fn div(self, item: T) -> Self::Output {
77        self.append(item)
78    }
79}
80
81impl Display for Path {
82    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
83        let mut buffer = String::new();
84        for item in self.iter() {
85            buffer.push('/');
86            match item {
87                PathItem::Number(n) => {
88                    url_escape::encode_component_to_string(n.to_string(), &mut buffer);
89                }
90                PathItem::Name(c) => {
91                    if let Some(x) = c.chars().next() {
92                        if x.is_ascii_digit() || x == '\'' {
93                            buffer.push('\'');
94                        }
95                    }
96                    url_escape::encode_component_to_string(c, &mut buffer);
97                }
98            }
99        }
100        f.write_str(&buffer)
101    }
102}
103
104#[derive(Debug, PartialEq)]
105pub enum ParseError {
106    NoRoot,
107    BadInt(core::num::ParseIntError),
108}
109
110impl FromStr for Path {
111    type Err = ParseError;
112
113    fn from_str(s: &str) -> Result<Self, Self::Err> {
114        if !s.starts_with('/') {
115            return Err(ParseError::NoRoot);
116        }
117        let mut path = Self::root();
118        let mut raw_path_items = s.split('/');
119        let _ = raw_path_items.next();
120        let mut decode_buffer = String::with_capacity(s.len());
121        for raw_path_item in raw_path_items {
122            let mut raw_path_item_iter = raw_path_item.chars();
123            let path_item = match raw_path_item_iter.next() {
124                Some(c) if c.is_ascii_digit() => {
125                    PathItem::Number(raw_path_item.parse().map_err(ParseError::BadInt)?)
126                }
127                Some('\'') => {
128                    url_escape::decode_to_string(raw_path_item_iter.as_str(), &mut decode_buffer);
129                    PathItem::Name(SmolStr::from(&decode_buffer))
130                }
131                _ => {
132                    url_escape::decode_to_string(raw_path_item, &mut decode_buffer);
133                    PathItem::Name(SmolStr::from(&decode_buffer))
134                }
135            };
136            path.push(path_item);
137            decode_buffer.clear();
138        }
139        Ok(path)
140    }
141}
142
143/// One element of a `Path` can be a number or a name.
144#[derive(
145    PartialEq, Eq, PartialOrd, Ord, Clone, Debug, From, Serialize, Deserialize, Hash, TryInto,
146)]
147#[serde(untagged)]
148pub enum PathItem {
149    Number(u64),
150    Name(SmolStr),
151}
152
153impl From<&'static str> for PathItem {
154    fn from(value: &'static str) -> Self {
155        SmolStr::new_static(value).into()
156    }
157}
158
159impl From<String> for PathItem {
160    fn from(value: String) -> Self {
161        SmolStr::new(value).into()
162    }
163}
164
165#[cfg(test)]
166mod test {
167    use crate::path::ParseError;
168
169    use super::{root, Path, PathItem};
170    use alloc::{format, string::ToString};
171    use smol_str::SmolStr;
172
173    #[test]
174    fn path_test() {
175        // bulding from various types (note: special smol_str support for &'static str engaged)
176        let version = "V1.6";
177        let path = root() / "CSMS" / 65 / format!("EVSE-{version}") / 2;
178
179        // indexing
180        let evse: u64 = path[3].clone().try_into().unwrap();
181        assert_eq!(evse, 2);
182
183        // indexing again
184        let x: Option<SmolStr> = path[2].clone().try_into().ok();
185        assert_eq!(x.unwrap(), "EVSE-V1.6");
186
187        // other slice operations
188        if let Some(&PathItem::Number(evse)) = path.last() {
189            assert_eq!(evse, 2);
190        } else {
191            panic!("test failed")
192        }
193
194        // pattern matching
195        match &*path {
196            [PathItem::Name(x), PathItem::Number(csms), ..] if x == "CSMS" => {
197                assert_eq!(*csms, 65)
198            }
199            _ => panic!("test failed"),
200        }
201
202        // iterating
203        let csms: u64 = path.iter().nth(1).unwrap().clone().try_into().unwrap();
204        assert_eq!(csms, 65);
205
206        // iterating
207        for item in path {
208            if let PathItem::Number(csms) = item {
209                assert_eq!(csms, 65);
210                break;
211            }
212        }
213    }
214
215    #[test]
216    fn path_serialisation_json() {
217        let p = root() / "CSMS" / 65 / "EVSE" / 2;
218        let s = serde_json::to_string(&p).unwrap();
219        assert_eq!(s, r#"["CSMS",65,"EVSE",2]"#);
220    }
221
222    #[test]
223    fn path_deserialisation_json() {
224        let s = r#"["CSMS",65,"EVSE",2]"#;
225        let p: Path = serde_json::from_str(s).unwrap();
226        assert_eq!(p, root() / "CSMS" / 65 / "EVSE" / 2);
227    }
228
229    #[test]
230    fn path_serialisation_qs() {
231        let p = root() / "CSMS" / 65 / "EVSE" / 2;
232        let s = serde_qs::to_string(&p).unwrap();
233        assert_eq!(s, "0=CSMS&1=65&2=EVSE&3=2");
234    }
235
236    #[test]
237    fn to_string_1() {
238        let p = root() / "CS" / 1;
239        assert_eq!(p.to_string(), "/CS/1");
240    }
241
242    #[test]
243    fn to_string_2() {
244        let p = root() / "CS/MS" / 65 / "EV?S&E" / 2;
245        assert_eq!(p.to_string(), "/CS%2FMS/65/EV%3FS%26E/2");
246    }
247
248    #[test]
249    fn to_string_3() {
250        let p = root() / "CS" / "2";
251        assert_eq!(p.to_string(), "/CS/'2");
252    }
253
254    #[test]
255    fn to_string_4() {
256        let p = root() / "'CS" / 2;
257        assert_eq!(p.to_string(), "/''CS/2");
258    }
259
260    #[test]
261    fn from_string_1() {
262        let p = root() / "CS" / 1;
263        assert_eq!("/CS/1".parse(), Ok(p));
264    }
265
266    #[test]
267    fn from_string_2() {
268        let p = root() / "CS/MS" / 65 / "EV?S&E" / 2;
269        assert_eq!("/CS%2FMS/65/EV%3FS%26E/2".parse(), Ok(p));
270    }
271
272    #[test]
273    fn from_string_3() {
274        let p = root() / "CS" / "2";
275        assert_eq!("/CS/'2".parse(), Ok(p));
276    }
277
278    #[test]
279    fn from_string_4() {
280        let p = root() / "'CS" / 2;
281        assert_eq!("/''CS/2".parse(), Ok(p));
282    }
283
284    #[test]
285    fn from_string_no_root_err() {
286        assert_eq!("CS".parse::<Path>(), Err(ParseError::NoRoot));
287    }
288
289    #[test]
290    fn from_string_bad_int_err() {
291        assert!(matches!(
292            "/1n".parse::<Path>(),
293            Err(ParseError::BadInt(core::num::ParseIntError { .. }))
294        ));
295    }
296}