use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
use crate::constants::{MAX_CAPABILITY_PATH_LENGTH, MAX_PATH_SEGMENTS};
use crate::error::CapabilityPathError;
use crate::path_segment::PathSegment;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CapabilityPath {
segments: Vec<PathSegment>,
normalized: String,
}
impl CapabilityPath {
pub fn from_segments(segments: Vec<PathSegment>) -> Result<Self, CapabilityPathError> {
if segments.is_empty() {
return Err(CapabilityPathError::Empty);
}
if segments.len() > MAX_PATH_SEGMENTS {
return Err(CapabilityPathError::TooManySegments {
max: MAX_PATH_SEGMENTS,
actual: segments.len(),
});
}
let normalized = segments
.iter()
.map(PathSegment::as_str)
.collect::<Vec<_>>()
.join("/");
if normalized.len() > MAX_CAPABILITY_PATH_LENGTH {
return Err(CapabilityPathError::TooLong {
max: MAX_CAPABILITY_PATH_LENGTH,
actual: normalized.len(),
});
}
Ok(Self {
segments,
normalized,
})
}
pub fn try_from_strs(segments: &[&str]) -> Result<Self, CapabilityPathError> {
if segments.is_empty() {
return Err(CapabilityPathError::Empty);
}
let parsed_segments: Result<Vec<PathSegment>, CapabilityPathError> = segments
.iter()
.enumerate()
.map(|(i, s)| {
PathSegment::parse(s).map_err(|e| CapabilityPathError::InvalidSegment {
segment: (*s).to_string(),
index: i,
reason: e,
})
})
.collect();
Self::from_segments(parsed_segments?)
}
pub fn parse(input: &str) -> Result<Self, CapabilityPathError> {
if input.is_empty() {
return Err(CapabilityPathError::Empty);
}
if input.len() > MAX_CAPABILITY_PATH_LENGTH {
return Err(CapabilityPathError::TooLong {
max: MAX_CAPABILITY_PATH_LENGTH,
actual: input.len(),
});
}
let segment_strs: Vec<&str> = input.split('/').collect();
if segment_strs.len() > MAX_PATH_SEGMENTS {
return Err(CapabilityPathError::TooManySegments {
max: MAX_PATH_SEGMENTS,
actual: segment_strs.len(),
});
}
let mut segments = Vec::with_capacity(segment_strs.len());
for (i, seg_str) in segment_strs.iter().enumerate() {
let segment =
PathSegment::parse(seg_str).map_err(|e| CapabilityPathError::InvalidSegment {
segment: (*seg_str).to_string(),
index: i,
reason: e,
})?;
segments.push(segment);
}
let normalized = segments
.iter()
.map(PathSegment::as_str)
.collect::<Vec<_>>()
.join("/");
Ok(Self {
segments,
normalized,
})
}
#[must_use]
pub fn segments(&self) -> &[PathSegment] {
&self.segments
}
#[must_use]
pub fn depth(&self) -> usize {
self.segments.len()
}
#[must_use]
pub fn starts_with(&self, prefix: &CapabilityPath) -> bool {
if prefix.segments.len() > self.segments.len() {
return false;
}
self.segments
.iter()
.zip(prefix.segments.iter())
.all(|(a, b)| a == b)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.normalized
}
#[must_use]
pub fn parent(&self) -> Option<Self> {
if self.segments.len() <= 1 {
return None;
}
let parent_segments: Vec<PathSegment> = self.segments[..self.segments.len() - 1].to_vec();
let normalized = parent_segments
.iter()
.map(PathSegment::as_str)
.collect::<Vec<_>>()
.join("/");
Some(Self {
segments: parent_segments,
normalized,
})
}
pub fn join(&self, segment: &PathSegment) -> Result<Self, CapabilityPathError> {
let new_len = self.segments.len() + 1;
if new_len > MAX_PATH_SEGMENTS {
return Err(CapabilityPathError::TooManySegments {
max: MAX_PATH_SEGMENTS,
actual: new_len,
});
}
let mut segments = self.segments.clone();
segments.push(segment.clone());
let normalized = segments
.iter()
.map(PathSegment::as_str)
.collect::<Vec<_>>()
.join("/");
if normalized.len() > MAX_CAPABILITY_PATH_LENGTH {
return Err(CapabilityPathError::TooLong {
max: MAX_CAPABILITY_PATH_LENGTH,
actual: normalized.len(),
});
}
Ok(Self {
segments,
normalized,
})
}
pub fn try_join(&self, s: &str) -> Result<Self, CapabilityPathError> {
let segment = PathSegment::parse(s).map_err(|e| CapabilityPathError::InvalidSegment {
segment: s.to_string(),
index: self.segments.len(),
reason: e,
})?;
self.join(&segment)
}
#[must_use]
pub fn last(&self) -> &PathSegment {
self.segments.last().expect("path has at least one segment")
}
pub fn iter(&self) -> std::slice::Iter<'_, PathSegment> {
self.segments.iter()
}
}
impl<'a> IntoIterator for &'a CapabilityPath {
type Item = &'a PathSegment;
type IntoIter = std::slice::Iter<'a, PathSegment>;
fn into_iter(self) -> Self::IntoIter {
self.segments.iter()
}
}
impl fmt::Display for CapabilityPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.normalized)
}
}
impl FromStr for CapabilityPath {
type Err = CapabilityPathError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl AsRef<str> for CapabilityPath {
fn as_ref(&self) -> &str {
&self.normalized
}
}
impl TryFrom<&str> for CapabilityPath {
type Error = CapabilityPathError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::parse(s)
}
}
impl PartialOrd for CapabilityPath {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CapabilityPath {
fn cmp(&self, other: &Self) -> Ordering {
self.normalized.cmp(&other.normalized)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for CapabilityPath {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.normalized)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for CapabilityPath {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_single_segment() {
let path = CapabilityPath::parse("chat").unwrap();
assert_eq!(path.depth(), 1);
assert_eq!(path.as_str(), "chat");
}
#[test]
fn parse_multiple_segments() {
let path = CapabilityPath::parse("workflow/approval/invoice").unwrap();
assert_eq!(path.depth(), 3);
assert_eq!(path.segments()[0].as_str(), "workflow");
assert_eq!(path.segments()[1].as_str(), "approval");
assert_eq!(path.segments()[2].as_str(), "invoice");
}
#[test]
fn starts_with_prefix() {
let path = CapabilityPath::parse("workflow/approval/invoice").unwrap();
let prefix = CapabilityPath::parse("workflow/approval").unwrap();
assert!(path.starts_with(&prefix));
}
#[test]
fn does_not_start_with_longer_prefix() {
let path = CapabilityPath::parse("workflow/approval").unwrap();
let prefix = CapabilityPath::parse("workflow/approval/invoice").unwrap();
assert!(!path.starts_with(&prefix));
}
#[test]
fn does_not_start_with_different_prefix() {
let path = CapabilityPath::parse("workflow/approval").unwrap();
let prefix = CapabilityPath::parse("assistant/chat").unwrap();
assert!(!path.starts_with(&prefix));
}
#[test]
fn parse_empty_fails() {
let result = CapabilityPath::parse("");
assert!(matches!(result, Err(CapabilityPathError::Empty)));
}
#[test]
fn parse_too_long_fails() {
let long = "a".repeat(257);
let result = CapabilityPath::parse(&long);
assert!(matches!(result, Err(CapabilityPathError::TooLong { .. })));
}
#[test]
fn parse_too_many_segments_fails() {
let segments = (0..33).map(|_| "a").collect::<Vec<_>>().join("/");
let result = CapabilityPath::parse(&segments);
assert!(matches!(
result,
Err(CapabilityPathError::TooManySegments { max: 32, actual: 33 })
));
}
#[test]
fn parse_invalid_segment_fails() {
let result = CapabilityPath::parse("valid/INVALID/also-valid");
assert!(matches!(
result,
Err(CapabilityPathError::InvalidSegment { index: 1, .. })
));
}
#[test]
fn parse_empty_segment_fails() {
let result = CapabilityPath::parse("valid//invalid");
assert!(matches!(
result,
Err(CapabilityPathError::InvalidSegment { index: 1, .. })
));
}
#[test]
fn from_segments_empty_returns_error() {
let result = CapabilityPath::from_segments(vec![]);
assert!(matches!(result, Err(CapabilityPathError::Empty)));
}
#[test]
fn from_segments_single_segment_succeeds() {
let segments = vec![PathSegment::parse("chat").unwrap()];
let path = CapabilityPath::from_segments(segments).unwrap();
assert_eq!(path.depth(), 1);
assert_eq!(path.as_str(), "chat");
}
#[test]
fn from_segments_multiple_segments_succeeds() {
let segments = vec![
PathSegment::parse("assistant").unwrap(),
PathSegment::parse("chat").unwrap(),
];
let path = CapabilityPath::from_segments(segments).unwrap();
assert_eq!(path.depth(), 2);
assert_eq!(path.as_str(), "assistant/chat");
}
#[test]
fn from_segments_max_segments_succeeds() {
let segments: Vec<PathSegment> = (0..32)
.map(|_| PathSegment::parse("a").unwrap())
.collect();
let path = CapabilityPath::from_segments(segments).unwrap();
assert_eq!(path.depth(), 32);
}
#[test]
fn from_segments_too_many_segments_returns_error() {
let segments: Vec<PathSegment> = (0..33)
.map(|_| PathSegment::parse("a").unwrap())
.collect();
let result = CapabilityPath::from_segments(segments);
assert!(matches!(
result,
Err(CapabilityPathError::TooManySegments {
max: 32,
actual: 33
})
));
}
#[test]
fn from_segments_too_long_returns_error() {
let long_segment = "a".repeat(32);
let segments: Vec<PathSegment> = (0..8)
.map(|_| PathSegment::parse(&long_segment).unwrap())
.collect();
let result = CapabilityPath::from_segments(segments);
assert!(matches!(result, Err(CapabilityPathError::TooLong { .. })));
}
#[test]
fn try_from_strs_empty_returns_error() {
let result = CapabilityPath::try_from_strs(&[]);
assert!(matches!(result, Err(CapabilityPathError::Empty)));
}
#[test]
fn try_from_strs_valid_segments_succeeds() {
let path = CapabilityPath::try_from_strs(&["assistant", "chat"]).unwrap();
assert_eq!(path.as_str(), "assistant/chat");
}
#[test]
fn try_from_strs_invalid_segment_uppercase_returns_error() {
let result = CapabilityPath::try_from_strs(&["valid", "INVALID"]);
assert!(matches!(
result,
Err(CapabilityPathError::InvalidSegment { index: 1, .. })
));
}
#[test]
fn try_from_strs_invalid_segment_empty_returns_error() {
let result = CapabilityPath::try_from_strs(&["valid", ""]);
assert!(matches!(
result,
Err(CapabilityPathError::InvalidSegment { index: 1, .. })
));
}
#[test]
fn try_from_strs_first_invalid_returns_error() {
let result = CapabilityPath::try_from_strs(&["INVALID", "also-valid"]);
assert!(matches!(
result,
Err(CapabilityPathError::InvalidSegment { index: 0, .. })
));
}
}