Skip to main content

modeldriveprotocol_client/
path_utils.rs

1use 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}