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");
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 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 pub fn is_installed(&self) -> bool {
87 let canon_file = self.local_path().join("canon.yml");
88 canon_file.exists()
89 }
90
91 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}