modeldriveprotocol_client/
path_utils.rs1use once_cell::sync::Lazy;
2use regex::Regex;
3use serde_json::{Map, Value};
4
5static STATIC_SEGMENT_PATTERN: Lazy<Regex> = Lazy::new(|| {
6 Regex::new(r"^(?:[a-z0-9](?:[a-z0-9_-]*)?|\.[a-z0-9](?:[a-z0-9_-]*)?)$").unwrap()
7});
8static PARAM_SEGMENT_PATTERN: Lazy<Regex> =
9 Lazy::new(|| Regex::new(r"^:[a-z0-9](?:[a-z0-9_-]*)$").unwrap());
10static RESERVED_LEAF_NAMES: &[&str] = &["skill.md", "prompt.md", "SKILL.md", "PROMPT.md"];
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct PathPatternMatch {
14 pub params: Map<String, Value>,
15 pub specificity: Vec<i32>,
16}
17
18pub fn is_path_pattern(value: &str) -> bool {
19 validate_path(value, true)
20}
21
22pub fn is_concrete_path(value: &str) -> bool {
23 validate_path(value, false)
24}
25
26pub fn is_skill_path(value: &str) -> bool {
27 is_path_pattern(value)
28 && split_path(value)
29 .last()
30 .map(|segment| *segment == "skill.md" || *segment == "SKILL.md")
31 .unwrap_or(false)
32}
33
34pub fn is_prompt_path(value: &str) -> bool {
35 is_path_pattern(value)
36 && split_path(value)
37 .last()
38 .map(|segment| *segment == "prompt.md" || *segment == "PROMPT.md")
39 .unwrap_or(false)
40}
41
42pub fn match_path_pattern(pattern: &str, path: &str) -> Option<PathPatternMatch> {
43 if !is_path_pattern(pattern) || !is_concrete_path(path) {
44 return None;
45 }
46
47 let pattern_segments = split_path(pattern);
48 let path_segments = split_path(path);
49 if pattern_segments.len() != path_segments.len() {
50 return None;
51 }
52
53 let mut params = Map::new();
54 let mut specificity = Vec::with_capacity(pattern_segments.len());
55
56 for (pattern_segment, path_segment) in pattern_segments.iter().zip(path_segments.iter()) {
57 if let Some(param_name) = pattern_segment.strip_prefix(':') {
58 params.insert(param_name.to_string(), Value::String((*path_segment).to_string()));
59 specificity.push(0);
60 continue;
61 }
62
63 if pattern_segment != path_segment {
64 return None;
65 }
66
67 specificity.push(if is_reserved_leaf_name(pattern_segment) { 1 } else { 2 });
68 }
69
70 Some(PathPatternMatch { params, specificity })
71}
72
73pub fn compare_path_specificity(left: &[i32], right: &[i32]) -> i32 {
74 let max_length = left.len().max(right.len());
75 for index in 0..max_length {
76 let left_value = left.get(index).copied().unwrap_or(-1);
77 let right_value = right.get(index).copied().unwrap_or(-1);
78 if left_value != right_value {
79 return left_value - right_value;
80 }
81 }
82 0
83}
84
85fn validate_path(value: &str, allow_params: bool) -> bool {
86 if !value.starts_with('/') || value.contains('?') || value.contains('#') {
87 return false;
88 }
89
90 let segments = split_path(value);
91 if segments.is_empty() {
92 return false;
93 }
94
95 for (index, segment) in segments.iter().enumerate() {
96 if segment.is_empty() {
97 return false;
98 }
99
100 let is_last = index == segments.len() - 1;
101 if is_reserved_leaf_name(segment) {
102 if !is_last {
103 return false;
104 }
105 continue;
106 }
107
108 if allow_params && PARAM_SEGMENT_PATTERN.is_match(segment) {
109 continue;
110 }
111
112 if !STATIC_SEGMENT_PATTERN.is_match(segment) {
113 return false;
114 }
115 }
116
117 true
118}
119
120fn split_path(value: &str) -> Vec<&str> {
121 value.split('/').skip(1).collect()
122}
123
124fn is_reserved_leaf_name(value: &str) -> bool {
125 RESERVED_LEAF_NAMES.contains(&value)
126}