actr_cli/core/components/
dependency_resolver.rs1use 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}