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 name: String,
9    pub version: Option<String>,
10}
11
12impl Dependency {
13    /// Parse a dependency URI like "canon-protocol.org/type@1.0.0"
14    pub fn parse(uri: &str) -> ProtocolResult<Self> {
15        // Split by @ to separate the path from version
16        let parts: Vec<&str> = uri.splitn(2, '@').collect();
17
18        // Parse the path part
19        let path_parts: Vec<&str> = parts[0].split('/').collect();
20        if path_parts.len() != 2 {
21            return Err(ProtocolError::InvalidUri(format!(
22                "Invalid dependency URI format: {}",
23                uri
24            )));
25        }
26
27        let publisher = path_parts[0].to_string();
28        let name = path_parts[1].to_string();
29
30        // Parse version if present
31        let version = if parts.len() > 1 {
32            Some(parts[1].to_string())
33        } else {
34            None
35        };
36
37        Ok(Self {
38            publisher,
39            name,
40            version,
41        })
42    }
43
44    /// Get the local storage path for this dependency
45    pub fn local_path(&self, registry_domain: &str) -> PathBuf {
46        let mut path = PathBuf::from(".canon");
47        path.push("specs"); // All specs under .canon/specs/
48        path.push(registry_domain); // Then organized by registry
49        path.push(&self.publisher);
50        path.push(&self.name);
51        if let Some(ref version) = self.version {
52            path.push(version);
53        }
54        path
55    }
56
57    /// Construct the registry URL for this dependency
58    pub fn registry_url(&self, registry_base: &str) -> String {
59        let version = self.version.as_deref().unwrap_or("latest");
60        format!(
61            "{}/specs/{}/{}/{}/",
62            registry_base.trim_end_matches('/'),
63            self.publisher,
64            self.name,
65            version
66        )
67    }
68
69    /// Check if this dependency is already installed
70    pub fn is_installed(&self, registry_domain: &str) -> bool {
71        self.local_path(registry_domain).exists()
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_parse_dependency() {
81        let dep = Dependency::parse("canon-protocol.org/type@1.0.0").unwrap();
82        assert_eq!(dep.publisher, "canon-protocol.org");
83        assert_eq!(dep.name, "type");
84        assert_eq!(dep.version, Some("1.0.0".to_string()));
85
86        let dep_no_version = Dependency::parse("example.com/api").unwrap();
87        assert_eq!(dep_no_version.publisher, "example.com");
88        assert_eq!(dep_no_version.name, "api");
89        assert_eq!(dep_no_version.version, None);
90    }
91
92    #[test]
93    fn test_local_path() {
94        let dep = Dependency {
95            publisher: "canon-protocol.org".to_string(),
96            name: "type".to_string(),
97            version: Some("1.0.0".to_string()),
98        };
99
100        let path = dep.local_path("spec.farm");
101        assert_eq!(
102            path,
103            PathBuf::from(".canon/specs/spec.farm/canon-protocol.org/type/1.0.0")
104        );
105    }
106
107    #[test]
108    fn test_registry_url() {
109        let dep = Dependency {
110            publisher: "canon-protocol.org".to_string(),
111            name: "type".to_string(),
112            version: Some("1.0.0".to_string()),
113        };
114
115        let url = dep.registry_url("https://spec.farm");
116        assert_eq!(
117            url,
118            "https://spec.farm/specs/canon-protocol.org/type/1.0.0/"
119        );
120
121        // Test with trailing slash
122        let url_with_slash = dep.registry_url("https://spec.farm/");
123        assert_eq!(
124            url_with_slash,
125            "https://spec.farm/specs/canon-protocol.org/type/1.0.0/"
126        );
127    }
128
129    #[test]
130    fn test_registry_url_without_version() {
131        let dep = Dependency {
132            publisher: "example.com".to_string(),
133            name: "api".to_string(),
134            version: None,
135        };
136
137        let url = dep.registry_url("https://registry.canon-protocol.org");
138        assert_eq!(
139            url,
140            "https://registry.canon-protocol.org/specs/example.com/api/latest/"
141        );
142    }
143}