actr_cli/core/components/
dependency_resolver.rs

1use anyhow::Result;
2use async_trait::async_trait;
3
4use super::{
5    ConflictReport, ConflictType, DependencyGraph, DependencyResolver, DependencySpec,
6    ResolvedDependency,
7};
8
9pub struct DefaultDependencyResolver;
10
11impl DefaultDependencyResolver {
12    pub fn new() -> Self {
13        Self
14    }
15
16    fn parse_actr_uri(&self, spec: &str) -> Result<DependencySpec> {
17        let without_scheme = spec
18            .strip_prefix("actr://")
19            .ok_or_else(|| anyhow::anyhow!("Invalid actr:// URI: {spec}"))?;
20        let name_end = without_scheme
21            .find(|c| ['/', '?'].contains(&c))
22            .unwrap_or(without_scheme.len());
23        let name = without_scheme[..name_end].trim();
24        if name.is_empty() {
25            return Err(anyhow::anyhow!("Invalid actr:// URI: {spec}"));
26        }
27
28        let mut version = None;
29        let mut fingerprint = None;
30        if let Some(query_start) = spec.find('?') {
31            let query = &spec[query_start + 1..];
32            for pair in query.split('&') {
33                if pair.is_empty() {
34                    continue;
35                }
36                let mut iter = pair.splitn(2, '=');
37                let key = iter.next().unwrap_or_default();
38                let value = iter.next().unwrap_or_default();
39                match key {
40                    "version" if !value.is_empty() => {
41                        version = Some(value.to_string());
42                    }
43                    "fingerprint" if !value.is_empty() => {
44                        fingerprint = Some(value.to_string());
45                    }
46                    _ => {}
47                }
48            }
49        }
50
51        Ok(DependencySpec {
52            name: name.to_string(),
53            uri: spec.to_string(),
54            version,
55            fingerprint,
56        })
57    }
58
59    fn parse_versioned_spec(&self, spec: &str) -> Result<DependencySpec> {
60        let (name, version) = spec
61            .rsplit_once('@')
62            .ok_or_else(|| anyhow::anyhow!("Invalid package specification: {spec}"))?;
63        if name.is_empty() || version.is_empty() {
64            return Err(anyhow::anyhow!("Invalid package specification: {spec}"));
65        }
66
67        let uri = format!("actr://{name}/?version={version}");
68        Ok(DependencySpec {
69            name: name.to_string(),
70            uri,
71            version: Some(version.to_string()),
72            fingerprint: None,
73        })
74    }
75
76    fn parse_simple_spec(&self, spec: &str) -> Result<DependencySpec> {
77        let name = spec.trim();
78        if name.is_empty() {
79            return Err(anyhow::anyhow!("Invalid package specification: {spec}"));
80        }
81        let uri = format!("actr://{name}/");
82        Ok(DependencySpec {
83            name: name.to_string(),
84            uri,
85            version: None,
86            fingerprint: None,
87        })
88    }
89}
90
91impl Default for DefaultDependencyResolver {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97#[async_trait]
98impl DependencyResolver for DefaultDependencyResolver {
99    async fn resolve_spec(&self, spec: &str) -> Result<DependencySpec> {
100        if spec.starts_with("actr://") {
101            return self.parse_actr_uri(spec);
102        }
103
104        if spec.contains('@') {
105            return self.parse_versioned_spec(spec);
106        }
107
108        self.parse_simple_spec(spec)
109    }
110
111    async fn resolve_dependencies(
112        &self,
113        specs: &[DependencySpec],
114    ) -> Result<Vec<ResolvedDependency>> {
115        let mut resolved = Vec::with_capacity(specs.len());
116
117        for spec in specs {
118            resolved.push(ResolvedDependency {
119                spec: spec.clone(),
120                uri: spec.uri.clone(),
121                resolved_version: spec.version.clone().unwrap_or_else(|| "latest".to_string()),
122                fingerprint: spec.fingerprint.clone().unwrap_or_default(),
123                proto_files: Vec::new(),
124            });
125        }
126
127        Ok(resolved)
128    }
129
130    async fn check_conflicts(&self, deps: &[ResolvedDependency]) -> Result<Vec<ConflictReport>> {
131        let mut conflicts = Vec::new();
132
133        for i in 0..deps.len() {
134            for j in (i + 1)..deps.len() {
135                if deps[i].spec.name != deps[j].spec.name {
136                    continue;
137                }
138
139                if deps[i].resolved_version != deps[j].resolved_version {
140                    conflicts.push(ConflictReport {
141                        dependency_a: deps[i].spec.name.clone(),
142                        dependency_b: deps[j].spec.name.clone(),
143                        conflict_type: ConflictType::VersionConflict,
144                        description: format!(
145                            "Dependency {} has conflicting versions: {} vs {}",
146                            deps[i].spec.name, deps[i].resolved_version, deps[j].resolved_version
147                        ),
148                    });
149                }
150
151                if !deps[i].fingerprint.is_empty()
152                    && !deps[j].fingerprint.is_empty()
153                    && deps[i].fingerprint != deps[j].fingerprint
154                {
155                    conflicts.push(ConflictReport {
156                        dependency_a: deps[i].spec.name.clone(),
157                        dependency_b: deps[j].spec.name.clone(),
158                        conflict_type: ConflictType::FingerprintMismatch,
159                        description: format!(
160                            "Dependency {} has conflicting fingerprints",
161                            deps[i].spec.name
162                        ),
163                    });
164                }
165            }
166        }
167
168        Ok(conflicts)
169    }
170
171    async fn build_dependency_graph(&self, deps: &[ResolvedDependency]) -> Result<DependencyGraph> {
172        let mut nodes = Vec::new();
173        for dep in deps {
174            if !nodes.contains(&dep.spec.name) {
175                nodes.push(dep.spec.name.clone());
176            }
177        }
178
179        Ok(DependencyGraph {
180            nodes,
181            edges: Vec::new(),
182            has_cycles: false,
183        })
184    }
185}