laser_wire/filter/
path.rs1use crate::error::InvalidError;
2use crate::limits::{MAX_FILTER_PATH_BYTES, MAX_FILTER_PATH_SEGMENTS};
3use serde::de::Error as _;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::fmt;
6
7#[derive(Clone, Debug, PartialEq, Eq, Hash)]
9pub enum PathSegment {
10 Key(String),
11 Index(u32),
12}
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
23pub struct FieldPath {
24 segments: Vec<PathSegment>,
25}
26
27impl FieldPath {
28 pub fn parse(text: &str) -> Result<Self, InvalidError> {
31 if text.is_empty() {
32 return Err(InvalidError::new("field path must not be empty"));
33 }
34 if text.len() > MAX_FILTER_PATH_BYTES {
35 return Err(InvalidError::new(format!(
36 "field path is {}B, exceeds cap {MAX_FILTER_PATH_BYTES}B",
37 text.len()
38 )));
39 }
40 let mut parser = PathParser {
41 bytes: text.as_bytes(),
42 position: 0,
43 text,
44 };
45 let segments = parser.segments()?;
46 if segments.len() > MAX_FILTER_PATH_SEGMENTS {
47 return Err(InvalidError::new(format!(
48 "field path `{text}` has {} segments, exceeds cap {MAX_FILTER_PATH_SEGMENTS}",
49 segments.len()
50 )));
51 }
52 Ok(Self { segments })
53 }
54
55 pub fn segments(&self) -> &[PathSegment] {
56 &self.segments
57 }
58}
59
60impl fmt::Display for FieldPath {
61 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 for (position, segment) in self.segments.iter().enumerate() {
63 match segment {
64 PathSegment::Key(key) => {
65 if position > 0 {
66 formatter.write_str(".")?;
67 }
68 for character in key.chars() {
69 if matches!(character, '.' | '[' | ']' | '\\') {
70 formatter.write_str("\\")?;
71 }
72 write!(formatter, "{character}")?;
73 }
74 }
75 PathSegment::Index(index) => write!(formatter, "[{index}]")?,
76 }
77 }
78 Ok(())
79 }
80}
81
82impl std::str::FromStr for FieldPath {
83 type Err = InvalidError;
84
85 fn from_str(text: &str) -> Result<Self, Self::Err> {
86 Self::parse(text)
87 }
88}
89
90impl Serialize for FieldPath {
91 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
92 serializer.collect_str(self)
93 }
94}
95
96impl<'de> Deserialize<'de> for FieldPath {
97 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
98 let text = String::deserialize(deserializer)?;
99 Self::parse(&text).map_err(D::Error::custom)
100 }
101}
102
103struct PathParser<'a> {
104 bytes: &'a [u8],
105 position: usize,
106 text: &'a str,
107}
108
109impl PathParser<'_> {
110 fn segments(&mut self) -> Result<Vec<PathSegment>, InvalidError> {
111 let mut segments = Vec::new();
112 if self.peek() == Some(b'[') {
113 segments.push(self.index()?);
114 } else {
115 segments.push(self.key()?);
116 }
117 while let Some(byte) = self.peek() {
118 match byte {
119 b'.' => {
120 self.position += 1;
121 segments.push(self.key()?);
122 }
123 b'[' => segments.push(self.index()?),
124 _ => return Err(self.invalid("expected `.` or `[` after a segment")),
125 }
126 }
127 Ok(segments)
128 }
129
130 fn key(&mut self) -> Result<PathSegment, InvalidError> {
131 let mut key = String::new();
132 while let Some(byte) = self.peek() {
133 match byte {
134 b'.' | b'[' => break,
135 b']' => return Err(self.invalid("unescaped `]` inside a key")),
136 b'\\' => {
137 self.position += 1;
138 match self.peek() {
139 Some(escaped @ (b'.' | b'[' | b']' | b'\\')) => {
140 key.push(char::from(escaped));
141 self.position += 1;
142 }
143 _ => return Err(self.invalid("`\\` must escape `.`, `[`, `]`, or `\\`")),
144 }
145 }
146 _ => {
147 let rest = &self.text[self.position..];
148 let character = rest
149 .chars()
150 .next()
151 .expect("a peeked byte starts a character");
152 key.push(character);
153 self.position += character.len_utf8();
154 }
155 }
156 }
157 if key.is_empty() {
158 return Err(self.invalid("empty key"));
159 }
160 Ok(PathSegment::Key(key))
161 }
162
163 fn index(&mut self) -> Result<PathSegment, InvalidError> {
164 self.position += 1;
165 let start = self.position;
166 while self.peek().is_some_and(|byte| byte.is_ascii_digit()) {
167 self.position += 1;
168 }
169 let digits = &self.text[start..self.position];
170 if self.peek() != Some(b']') {
171 return Err(self.invalid("an index must be digits closed by `]`"));
172 }
173 self.position += 1;
174 if digits.is_empty() || (digits.len() > 1 && digits.starts_with('0')) {
175 return Err(self.invalid("an index must be a canonical decimal number"));
176 }
177 digits
178 .parse::<u32>()
179 .map(PathSegment::Index)
180 .map_err(|_| self.invalid("an index must fit in 32 bits"))
181 }
182
183 fn peek(&self) -> Option<u8> {
184 self.bytes.get(self.position).copied()
185 }
186
187 fn invalid(&self, reason: &str) -> InvalidError {
188 InvalidError::new(format!(
189 "field path `{}` is invalid at byte {}: {reason}",
190 self.text, self.position
191 ))
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 fn round_trip(text: &str) -> Vec<PathSegment> {
200 let path = FieldPath::parse(text).expect("the path parses");
201 assert_eq!(path.to_string(), text, "the text form is canonical");
202 path.segments().to_vec()
203 }
204
205 #[test]
206 fn given_dotted_keys_when_parsed_then_should_split_on_dots() {
207 assert_eq!(
208 round_trip("after.orbit.kind"),
209 vec![
210 PathSegment::Key("after".to_owned()),
211 PathSegment::Key("orbit".to_owned()),
212 PathSegment::Key("kind".to_owned()),
213 ]
214 );
215 }
216
217 #[test]
218 fn given_brackets_when_parsed_then_should_select_indexes() {
219 assert_eq!(
220 round_trip("[0].ground_stations[12]"),
221 vec![
222 PathSegment::Index(0),
223 PathSegment::Key("ground_stations".to_owned()),
224 PathSegment::Index(12),
225 ]
226 );
227 }
228
229 #[test]
230 fn given_a_digit_key_when_parsed_then_should_stay_a_key() {
231 assert_eq!(
232 round_trip("codes.200"),
233 vec![
234 PathSegment::Key("codes".to_owned()),
235 PathSegment::Key("200".to_owned()),
236 ]
237 );
238 }
239
240 #[test]
241 fn given_escaped_characters_when_parsed_then_should_keep_them_in_the_key() {
242 assert_eq!(
243 round_trip(r"a\.b.c\[d\]\\"),
244 vec![
245 PathSegment::Key("a.b".to_owned()),
246 PathSegment::Key(r"c[d]\".to_owned()),
247 ]
248 );
249 }
250
251 #[test]
252 fn given_malformed_paths_when_parsed_then_should_be_rejected() {
253 for text in [
254 "",
255 "a..b",
256 ".a",
257 "a.",
258 "a[",
259 "a[]",
260 "a[01]",
261 "a[x]",
262 "a]",
263 r"a\b",
264 r"a\",
265 "a[1]b",
266 "a[4294967296]",
267 ] {
268 assert!(FieldPath::parse(text).is_err(), "`{text}` must be rejected");
269 }
270 }
271
272 #[test]
273 fn given_too_many_segments_when_parsed_then_should_be_rejected() {
274 let text = vec!["a"; MAX_FILTER_PATH_SEGMENTS + 1].join(".");
275 assert!(FieldPath::parse(&text).is_err());
276 let text = vec!["a"; MAX_FILTER_PATH_SEGMENTS].join(".");
277 assert!(FieldPath::parse(&text).is_ok());
278 }
279
280 #[test]
281 fn given_a_path_when_serialized_then_should_be_its_text_form() {
282 let path = FieldPath::parse("fields.mode").expect("parses");
283 let json = serde_json::to_string(&path).expect("serializes");
284 assert_eq!(json, r#""fields.mode""#);
285 let back: FieldPath = serde_json::from_str(&json).expect("deserializes");
286 assert_eq!(back, path);
287 assert!(serde_json::from_str::<FieldPath>(r#""a..b""#).is_err());
288 }
289}