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_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 pub fn local_path(&self) -> PathBuf {
79 self.local_path_with_registry("canon.canon-protocol.org")
80 }
81
82 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 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 pub fn is_installed(&self) -> bool {
99 let canon_file = self.local_path().join("canon.yml");
100 canon_file.exists()
101 }
102
103 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}