ephemeris_documents/
path.rs1use std::fmt;
4use std::iter::Peekable;
5use std::str::CharIndices;
6
7use crate::error::{OwnershipError, PathSyntaxError};
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum Segment {
12 Key(String),
14 Index(usize),
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct KeyPath(Vec<Segment>);
24
25impl KeyPath {
26 #[must_use]
28 pub const fn root() -> Self {
29 Self(Vec::new())
30 }
31
32 #[must_use]
34 pub const fn is_root(&self) -> bool {
35 self.0.is_empty()
36 }
37
38 #[must_use]
40 pub fn segments(&self) -> &[Segment] {
41 &self.0
42 }
43
44 #[must_use]
46 pub fn under(mut self, segment: Segment) -> Self {
47 self.0.insert(0, segment);
48 self
49 }
50}
51
52impl FromIterator<Segment> for KeyPath {
53 fn from_iter<I: IntoIterator<Item = Segment>>(iter: I) -> Self {
54 Self(iter.into_iter().collect())
55 }
56}
57
58impl From<&[String]> for KeyPath {
59 fn from(keys: &[String]) -> Self {
60 keys.iter().map(|key| Segment::Key(key.clone())).collect()
61 }
62}
63
64impl fmt::Display for Segment {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 Self::Index(index) => write!(f, "[{index}]"),
68 Self::Key(key) => KeyText(key).fmt(f),
69 }
70 }
71}
72
73struct KeyText<'a>(&'a str);
76
77impl KeyText<'_> {
78 const fn is_bare_char(c: char) -> bool {
79 c.is_ascii_alphanumeric() || c == '_' || c == '-'
80 }
81
82 const fn is_escaped_char(c: char) -> bool {
83 matches!(c, '\\' | '"')
84 }
85
86 fn is_bare(&self) -> bool {
87 !self.0.is_empty() && self.0.chars().all(Self::is_bare_char)
88 }
89}
90
91impl fmt::Display for KeyText<'_> {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 if self.is_bare() {
94 return f.write_str(self.0);
95 }
96 f.write_str("\"")?;
97 for c in self.0.chars() {
98 if Self::is_escaped_char(c) {
99 f.write_str("\\")?;
100 }
101 write!(f, "{c}")?;
102 }
103 f.write_str("\"")
104 }
105}
106
107pub(crate) struct PathTokens<'a> {
109 chars: Peekable<CharIndices<'a>>,
110 end: usize,
111}
112
113impl<'a> PathTokens<'a> {
114 pub(crate) fn parse(text: &'a str) -> Result<Vec<String>, OwnershipError> {
121 Self::new(text).keys()
122 }
123
124 fn new(text: &'a str) -> Self {
125 Self {
126 chars: text.char_indices().peekable(),
127 end: text.len(),
128 }
129 }
130
131 fn offset(&mut self) -> usize {
132 self.chars.peek().map_or(self.end, |&(offset, _)| offset)
133 }
134
135 fn error(&mut self, kind: PathSyntaxError) -> OwnershipError {
136 OwnershipError::PathSyntax {
137 offset: self.offset(),
138 kind,
139 }
140 }
141
142 fn eat(&mut self, expected: char) -> bool {
143 self.chars.next_if(|&(_, c)| c == expected).is_some()
144 }
145
146 fn keys(mut self) -> Result<Vec<String>, OwnershipError> {
147 if self.chars.peek().is_none() {
148 return Err(OwnershipError::PathEmpty);
149 }
150 let mut keys = vec![self.key()?];
151 while self.eat('.') {
152 keys.push(self.key()?);
153 }
154 match self.chars.peek() {
155 None => Ok(keys),
156 Some(_) => Err(self.error(PathSyntaxError::UnexpectedCharacter)),
157 }
158 }
159
160 fn key(&mut self) -> Result<String, OwnershipError> {
161 if self.chars.peek().is_some_and(|&(_, c)| c == '"') {
162 self.quoted()
163 } else {
164 self.bare()
165 }
166 }
167
168 fn bare(&mut self) -> Result<String, OwnershipError> {
169 let mut key = String::new();
170 while let Some((_, c)) = self.chars.next_if(|&(_, c)| KeyText::is_bare_char(c)) {
171 key.push(c);
172 }
173 if key.is_empty() {
174 let kind = match self.chars.peek() {
175 None | Some(&(_, '.')) => PathSyntaxError::EmptyKey,
176 Some(_) => PathSyntaxError::UnexpectedCharacter,
177 };
178 return Err(self.error(kind));
179 }
180 Ok(key)
181 }
182
183 fn quoted(&mut self) -> Result<String, OwnershipError> {
184 let start = self.offset();
185 self.chars.next();
186 let mut key = String::new();
187 loop {
188 let unterminated = OwnershipError::PathSyntax {
189 offset: start,
190 kind: PathSyntaxError::UnterminatedQuote,
191 };
192 match self.chars.next().ok_or_else(|| unterminated.clone())? {
193 (_, '"') => return Ok(key),
194 (escape, '\\') => match self.chars.next().ok_or(unterminated)? {
195 (_, c) if KeyText::is_escaped_char(c) => key.push(c),
196 _ => {
197 return Err(OwnershipError::PathSyntax {
198 offset: escape,
199 kind: PathSyntaxError::InvalidEscape,
200 });
201 }
202 },
203 (_, c) => key.push(c),
204 }
205 }
206 }
207}
208
209impl fmt::Display for KeyPath {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 if self.is_root() {
212 return f.write_str(".");
213 }
214 for (position, segment) in self.0.iter().enumerate() {
215 if position > 0 && matches!(segment, Segment::Key(_)) {
216 f.write_str(".")?;
217 }
218 write!(f, "{segment}")?;
219 }
220 Ok(())
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::{KeyPath, Segment};
227
228 fn key(name: &str) -> Segment {
229 Segment::Key(name.to_owned())
230 }
231
232 fn path(segments: impl IntoIterator<Item = Segment>) -> KeyPath {
233 segments.into_iter().collect()
234 }
235
236 #[test]
237 fn root_prints_as_a_dot() {
238 assert!(KeyPath::root().is_root());
239 assert_eq!(KeyPath::root().to_string(), ".");
240 assert_eq!(KeyPath::root().segments(), []);
241 }
242
243 #[test]
244 fn keys_and_indexes_are_joined() {
245 let nested = path([key("a"), key("b"), Segment::Index(2), key("c")]);
246 assert_eq!(nested.to_string(), "a.b[2].c");
247 let lists = path([key("list"), Segment::Index(0), Segment::Index(1)]);
248 assert_eq!(lists.to_string(), "list[0][1]");
249 assert_eq!(path([Segment::Index(3), key("a")]).to_string(), "[3].a");
250 }
251
252 #[test]
253 fn unusual_keys_are_quoted() {
254 assert_eq!(path([key("a.b"), key("c")]).to_string(), "\"a.b\".c");
255 assert_eq!(path([key("")]).to_string(), "\"\"");
256 assert_eq!(path([key("x y")]).to_string(), "\"x y\"");
257 assert_eq!(path([key("a\"b\\c")]).to_string(), "\"a\\\"b\\\\c\"");
258 assert_eq!(path([key("a_b-C9")]).to_string(), "a_b-C9");
259 }
260
261 #[test]
262 fn under_puts_the_segment_in_front() {
263 let built = KeyPath::root()
264 .under(key("c"))
265 .under(Segment::Index(1))
266 .under(key("b"));
267 assert_eq!(built.to_string(), "b[1].c");
268 assert!(!built.is_root());
269 assert_eq!(built.segments().len(), 3);
270 }
271
272 #[test]
273 fn keys_convert_from_a_slice() {
274 let keys = ["a".to_owned(), "b".to_owned()];
275 assert_eq!(KeyPath::from(&keys[..]), path([key("a"), key("b")]));
276 assert!(KeyPath::from(&[][..]).is_root());
277 }
278}