Skip to main content

agentstategraph_core/
path.rs

1//! Path addressing for navigating state trees.
2//!
3//! Paths use a JSON-path-like syntax:
4//!   /                       → root node
5//!   /nodes                  → "nodes" key in root map
6//!   /nodes/0                → first element of "nodes" list
7//!   /nodes/0/hostname       → "hostname" key in first node object
8
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12/// Maximum number of path components accepted by `StatePath::parse`.
13/// Prevents pathological deeply-nested user input from blowing the stack
14/// or causing quadratic work in tree walkers.
15pub const MAX_PATH_DEPTH: usize = 64;
16
17/// Maximum length (in bytes) of a single path segment accepted by
18/// `StatePath::parse`. Keys longer than this are almost always attacks
19/// or bugs — real keys are short identifiers.
20pub const MAX_SEGMENT_LEN: usize = 4096;
21
22/// A component of a path — either a map key or a list/set index.
23#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum PathComponent {
25    Key(String),
26    Index(usize),
27}
28
29/// A path into a state tree.
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub struct StatePath {
32    components: Vec<PathComponent>,
33}
34
35impl StatePath {
36    /// The root path (empty components).
37    pub fn root() -> Self {
38        Self {
39            components: Vec::new(),
40        }
41    }
42
43    /// Parse a path from a string like "/nodes/0/hostname".
44    pub fn parse(s: &str) -> Result<Self, PathError> {
45        if s.is_empty() || s == "/" {
46            return Ok(Self::root());
47        }
48
49        let s = s.strip_prefix('/').ok_or(PathError::MustStartWithSlash)?;
50        let components = s
51            .split('/')
52            .enumerate()
53            .map(|(i, segment)| {
54                if segment.is_empty() {
55                    Err(PathError::EmptySegment)
56                } else if segment.len() > MAX_SEGMENT_LEN {
57                    Err(PathError::SegmentTooLong {
58                        index: i,
59                        len: segment.len(),
60                    })
61                } else if let Ok(index) = segment.parse::<usize>() {
62                    Ok(PathComponent::Index(index))
63                } else {
64                    Ok(PathComponent::Key(segment.to_string()))
65                }
66            })
67            .collect::<Result<Vec<_>, _>>()?;
68
69        if components.len() > MAX_PATH_DEPTH {
70            return Err(PathError::TooDeep {
71                depth: components.len(),
72            });
73        }
74
75        Ok(Self { components })
76    }
77
78    /// Return the path components.
79    pub fn components(&self) -> &[PathComponent] {
80        &self.components
81    }
82
83    /// Whether this is the root path.
84    pub fn is_root(&self) -> bool {
85        self.components.is_empty()
86    }
87
88    /// Return the parent path (or None if this is root).
89    pub fn parent(&self) -> Option<Self> {
90        if self.components.is_empty() {
91            None
92        } else {
93            Some(Self {
94                components: self.components[..self.components.len() - 1].to_vec(),
95            })
96        }
97    }
98
99    /// Return the last component (or None if this is root).
100    pub fn last(&self) -> Option<&PathComponent> {
101        self.components.last()
102    }
103
104    /// Append a key component.
105    pub fn push_key(&self, key: impl Into<String>) -> Self {
106        let mut components = self.components.clone();
107        components.push(PathComponent::Key(key.into()));
108        Self { components }
109    }
110
111    /// Append an index component.
112    pub fn push_index(&self, index: usize) -> Self {
113        let mut components = self.components.clone();
114        components.push(PathComponent::Index(index));
115        Self { components }
116    }
117
118    /// Number of components in this path.
119    pub fn depth(&self) -> usize {
120        self.components.len()
121    }
122}
123
124impl fmt::Display for StatePath {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        if self.components.is_empty() {
127            write!(f, "/")
128        } else {
129            for component in &self.components {
130                match component {
131                    PathComponent::Key(k) => write!(f, "/{}", k)?,
132                    PathComponent::Index(i) => write!(f, "/{}", i)?,
133                }
134            }
135            Ok(())
136        }
137    }
138}
139
140/// Errors that can occur when parsing a path.
141#[derive(Debug, Clone, PartialEq, thiserror::Error)]
142pub enum PathError {
143    #[error("path must start with '/'")]
144    MustStartWithSlash,
145    #[error("path contains an empty segment")]
146    EmptySegment,
147    #[error("path is too deep: {depth} components (max {})", MAX_PATH_DEPTH)]
148    TooDeep { depth: usize },
149    #[error(
150        "path segment at index {index} is too long: {len} bytes (max {})",
151        MAX_SEGMENT_LEN
152    )]
153    SegmentTooLong { index: usize, len: usize },
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_parse_root() {
162        assert_eq!(StatePath::parse("/").unwrap(), StatePath::root());
163        assert_eq!(StatePath::parse("").unwrap(), StatePath::root());
164    }
165
166    #[test]
167    fn test_parse_simple_key() {
168        let path = StatePath::parse("/nodes").unwrap();
169        assert_eq!(
170            path.components(),
171            &[PathComponent::Key("nodes".to_string())]
172        );
173    }
174
175    #[test]
176    fn test_parse_nested() {
177        let path = StatePath::parse("/nodes/0/hostname").unwrap();
178        assert_eq!(
179            path.components(),
180            &[
181                PathComponent::Key("nodes".to_string()),
182                PathComponent::Index(0),
183                PathComponent::Key("hostname".to_string()),
184            ]
185        );
186    }
187
188    #[test]
189    fn test_display_roundtrip() {
190        let path = StatePath::parse("/nodes/0/hostname").unwrap();
191        assert_eq!(path.to_string(), "/nodes/0/hostname");
192    }
193
194    #[test]
195    fn test_parent() {
196        let path = StatePath::parse("/nodes/0/hostname").unwrap();
197        let parent = path.parent().unwrap();
198        assert_eq!(parent.to_string(), "/nodes/0");
199    }
200
201    #[test]
202    fn test_root_has_no_parent() {
203        assert!(StatePath::root().parent().is_none());
204    }
205
206    #[test]
207    fn test_push() {
208        let path = StatePath::root()
209            .push_key("nodes")
210            .push_index(0)
211            .push_key("hostname");
212        assert_eq!(path.to_string(), "/nodes/0/hostname");
213    }
214
215    #[test]
216    fn test_error_no_leading_slash() {
217        assert!(StatePath::parse("nodes").is_err());
218    }
219
220    #[test]
221    fn test_error_empty_segment() {
222        assert!(StatePath::parse("/nodes//hostname").is_err());
223    }
224
225    #[test]
226    fn test_error_too_deep() {
227        let mut s = String::new();
228        for i in 0..(MAX_PATH_DEPTH + 5) {
229            s.push_str(&format!("/k{}", i));
230        }
231        match StatePath::parse(&s) {
232            Err(PathError::TooDeep { depth }) => {
233                assert_eq!(depth, MAX_PATH_DEPTH + 5);
234            }
235            other => panic!("expected TooDeep, got {:?}", other),
236        }
237    }
238
239    #[test]
240    fn test_error_segment_too_long() {
241        let big = "x".repeat(MAX_SEGMENT_LEN + 1);
242        let s = format!("/a/{}", big);
243        match StatePath::parse(&s) {
244            Err(PathError::SegmentTooLong { index, len }) => {
245                assert_eq!(index, 1);
246                assert_eq!(len, MAX_SEGMENT_LEN + 1);
247            }
248            other => panic!("expected SegmentTooLong, got {:?}", other),
249        }
250    }
251
252    #[test]
253    fn test_caps_boundary_accepts() {
254        // Exactly at the cap should be accepted.
255        let seg = "x".repeat(MAX_SEGMENT_LEN);
256        let s = format!("/{}", seg);
257        assert!(StatePath::parse(&s).is_ok());
258
259        let mut deep = String::new();
260        for i in 0..MAX_PATH_DEPTH {
261            deep.push_str(&format!("/k{}", i));
262        }
263        assert!(StatePath::parse(&deep).is_ok());
264    }
265}