canon_protocol/
dependency.rs

1use crate::error::{ProtocolError, ProtocolResult};
2use std::path::PathBuf;
3
4/// Represents a parsed dependency URI
5#[derive(Debug, Clone)]
6pub struct Dependency {
7    pub publisher: String,
8    pub id: String,
9    pub version: Option<String>,
10    pub version_operator: Option<VersionOperator>,
11}
12
13/// Version operators for flexible versioning in schemas
14#[derive(Debug, Clone, PartialEq)]
15pub enum VersionOperator {
16    /// Caret (^) - Compatible changes
17    Caret,
18    /// Tilde (~) - Patch-level changes  
19    Tilde,
20}
21
22impl Dependency {
23    /// Parse a dependency URI like "canon-protocol.org/type@1.0.0"
24    /// or with version operators like "canon-protocol.org/type@^1.0.0"
25    pub fn parse(uri: &str) -> ProtocolResult<Self> {
26        // Split by @ to separate the path from version
27        let parts: Vec<&str> = uri.splitn(2, '@').collect();
28
29        // Parse the path part
30        let path_parts: Vec<&str> = parts[0].split('/').collect();
31        if path_parts.len() != 2 {
32            return Err(ProtocolError::InvalidUri(format!(
33                "Invalid dependency URI format: {}. Expected format: publisher/id[@version]",
34                uri
35            )));
36        }
37
38        let publisher = path_parts[0].to_string();
39        let id = path_parts[1].to_string();
40
41        // Parse version if present
42        let (version, version_operator) = if parts.len() > 1 {
43            let version_str = parts[1];
44
45            // Check for version operators
46            if let Some(stripped) = version_str.strip_prefix('^') {
47                (Some(stripped.to_string()), Some(VersionOperator::Caret))
48            } else if let Some(stripped) = version_str.strip_prefix('~') {
49                (Some(stripped.to_string()), Some(VersionOperator::Tilde))
50            } else {
51                (Some(version_str.to_string()), None)
52            }
53        } else {
54            (None, None)
55        };
56
57        Ok(Self {
58            publisher,
59            id,
60            version,
61            version_operator,
62        })
63    }
64
65    /// Get the local path for this dependency
66    pub fn local_path(&self) -> PathBuf {
67        let mut path = PathBuf::from(".canon/localhost");
68        path.push(&self.publisher);
69        path.push(&self.id);
70        if let Some(ref version) = self.version {
71            path.push(version);
72        }
73        path
74    }
75
76    /// Get the remote cache path for this dependency
77    pub fn remote_cache_path(&self) -> PathBuf {
78        let mut path = PathBuf::from(".canon/canon.canon-protocol.org");
79        path.push(&self.publisher);
80        path.push(&self.id);
81        if let Some(ref version) = self.version {
82            path.push(version);
83        }
84        path
85    }
86
87    /// Check if this dependency exists locally
88    pub fn exists_locally(&self) -> bool {
89        self.local_path().join("canon.yml").exists()
90    }
91
92    /// Check if this dependency is cached from remote
93    pub fn is_cached(&self) -> bool {
94        self.remote_cache_path().join("canon.yml").exists()
95    }
96
97    /// Construct the URL for fetching from canon.canon-protocol.org
98    pub fn canon_url(&self) -> String {
99        let version = self.version.as_deref().unwrap_or("latest");
100        format!(
101            "https://canon.canon-protocol.org/{}/{}/{}/canon.yml",
102            self.publisher, self.id, version
103        )
104    }
105
106    /// Format the dependency as a URI string
107    pub fn to_uri(&self) -> String {
108        match (&self.version, &self.version_operator) {
109            (Some(v), Some(VersionOperator::Caret)) => {
110                format!("{}/{}@^{}", self.publisher, self.id, v)
111            }
112            (Some(v), Some(VersionOperator::Tilde)) => {
113                format!("{}/{}@~{}", self.publisher, self.id, v)
114            }
115            (Some(v), None) => format!("{}/{}@{}", self.publisher, self.id, v),
116            (None, _) => format!("{}/{}", self.publisher, self.id),
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_parse_dependency() {
127        let dep = Dependency::parse("canon-protocol.org/type@1.0.0").unwrap();
128        assert_eq!(dep.publisher, "canon-protocol.org");
129        assert_eq!(dep.id, "type");
130        assert_eq!(dep.version, Some("1.0.0".to_string()));
131        assert_eq!(dep.version_operator, None);
132
133        let dep_no_version = Dependency::parse("example.com/api").unwrap();
134        assert_eq!(dep_no_version.publisher, "example.com");
135        assert_eq!(dep_no_version.id, "api");
136        assert_eq!(dep_no_version.version, None);
137        assert_eq!(dep_no_version.version_operator, None);
138    }
139
140    #[test]
141    fn test_parse_dependency_with_operators() {
142        let dep_caret = Dependency::parse("profiles.org/author@^1.0.0").unwrap();
143        assert_eq!(dep_caret.publisher, "profiles.org");
144        assert_eq!(dep_caret.id, "author");
145        assert_eq!(dep_caret.version, Some("1.0.0".to_string()));
146        assert_eq!(dep_caret.version_operator, Some(VersionOperator::Caret));
147
148        let dep_tilde = Dependency::parse("standards.org/metadata@~2.1.0").unwrap();
149        assert_eq!(dep_tilde.publisher, "standards.org");
150        assert_eq!(dep_tilde.id, "metadata");
151        assert_eq!(dep_tilde.version, Some("2.1.0".to_string()));
152        assert_eq!(dep_tilde.version_operator, Some(VersionOperator::Tilde));
153    }
154
155    #[test]
156    fn test_local_path() {
157        let dep = Dependency {
158            publisher: "canon-protocol.org".to_string(),
159            id: "type".to_string(),
160            version: Some("1.0.0".to_string()),
161            version_operator: None,
162        };
163
164        let path = dep.local_path();
165        assert_eq!(
166            path,
167            PathBuf::from(".canon/localhost/canon-protocol.org/type/1.0.0")
168        );
169    }
170
171    #[test]
172    fn test_remote_cache_path() {
173        let dep = Dependency {
174            publisher: "canon-protocol.org".to_string(),
175            id: "type".to_string(),
176            version: Some("1.0.0".to_string()),
177            version_operator: None,
178        };
179
180        let path = dep.remote_cache_path();
181        assert_eq!(
182            path,
183            PathBuf::from(".canon/canon.canon-protocol.org/canon-protocol.org/type/1.0.0")
184        );
185    }
186
187    #[test]
188    fn test_canon_url() {
189        let dep = Dependency {
190            publisher: "canon-protocol.org".to_string(),
191            id: "type".to_string(),
192            version: Some("1.0.0".to_string()),
193            version_operator: None,
194        };
195
196        let url = dep.canon_url();
197        assert_eq!(
198            url,
199            "https://canon.canon-protocol.org/canon-protocol.org/type/1.0.0/canon.yml"
200        );
201    }
202
203    #[test]
204    fn test_to_uri() {
205        let dep = Dependency {
206            publisher: "canon-protocol.org".to_string(),
207            id: "type".to_string(),
208            version: Some("1.0.0".to_string()),
209            version_operator: None,
210        };
211        assert_eq!(dep.to_uri(), "canon-protocol.org/type@1.0.0");
212
213        let dep_caret = Dependency {
214            publisher: "profiles.org".to_string(),
215            id: "author".to_string(),
216            version: Some("1.0.0".to_string()),
217            version_operator: Some(VersionOperator::Caret),
218        };
219        assert_eq!(dep_caret.to_uri(), "profiles.org/author@^1.0.0");
220    }
221}