Skip to main content

eure_document/
path.rs

1use core::fmt::Display;
2
3use crate::{prelude_internal::*, value::PartialObjectKey};
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Plural)]
6pub struct EurePath(pub Vec<PathSegment>);
7
8impl EurePath {
9    /// Create an empty path representing the document root
10    pub fn root() -> Self {
11        EurePath(Vec::new())
12    }
13
14    /// Check if this is the root path
15    pub fn is_root(&self) -> bool {
16        self.0.is_empty()
17    }
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub enum PathSegment {
22    /// Regular identifiers like id, description
23    Ident(Identifier),
24    /// Extension namespace fields starting with $ like $eure, $variant
25    Extension(Identifier),
26    /// Arbitrary value used as key
27    Value(ObjectKey),
28    /// Partial-map key that cannot be losslessly represented as ObjectKey
29    PartialValue(PartialObjectKey),
30    /// Tuple element index (0-255)
31    TupleIndex(u8),
32    /// Array element access
33    ArrayIndex(ArrayIndexKind),
34    /// A hole key in a PartialMap: `!` or `!label`
35    HoleKey(Option<Identifier>),
36}
37
38/// Kind of array index used in a path segment.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum ArrayIndexKind {
41    /// `[]` — always pushes a new element onto the array
42    Push,
43    /// `[^]` — references the element most recently pushed into this array
44    /// within the current block scope. Errors if there is no such push in scope.
45    Current,
46    /// `[n]` — references a specific index, creating the element if it does not exist
47    Specific(usize),
48}
49
50impl PathSegment {
51    pub fn from_partial_object_key(key: PartialObjectKey) -> Self {
52        match key {
53            PartialObjectKey::Number(n) => Self::Value(ObjectKey::Number(n)),
54            PartialObjectKey::String(s) => Self::Value(ObjectKey::String(s)),
55            PartialObjectKey::Hole(label) => Self::HoleKey(label),
56            PartialObjectKey::Tuple(items) => Self::PartialValue(PartialObjectKey::Tuple(items)),
57        }
58    }
59}
60
61impl Display for EurePath {
62    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
63        if self.0.is_empty() {
64            return write!(f, "(root)");
65        }
66        for (i, segment) in self.0.iter().enumerate() {
67            let is_first = i == 0;
68            match segment {
69                PathSegment::Ident(id) => {
70                    if !is_first {
71                        write!(f, ".")?;
72                    }
73                    write!(f, "{}", id)?;
74                }
75                PathSegment::Extension(id) => {
76                    if !is_first {
77                        write!(f, ".")?;
78                    }
79                    write!(f, "${}", id)?;
80                }
81                PathSegment::Value(key) => {
82                    if !is_first {
83                        write!(f, ".")?;
84                    }
85                    write!(f, "{}", key)?;
86                }
87                PathSegment::PartialValue(key) => {
88                    if !is_first {
89                        write!(f, ".")?;
90                    }
91                    write!(f, "{}", key)?;
92                }
93                PathSegment::TupleIndex(index) => {
94                    if !is_first {
95                        write!(f, ".")?;
96                    }
97                    write!(f, "#{}", index)?;
98                }
99                PathSegment::ArrayIndex(ArrayIndexKind::Specific(index)) => {
100                    write!(f, "[{}]", index)?
101                }
102                PathSegment::ArrayIndex(ArrayIndexKind::Push) => write!(f, "[]")?,
103                PathSegment::ArrayIndex(ArrayIndexKind::Current) => write!(f, "[^]")?,
104                PathSegment::HoleKey(None) => {
105                    if !is_first {
106                        write!(f, ".")?;
107                    }
108                    write!(f, "!")?;
109                }
110                PathSegment::HoleKey(Some(label)) => {
111                    if !is_first {
112                        write!(f, ".")?;
113                    }
114                    write!(f, "!{}", label)?;
115                }
116            }
117        }
118        Ok(())
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use alloc::format;
125
126    use super::*;
127    use crate::value::{ObjectKey, PartialObjectKey, Tuple};
128
129    #[test]
130    fn test_display_empty_path() {
131        let path = EurePath::root();
132        assert_eq!(format!("{}", path), "(root)");
133    }
134
135    #[test]
136    fn test_display_single_ident() {
137        let path = EurePath(vec![PathSegment::Ident(Identifier::new_unchecked("name"))]);
138        assert_eq!(format!("{}", path), "name");
139    }
140
141    #[test]
142    fn test_display_nested_idents() {
143        let path = EurePath(vec![
144            PathSegment::Ident(Identifier::new_unchecked("a")),
145            PathSegment::Ident(Identifier::new_unchecked("b")),
146            PathSegment::Ident(Identifier::new_unchecked("c")),
147        ]);
148        assert_eq!(format!("{}", path), "a.b.c");
149    }
150
151    #[test]
152    fn test_display_extension() {
153        let path = EurePath(vec![PathSegment::Extension(Identifier::new_unchecked(
154            "variant",
155        ))]);
156        assert_eq!(format!("{}", path), "$variant");
157    }
158
159    #[test]
160    fn test_display_array_index() {
161        let path = EurePath(vec![
162            PathSegment::Ident(Identifier::new_unchecked("items")),
163            PathSegment::ArrayIndex(ArrayIndexKind::Specific(0)),
164        ]);
165        assert_eq!(format!("{}", path), "items[0]");
166    }
167
168    #[test]
169    fn test_display_array_index_push() {
170        let path = EurePath(vec![
171            PathSegment::Ident(Identifier::new_unchecked("items")),
172            PathSegment::ArrayIndex(ArrayIndexKind::Push),
173        ]);
174        assert_eq!(format!("{}", path), "items[]");
175    }
176
177    #[test]
178    fn test_display_array_index_current() {
179        let path = EurePath(vec![
180            PathSegment::Ident(Identifier::new_unchecked("items")),
181            PathSegment::ArrayIndex(ArrayIndexKind::Current),
182        ]);
183        assert_eq!(format!("{}", path), "items[^]");
184    }
185
186    #[test]
187    fn test_display_tuple_index() {
188        let path = EurePath(vec![
189            PathSegment::Ident(Identifier::new_unchecked("point")),
190            PathSegment::TupleIndex(1),
191        ]);
192        assert_eq!(format!("{}", path), "point.#1");
193    }
194
195    #[test]
196    fn test_display_string_key() {
197        let path = EurePath(vec![PathSegment::Value(ObjectKey::String(
198            "hello".to_string(),
199        ))]);
200        assert_eq!(format!("{}", path), "\"hello\"");
201    }
202
203    #[test]
204    fn test_display_string_key_with_spaces() {
205        let path = EurePath(vec![PathSegment::Value(ObjectKey::String(
206            "hello world".to_string(),
207        ))]);
208        assert_eq!(format!("{}", path), "\"hello world\"");
209    }
210
211    #[test]
212    fn test_display_string_key_with_quotes() {
213        let path = EurePath(vec![PathSegment::Value(ObjectKey::String(
214            "say \"hi\"".to_string(),
215        ))]);
216        assert_eq!(format!("{}", path), "\"say \\\"hi\\\"\"");
217    }
218
219    #[test]
220    fn test_display_number_key() {
221        let path = EurePath(vec![PathSegment::Value(ObjectKey::Number(42.into()))]);
222        assert_eq!(format!("{}", path), "42");
223    }
224
225    #[test]
226    fn test_display_bool_key() {
227        // Boolean identifiers in key position become string keys
228        let path = EurePath(vec![PathSegment::Value(ObjectKey::String("true".into()))]);
229        assert_eq!(format!("{}", path), "\"true\"");
230    }
231
232    #[test]
233    fn test_display_complex_path() {
234        let path = EurePath(vec![
235            PathSegment::Ident(Identifier::new_unchecked("config")),
236            PathSegment::Extension(Identifier::new_unchecked("eure")),
237            PathSegment::Ident(Identifier::new_unchecked("items")),
238            PathSegment::ArrayIndex(ArrayIndexKind::Specific(0)),
239            PathSegment::Value(ObjectKey::String("key with space".to_string())),
240        ]);
241        assert_eq!(
242            format!("{}", path),
243            "config.$eure.items[0].\"key with space\""
244        );
245    }
246
247    #[test]
248    fn test_display_partial_tuple_key() {
249        let path = EurePath(vec![PathSegment::PartialValue(PartialObjectKey::Tuple(
250            Tuple(vec![
251                PartialObjectKey::Number(1.into()),
252                PartialObjectKey::Hole(None),
253            ]),
254        ))]);
255        assert_eq!(format!("{}", path), "(1, !)");
256    }
257}