1use std::cmp::Ordering;
19use std::fmt;
20use std::str::FromStr;
21
22use crate::constants::{MAX_CAPABILITY_PATH_LENGTH, MAX_PATH_SEGMENTS};
23use crate::error::CapabilityPathError;
24use crate::path_segment::PathSegment;
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
53pub struct CapabilityPath {
54 segments: Vec<PathSegment>,
55 normalized: String,
57}
58
59impl CapabilityPath {
60 pub fn from_segments(segments: Vec<PathSegment>) -> Result<Self, CapabilityPathError> {
94 if segments.is_empty() {
95 return Err(CapabilityPathError::Empty);
96 }
97
98 if segments.len() > MAX_PATH_SEGMENTS {
99 return Err(CapabilityPathError::TooManySegments {
100 max: MAX_PATH_SEGMENTS,
101 actual: segments.len(),
102 });
103 }
104
105 let normalized = segments
106 .iter()
107 .map(PathSegment::as_str)
108 .collect::<Vec<_>>()
109 .join("/");
110
111 if normalized.len() > MAX_CAPABILITY_PATH_LENGTH {
112 return Err(CapabilityPathError::TooLong {
113 max: MAX_CAPABILITY_PATH_LENGTH,
114 actual: normalized.len(),
115 });
116 }
117
118 Ok(Self {
119 segments,
120 normalized,
121 })
122 }
123
124 pub fn try_from_strs(segments: &[&str]) -> Result<Self, CapabilityPathError> {
158 if segments.is_empty() {
159 return Err(CapabilityPathError::Empty);
160 }
161
162 let parsed_segments: Result<Vec<PathSegment>, CapabilityPathError> = segments
163 .iter()
164 .enumerate()
165 .map(|(i, s)| {
166 PathSegment::parse(s).map_err(|e| CapabilityPathError::InvalidSegment {
167 segment: (*s).to_string(),
168 index: i,
169 reason: e,
170 })
171 })
172 .collect();
173
174 Self::from_segments(parsed_segments?)
175 }
176
177 pub fn parse(input: &str) -> Result<Self, CapabilityPathError> {
187 if input.is_empty() {
188 return Err(CapabilityPathError::Empty);
189 }
190
191 if input.len() > MAX_CAPABILITY_PATH_LENGTH {
192 return Err(CapabilityPathError::TooLong {
193 max: MAX_CAPABILITY_PATH_LENGTH,
194 actual: input.len(),
195 });
196 }
197
198 let segment_strs: Vec<&str> = input.split('/').collect();
199
200 if segment_strs.len() > MAX_PATH_SEGMENTS {
201 return Err(CapabilityPathError::TooManySegments {
202 max: MAX_PATH_SEGMENTS,
203 actual: segment_strs.len(),
204 });
205 }
206
207 let mut segments = Vec::with_capacity(segment_strs.len());
208 for (i, seg_str) in segment_strs.iter().enumerate() {
209 let segment =
210 PathSegment::parse(seg_str).map_err(|e| CapabilityPathError::InvalidSegment {
211 segment: (*seg_str).to_string(),
212 index: i,
213 reason: e,
214 })?;
215 segments.push(segment);
216 }
217
218 let normalized = segments
219 .iter()
220 .map(PathSegment::as_str)
221 .collect::<Vec<_>>()
222 .join("/");
223
224 Ok(Self {
225 segments,
226 normalized,
227 })
228 }
229
230 #[must_use]
232 pub fn segments(&self) -> &[PathSegment] {
233 &self.segments
234 }
235
236 #[must_use]
238 pub fn depth(&self) -> usize {
239 self.segments.len()
240 }
241
242 #[must_use]
244 pub fn starts_with(&self, prefix: &CapabilityPath) -> bool {
245 if prefix.segments.len() > self.segments.len() {
246 return false;
247 }
248 self.segments
249 .iter()
250 .zip(prefix.segments.iter())
251 .all(|(a, b)| a == b)
252 }
253
254 #[must_use]
256 pub fn as_str(&self) -> &str {
257 &self.normalized
258 }
259
260 #[must_use]
275 pub fn parent(&self) -> Option<Self> {
276 if self.segments.len() <= 1 {
277 return None;
278 }
279 let parent_segments: Vec<PathSegment> = self.segments[..self.segments.len() - 1].to_vec();
280 let normalized = parent_segments
281 .iter()
282 .map(PathSegment::as_str)
283 .collect::<Vec<_>>()
284 .join("/");
285 Some(Self {
286 segments: parent_segments,
287 normalized,
288 })
289 }
290
291 pub fn join(&self, segment: &PathSegment) -> Result<Self, CapabilityPathError> {
308 let new_len = self.segments.len() + 1;
309 if new_len > MAX_PATH_SEGMENTS {
310 return Err(CapabilityPathError::TooManySegments {
311 max: MAX_PATH_SEGMENTS,
312 actual: new_len,
313 });
314 }
315
316 let mut segments = self.segments.clone();
317 segments.push(segment.clone());
318
319 let normalized = segments
320 .iter()
321 .map(PathSegment::as_str)
322 .collect::<Vec<_>>()
323 .join("/");
324
325 if normalized.len() > MAX_CAPABILITY_PATH_LENGTH {
326 return Err(CapabilityPathError::TooLong {
327 max: MAX_CAPABILITY_PATH_LENGTH,
328 actual: normalized.len(),
329 });
330 }
331
332 Ok(Self {
333 segments,
334 normalized,
335 })
336 }
337
338 pub fn try_join(&self, s: &str) -> Result<Self, CapabilityPathError> {
354 let segment = PathSegment::parse(s).map_err(|e| CapabilityPathError::InvalidSegment {
355 segment: s.to_string(),
356 index: self.segments.len(),
357 reason: e,
358 })?;
359 self.join(&segment)
360 }
361
362 #[must_use]
378 pub fn last(&self) -> &PathSegment {
379 self.segments.last().expect("path has at least one segment")
381 }
382
383 pub fn iter(&self) -> std::slice::Iter<'_, PathSegment> {
395 self.segments.iter()
396 }
397}
398
399impl<'a> IntoIterator for &'a CapabilityPath {
400 type Item = &'a PathSegment;
401 type IntoIter = std::slice::Iter<'a, PathSegment>;
402
403 fn into_iter(self) -> Self::IntoIter {
404 self.segments.iter()
405 }
406}
407
408impl fmt::Display for CapabilityPath {
409 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410 write!(f, "{}", self.normalized)
411 }
412}
413
414impl FromStr for CapabilityPath {
415 type Err = CapabilityPathError;
416
417 fn from_str(s: &str) -> Result<Self, Self::Err> {
418 Self::parse(s)
419 }
420}
421
422impl AsRef<str> for CapabilityPath {
423 fn as_ref(&self) -> &str {
424 &self.normalized
425 }
426}
427
428impl TryFrom<&str> for CapabilityPath {
429 type Error = CapabilityPathError;
430
431 fn try_from(s: &str) -> Result<Self, Self::Error> {
432 Self::parse(s)
433 }
434}
435
436impl PartialOrd for CapabilityPath {
437 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
438 Some(self.cmp(other))
439 }
440}
441
442impl Ord for CapabilityPath {
443 fn cmp(&self, other: &Self) -> Ordering {
444 self.normalized.cmp(&other.normalized)
445 }
446}
447
448#[cfg(feature = "serde")]
449impl serde::Serialize for CapabilityPath {
450 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
451 where
452 S: serde::Serializer,
453 {
454 serializer.serialize_str(&self.normalized)
455 }
456}
457
458#[cfg(feature = "serde")]
459impl<'de> serde::Deserialize<'de> for CapabilityPath {
460 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
461 where
462 D: serde::Deserializer<'de>,
463 {
464 let s = String::deserialize(deserializer)?;
465 Self::parse(&s).map_err(serde::de::Error::custom)
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn parse_single_segment() {
475 let path = CapabilityPath::parse("chat").unwrap();
476 assert_eq!(path.depth(), 1);
477 assert_eq!(path.as_str(), "chat");
478 }
479
480 #[test]
481 fn parse_multiple_segments() {
482 let path = CapabilityPath::parse("workflow/approval/invoice").unwrap();
483 assert_eq!(path.depth(), 3);
484 assert_eq!(path.segments()[0].as_str(), "workflow");
485 assert_eq!(path.segments()[1].as_str(), "approval");
486 assert_eq!(path.segments()[2].as_str(), "invoice");
487 }
488
489 #[test]
490 fn starts_with_prefix() {
491 let path = CapabilityPath::parse("workflow/approval/invoice").unwrap();
492 let prefix = CapabilityPath::parse("workflow/approval").unwrap();
493 assert!(path.starts_with(&prefix));
494 }
495
496 #[test]
497 fn does_not_start_with_longer_prefix() {
498 let path = CapabilityPath::parse("workflow/approval").unwrap();
499 let prefix = CapabilityPath::parse("workflow/approval/invoice").unwrap();
500 assert!(!path.starts_with(&prefix));
501 }
502
503 #[test]
504 fn does_not_start_with_different_prefix() {
505 let path = CapabilityPath::parse("workflow/approval").unwrap();
506 let prefix = CapabilityPath::parse("assistant/chat").unwrap();
507 assert!(!path.starts_with(&prefix));
508 }
509
510 #[test]
511 fn parse_empty_fails() {
512 let result = CapabilityPath::parse("");
513 assert!(matches!(result, Err(CapabilityPathError::Empty)));
514 }
515
516 #[test]
517 fn parse_too_long_fails() {
518 let long = "a".repeat(257);
519 let result = CapabilityPath::parse(&long);
520 assert!(matches!(result, Err(CapabilityPathError::TooLong { .. })));
521 }
522
523 #[test]
524 fn parse_too_many_segments_fails() {
525 let segments = (0..33).map(|_| "a").collect::<Vec<_>>().join("/");
526 let result = CapabilityPath::parse(&segments);
527 assert!(matches!(
528 result,
529 Err(CapabilityPathError::TooManySegments {
530 max: 32,
531 actual: 33
532 })
533 ));
534 }
535
536 #[test]
537 fn parse_invalid_segment_fails() {
538 let result = CapabilityPath::parse("valid/INVALID/also-valid");
539 assert!(matches!(
540 result,
541 Err(CapabilityPathError::InvalidSegment { index: 1, .. })
542 ));
543 }
544
545 #[test]
546 fn parse_empty_segment_fails() {
547 let result = CapabilityPath::parse("valid//invalid");
548 assert!(matches!(
549 result,
550 Err(CapabilityPathError::InvalidSegment { index: 1, .. })
551 ));
552 }
553
554 #[test]
557 fn from_segments_empty_returns_error() {
558 let result = CapabilityPath::from_segments(vec![]);
559 assert!(matches!(result, Err(CapabilityPathError::Empty)));
560 }
561
562 #[test]
563 fn from_segments_single_segment_succeeds() {
564 let segments = vec![PathSegment::parse("chat").unwrap()];
565 let path = CapabilityPath::from_segments(segments).unwrap();
566 assert_eq!(path.depth(), 1);
567 assert_eq!(path.as_str(), "chat");
568 }
569
570 #[test]
571 fn from_segments_multiple_segments_succeeds() {
572 let segments = vec![
573 PathSegment::parse("assistant").unwrap(),
574 PathSegment::parse("chat").unwrap(),
575 ];
576 let path = CapabilityPath::from_segments(segments).unwrap();
577 assert_eq!(path.depth(), 2);
578 assert_eq!(path.as_str(), "assistant/chat");
579 }
580
581 #[test]
582 fn from_segments_max_segments_succeeds() {
583 let segments: Vec<PathSegment> =
584 (0..32).map(|_| PathSegment::parse("a").unwrap()).collect();
585 let path = CapabilityPath::from_segments(segments).unwrap();
586 assert_eq!(path.depth(), 32);
587 }
588
589 #[test]
590 fn from_segments_too_many_segments_returns_error() {
591 let segments: Vec<PathSegment> =
592 (0..33).map(|_| PathSegment::parse("a").unwrap()).collect();
593 let result = CapabilityPath::from_segments(segments);
594 assert!(matches!(
595 result,
596 Err(CapabilityPathError::TooManySegments {
597 max: 32,
598 actual: 33
599 })
600 ));
601 }
602
603 #[test]
604 fn from_segments_too_long_returns_error() {
605 let long_segment = "a".repeat(32);
608 let segments: Vec<PathSegment> = (0..8)
609 .map(|_| PathSegment::parse(&long_segment).unwrap())
610 .collect();
611 let result = CapabilityPath::from_segments(segments);
612 assert!(matches!(result, Err(CapabilityPathError::TooLong { .. })));
613 }
614
615 #[test]
618 fn try_from_strs_empty_returns_error() {
619 let result = CapabilityPath::try_from_strs(&[]);
620 assert!(matches!(result, Err(CapabilityPathError::Empty)));
621 }
622
623 #[test]
624 fn try_from_strs_valid_segments_succeeds() {
625 let path = CapabilityPath::try_from_strs(&["assistant", "chat"]).unwrap();
626 assert_eq!(path.as_str(), "assistant/chat");
627 }
628
629 #[test]
630 fn try_from_strs_invalid_segment_uppercase_returns_error() {
631 let result = CapabilityPath::try_from_strs(&["valid", "INVALID"]);
632 assert!(matches!(
633 result,
634 Err(CapabilityPathError::InvalidSegment { index: 1, .. })
635 ));
636 }
637
638 #[test]
639 fn try_from_strs_invalid_segment_empty_returns_error() {
640 let result = CapabilityPath::try_from_strs(&["valid", ""]);
641 assert!(matches!(
642 result,
643 Err(CapabilityPathError::InvalidSegment { index: 1, .. })
644 ));
645 }
646
647 #[test]
648 fn try_from_strs_first_invalid_returns_error() {
649 let result = CapabilityPath::try_from_strs(&["INVALID", "also-valid"]);
651 assert!(matches!(
652 result,
653 Err(CapabilityPathError::InvalidSegment { index: 0, .. })
654 ));
655 }
656}