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 storage path for this dependency
66    pub fn local_path(&self) -> PathBuf {
67        let mut path = PathBuf::from(".canon");
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    /// Construct the URL for fetching from canon.canon-protocol.org
77    pub fn canon_url(&self) -> String {
78        let version = self.version.as_deref().unwrap_or("latest");
79        format!(
80            "https://canon.canon-protocol.org/{}/{}/{}/canon.yml",
81            self.publisher, self.id, version
82        )
83    }
84
85    /// Check if this dependency is already installed
86    pub fn is_installed(&self) -> bool {
87        let canon_file = self.local_path().join("canon.yml");
88        canon_file.exists()
89    }
90
91    /// Format the dependency as a URI string
92    pub fn to_uri(&self) -> String {
93        match (&self.version, &self.version_operator) {
94            (Some(v), Some(VersionOperator::Caret)) => {
95                format!("{}/{}@^{}", self.publisher, self.id, v)
96            }
97            (Some(v), Some(VersionOperator::Tilde)) => {
98                format!("{}/{}@~{}", self.publisher, self.id, v)
99            }
100            (Some(v), None) => format!("{}/{}@{}", self.publisher, self.id, v),
101            (None, _) => format!("{}/{}", self.publisher, self.id),
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_parse_dependency() {
112        let dep = Dependency::parse("canon-protocol.org/type@1.0.0").unwrap();
113        assert_eq!(dep.publisher, "canon-protocol.org");
114        assert_eq!(dep.id, "type");
115        assert_eq!(dep.version, Some("1.0.0".to_string()));
116        assert_eq!(dep.version_operator, None);
117
118        let dep_no_version = Dependency::parse("example.com/api").unwrap();
119        assert_eq!(dep_no_version.publisher, "example.com");
120        assert_eq!(dep_no_version.id, "api");
121        assert_eq!(dep_no_version.version, None);
122        assert_eq!(dep_no_version.version_operator, None);
123    }
124
125    #[test]
126    fn test_parse_dependency_with_operators() {
127        let dep_caret = Dependency::parse("profiles.org/author@^1.0.0").unwrap();
128        assert_eq!(dep_caret.publisher, "profiles.org");
129        assert_eq!(dep_caret.id, "author");
130        assert_eq!(dep_caret.version, Some("1.0.0".to_string()));
131        assert_eq!(dep_caret.version_operator, Some(VersionOperator::Caret));
132
133        let dep_tilde = Dependency::parse("standards.org/metadata@~2.1.0").unwrap();
134        assert_eq!(dep_tilde.publisher, "standards.org");
135        assert_eq!(dep_tilde.id, "metadata");
136        assert_eq!(dep_tilde.version, Some("2.1.0".to_string()));
137        assert_eq!(dep_tilde.version_operator, Some(VersionOperator::Tilde));
138    }
139
140    #[test]
141    fn test_local_path() {
142        let dep = Dependency {
143            publisher: "canon-protocol.org".to_string(),
144            id: "type".to_string(),
145            version: Some("1.0.0".to_string()),
146            version_operator: None,
147        };
148
149        let path = dep.local_path();
150        assert_eq!(path, PathBuf::from(".canon/canon-protocol.org/type/1.0.0"));
151    }
152
153    #[test]
154    fn test_canon_url() {
155        let dep = Dependency {
156            publisher: "canon-protocol.org".to_string(),
157            id: "type".to_string(),
158            version: Some("1.0.0".to_string()),
159            version_operator: None,
160        };
161
162        let url = dep.canon_url();
163        assert_eq!(
164            url,
165            "https://canon.canon-protocol.org/canon-protocol.org/type/1.0.0/canon.yml"
166        );
167    }
168
169    #[test]
170    fn test_to_uri() {
171        let dep = Dependency {
172            publisher: "canon-protocol.org".to_string(),
173            id: "type".to_string(),
174            version: Some("1.0.0".to_string()),
175            version_operator: None,
176        };
177        assert_eq!(dep.to_uri(), "canon-protocol.org/type@1.0.0");
178
179        let dep_caret = Dependency {
180            publisher: "profiles.org".to_string(),
181            id: "author".to_string(),
182            version: Some("1.0.0".to_string()),
183            version_operator: Some(VersionOperator::Caret),
184        };
185        assert_eq!(dep_caret.to_uri(), "profiles.org/author@^1.0.0");
186    }
187}