canon_protocol/
dependency.rs1use crate::error::{ProtocolError, ProtocolResult};
2use std::path::PathBuf;
3
4#[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#[derive(Debug, Clone, PartialEq)]
15pub enum VersionOperator {
16 Caret,
18 Tilde,
20}
21
22impl Dependency {
23 pub fn parse(uri: &str) -> ProtocolResult<Self> {
26 let parts: Vec<&str> = uri.splitn(2, '@').collect();
28
29 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 let (version, version_operator) = if parts.len() > 1 {
43 let version_str = parts[1];
44
45 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 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 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 pub fn exists_locally(&self) -> bool {
89 self.local_path().join("canon.yml").exists()
90 }
91
92 pub fn is_cached(&self) -> bool {
94 self.remote_cache_path().join("canon.yml").exists()
95 }
96
97 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 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}