use serde::{Deserialize, Serialize};
use std::fmt;
pub const MAX_PATH_DEPTH: usize = 64;
pub const MAX_SEGMENT_LEN: usize = 4096;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PathComponent {
Key(String),
Index(usize),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StatePath {
components: Vec<PathComponent>,
}
impl StatePath {
pub fn root() -> Self {
Self {
components: Vec::new(),
}
}
pub fn parse(s: &str) -> Result<Self, PathError> {
if s.is_empty() || s == "/" {
return Ok(Self::root());
}
let s = s.strip_prefix('/').ok_or(PathError::MustStartWithSlash)?;
let components = s
.split('/')
.enumerate()
.map(|(i, segment)| {
if segment.is_empty() {
Err(PathError::EmptySegment)
} else if segment.len() > MAX_SEGMENT_LEN {
Err(PathError::SegmentTooLong {
index: i,
len: segment.len(),
})
} else if let Ok(index) = segment.parse::<usize>() {
Ok(PathComponent::Index(index))
} else {
Ok(PathComponent::Key(segment.to_string()))
}
})
.collect::<Result<Vec<_>, _>>()?;
if components.len() > MAX_PATH_DEPTH {
return Err(PathError::TooDeep {
depth: components.len(),
});
}
Ok(Self { components })
}
pub fn components(&self) -> &[PathComponent] {
&self.components
}
pub fn is_root(&self) -> bool {
self.components.is_empty()
}
pub fn parent(&self) -> Option<Self> {
if self.components.is_empty() {
None
} else {
Some(Self {
components: self.components[..self.components.len() - 1].to_vec(),
})
}
}
pub fn last(&self) -> Option<&PathComponent> {
self.components.last()
}
pub fn push_key(&self, key: impl Into<String>) -> Self {
let mut components = self.components.clone();
components.push(PathComponent::Key(key.into()));
Self { components }
}
pub fn push_index(&self, index: usize) -> Self {
let mut components = self.components.clone();
components.push(PathComponent::Index(index));
Self { components }
}
pub fn depth(&self) -> usize {
self.components.len()
}
}
impl fmt::Display for StatePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.components.is_empty() {
write!(f, "/")
} else {
for component in &self.components {
match component {
PathComponent::Key(k) => write!(f, "/{}", k)?,
PathComponent::Index(i) => write!(f, "/{}", i)?,
}
}
Ok(())
}
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum PathError {
#[error("path must start with '/'")]
MustStartWithSlash,
#[error("path contains an empty segment")]
EmptySegment,
#[error("path is too deep: {depth} components (max {})", MAX_PATH_DEPTH)]
TooDeep { depth: usize },
#[error(
"path segment at index {index} is too long: {len} bytes (max {})",
MAX_SEGMENT_LEN
)]
SegmentTooLong { index: usize, len: usize },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_root() {
assert_eq!(StatePath::parse("/").unwrap(), StatePath::root());
assert_eq!(StatePath::parse("").unwrap(), StatePath::root());
}
#[test]
fn test_parse_simple_key() {
let path = StatePath::parse("/nodes").unwrap();
assert_eq!(
path.components(),
&[PathComponent::Key("nodes".to_string())]
);
}
#[test]
fn test_parse_nested() {
let path = StatePath::parse("/nodes/0/hostname").unwrap();
assert_eq!(
path.components(),
&[
PathComponent::Key("nodes".to_string()),
PathComponent::Index(0),
PathComponent::Key("hostname".to_string()),
]
);
}
#[test]
fn test_display_roundtrip() {
let path = StatePath::parse("/nodes/0/hostname").unwrap();
assert_eq!(path.to_string(), "/nodes/0/hostname");
}
#[test]
fn test_parent() {
let path = StatePath::parse("/nodes/0/hostname").unwrap();
let parent = path.parent().unwrap();
assert_eq!(parent.to_string(), "/nodes/0");
}
#[test]
fn test_root_has_no_parent() {
assert!(StatePath::root().parent().is_none());
}
#[test]
fn test_push() {
let path = StatePath::root()
.push_key("nodes")
.push_index(0)
.push_key("hostname");
assert_eq!(path.to_string(), "/nodes/0/hostname");
}
#[test]
fn test_error_no_leading_slash() {
assert!(StatePath::parse("nodes").is_err());
}
#[test]
fn test_error_empty_segment() {
assert!(StatePath::parse("/nodes//hostname").is_err());
}
#[test]
fn test_error_too_deep() {
let mut s = String::new();
for i in 0..(MAX_PATH_DEPTH + 5) {
s.push_str(&format!("/k{}", i));
}
match StatePath::parse(&s) {
Err(PathError::TooDeep { depth }) => {
assert_eq!(depth, MAX_PATH_DEPTH + 5);
}
other => panic!("expected TooDeep, got {:?}", other),
}
}
#[test]
fn test_error_segment_too_long() {
let big = "x".repeat(MAX_SEGMENT_LEN + 1);
let s = format!("/a/{}", big);
match StatePath::parse(&s) {
Err(PathError::SegmentTooLong { index, len }) => {
assert_eq!(index, 1);
assert_eq!(len, MAX_SEGMENT_LEN + 1);
}
other => panic!("expected SegmentTooLong, got {:?}", other),
}
}
#[test]
fn test_caps_boundary_accepts() {
let seg = "x".repeat(MAX_SEGMENT_LEN);
let s = format!("/{}", seg);
assert!(StatePath::parse(&s).is_ok());
let mut deep = String::new();
for i in 0..MAX_PATH_DEPTH {
deep.push_str(&format!("/k{}", i));
}
assert!(StatePath::parse(&deep).is_ok());
}
}