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#[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 pub fn root() -> Self {
36 Self::default()
37 }
38
39 pub fn append(mut self, item: impl Into<PathItem>) -> Self {
41 self.push(item.into());
42 self
43 }
44
45 pub fn push(&mut self, item: PathItem) {
47 self.0.push(item);
48 }
49
50 pub fn len(&self) -> usize {
52 self.0.len()
53 }
54
55 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
65pub 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#[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 let version = "V1.6";
177 let path = root() / "CSMS" / 65 / format!("EVSE-{version}") / 2;
178
179 let evse: u64 = path[3].clone().try_into().unwrap();
181 assert_eq!(evse, 2);
182
183 let x: Option<SmolStr> = path[2].clone().try_into().ok();
185 assert_eq!(x.unwrap(), "EVSE-V1.6");
186
187 if let Some(&PathItem::Number(evse)) = path.last() {
189 assert_eq!(evse, 2);
190 } else {
191 panic!("test failed")
192 }
193
194 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 let csms: u64 = path.iter().nth(1).unwrap().clone().try_into().unwrap();
204 assert_eq!(csms, 65);
205
206 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}