agentstategraph_core/
path.rs1use serde::{Deserialize, Serialize};
10use std::fmt;
11
12pub const MAX_PATH_DEPTH: usize = 64;
16
17pub const MAX_SEGMENT_LEN: usize = 4096;
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum PathComponent {
25 Key(String),
26 Index(usize),
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub struct StatePath {
32 components: Vec<PathComponent>,
33}
34
35impl StatePath {
36 pub fn root() -> Self {
38 Self {
39 components: Vec::new(),
40 }
41 }
42
43 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 pub fn components(&self) -> &[PathComponent] {
80 &self.components
81 }
82
83 pub fn is_root(&self) -> bool {
85 self.components.is_empty()
86 }
87
88 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 pub fn last(&self) -> Option<&PathComponent> {
101 self.components.last()
102 }
103
104 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 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 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#[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 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}