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 { max: 32, actual: 33 })
530 ));
531 }
532
533 #[test]
534 fn parse_invalid_segment_fails() {
535 let result = CapabilityPath::parse("valid/INVALID/also-valid");
536 assert!(matches!(
537 result,
538 Err(CapabilityPathError::InvalidSegment { index: 1, .. })
539 ));
540 }
541
542 #[test]
543 fn parse_empty_segment_fails() {
544 let result = CapabilityPath::parse("valid//invalid");
545 assert!(matches!(
546 result,
547 Err(CapabilityPathError::InvalidSegment { index: 1, .. })
548 ));
549 }
550
551 #[test]
554 fn from_segments_empty_returns_error() {
555 let result = CapabilityPath::from_segments(vec![]);
556 assert!(matches!(result, Err(CapabilityPathError::Empty)));
557 }
558
559 #[test]
560 fn from_segments_single_segment_succeeds() {
561 let segments = vec![PathSegment::parse("chat").unwrap()];
562 let path = CapabilityPath::from_segments(segments).unwrap();
563 assert_eq!(path.depth(), 1);
564 assert_eq!(path.as_str(), "chat");
565 }
566
567 #[test]
568 fn from_segments_multiple_segments_succeeds() {
569 let segments = vec![
570 PathSegment::parse("assistant").unwrap(),
571 PathSegment::parse("chat").unwrap(),
572 ];
573 let path = CapabilityPath::from_segments(segments).unwrap();
574 assert_eq!(path.depth(), 2);
575 assert_eq!(path.as_str(), "assistant/chat");
576 }
577
578 #[test]
579 fn from_segments_max_segments_succeeds() {
580 let segments: Vec<PathSegment> = (0..32)
581 .map(|_| PathSegment::parse("a").unwrap())
582 .collect();
583 let path = CapabilityPath::from_segments(segments).unwrap();
584 assert_eq!(path.depth(), 32);
585 }
586
587 #[test]
588 fn from_segments_too_many_segments_returns_error() {
589 let segments: Vec<PathSegment> = (0..33)
590 .map(|_| PathSegment::parse("a").unwrap())
591 .collect();
592 let result = CapabilityPath::from_segments(segments);
593 assert!(matches!(
594 result,
595 Err(CapabilityPathError::TooManySegments {
596 max: 32,
597 actual: 33
598 })
599 ));
600 }
601
602 #[test]
603 fn from_segments_too_long_returns_error() {
604 let long_segment = "a".repeat(32);
607 let segments: Vec<PathSegment> = (0..8)
608 .map(|_| PathSegment::parse(&long_segment).unwrap())
609 .collect();
610 let result = CapabilityPath::from_segments(segments);
611 assert!(matches!(result, Err(CapabilityPathError::TooLong { .. })));
612 }
613
614 #[test]
617 fn try_from_strs_empty_returns_error() {
618 let result = CapabilityPath::try_from_strs(&[]);
619 assert!(matches!(result, Err(CapabilityPathError::Empty)));
620 }
621
622 #[test]
623 fn try_from_strs_valid_segments_succeeds() {
624 let path = CapabilityPath::try_from_strs(&["assistant", "chat"]).unwrap();
625 assert_eq!(path.as_str(), "assistant/chat");
626 }
627
628 #[test]
629 fn try_from_strs_invalid_segment_uppercase_returns_error() {
630 let result = CapabilityPath::try_from_strs(&["valid", "INVALID"]);
631 assert!(matches!(
632 result,
633 Err(CapabilityPathError::InvalidSegment { index: 1, .. })
634 ));
635 }
636
637 #[test]
638 fn try_from_strs_invalid_segment_empty_returns_error() {
639 let result = CapabilityPath::try_from_strs(&["valid", ""]);
640 assert!(matches!(
641 result,
642 Err(CapabilityPathError::InvalidSegment { index: 1, .. })
643 ));
644 }
645
646 #[test]
647 fn try_from_strs_first_invalid_returns_error() {
648 let result = CapabilityPath::try_from_strs(&["INVALID", "also-valid"]);
650 assert!(matches!(
651 result,
652 Err(CapabilityPathError::InvalidSegment { index: 0, .. })
653 ));
654 }
655}