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