1use crate::file::{AbiFile, ImportSource};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct PackageId {
15 pub package_name: String,
17 pub version: String,
19}
20
21impl PackageId {
22 pub fn new(package_name: impl Into<String>, version: impl Into<String>) -> Self {
24 Self {
25 package_name: package_name.into(),
26 version: version.into(),
27 }
28 }
29
30 pub fn from_abi_file(abi_file: &AbiFile) -> Self {
32 Self {
33 package_name: abi_file.package().to_string(),
34 version: abi_file.package_version().to_string(),
35 }
36 }
37
38 pub fn conflicts_with(&self, other: &PackageId) -> bool {
40 self.package_name == other.package_name && self.version != other.version
41 }
42}
43
44impl std::fmt::Display for PackageId {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 write!(f, "{}@{}", self.package_name, self.version)
47 }
48}
49
50#[derive(Debug, Clone)]
56pub struct ResolvedPackage {
57 pub id: PackageId,
59 pub source: ImportSource,
61 pub abi_file: AbiFile,
63 pub dependencies: Vec<PackageId>,
65 pub is_remote: bool,
67}
68
69impl ResolvedPackage {
70 pub fn new(
72 source: ImportSource,
73 abi_file: AbiFile,
74 dependencies: Vec<PackageId>,
75 ) -> Self {
76 let is_remote = source.is_remote();
77 Self {
78 id: PackageId::from_abi_file(&abi_file),
79 source,
80 abi_file,
81 dependencies,
82 is_remote,
83 }
84 }
85
86 pub fn package_name(&self) -> &str {
88 &self.id.package_name
89 }
90
91 pub fn version(&self) -> &str {
93 &self.id.version
94 }
95}
96
97#[derive(Debug, Clone)]
103pub enum ResolveError {
104 CyclicDependency {
106 package_id: PackageId,
108 cycle_chain: Vec<PackageId>,
110 },
111
112 VersionConflict {
114 package_name: String,
116 version_a: String,
118 version_b: String,
120 },
121
122 LocalImportFromRemote {
124 remote_package: PackageId,
126 local_import: ImportSource,
128 },
129
130 ImportTypeNotAllowed {
132 source: ImportSource,
134 reason: String,
136 },
137
138 FetchError {
140 source: ImportSource,
142 message: String,
144 },
145
146 ParseError {
148 location: String,
150 message: String,
152 },
153
154 InitError {
156 message: String,
158 },
159
160 RevisionMismatch {
162 source: ImportSource,
164 required: String,
166 actual: u64,
168 },
169}
170
171impl std::fmt::Display for ResolveError {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 match self {
174 ResolveError::CyclicDependency { package_id, cycle_chain } => {
175 write!(
176 f,
177 "Circular dependency detected: {} (chain: {})",
178 package_id,
179 cycle_chain
180 .iter()
181 .map(|p| p.to_string())
182 .collect::<Vec<_>>()
183 .join(" -> ")
184 )
185 }
186 ResolveError::VersionConflict {
187 package_name,
188 version_a,
189 version_b,
190 } => {
191 write!(
192 f,
193 "Version conflict for package '{}': {} vs {}",
194 package_name, version_a, version_b
195 )
196 }
197 ResolveError::LocalImportFromRemote {
198 remote_package,
199 local_import,
200 } => {
201 write!(
202 f,
203 "Remote package '{}' cannot have local import: {:?}",
204 remote_package, local_import
205 )
206 }
207 ResolveError::ImportTypeNotAllowed { source, reason } => {
208 write!(f, "Import type not allowed: {:?} - {}", source, reason)
209 }
210 ResolveError::FetchError { source, message } => {
211 write!(f, "Failed to fetch {:?}: {}", source, message)
212 }
213 ResolveError::ParseError { location, message } => {
214 write!(f, "Failed to parse ABI at '{}': {}", location, message)
215 }
216 ResolveError::InitError { message } => {
217 write!(f, "Initialization error: {}", message)
218 }
219 ResolveError::RevisionMismatch {
220 source,
221 required,
222 actual,
223 } => {
224 write!(
225 f,
226 "Revision mismatch for {:?}: required {}, got {}",
227 source, required, actual
228 )
229 }
230 }
231 }
232}
233
234impl std::error::Error for ResolveError {}
235
236#[derive(Debug, Clone)]
242pub struct ResolutionResult {
243 pub root: ResolvedPackage,
245 pub all_packages: Vec<ResolvedPackage>,
247}
248
249impl ResolutionResult {
250 pub fn package_count(&self) -> usize {
252 self.all_packages.len()
253 }
254
255 pub fn get_package(&self, id: &PackageId) -> Option<&ResolvedPackage> {
257 self.all_packages.iter().find(|p| p.id == *id)
258 }
259
260 pub fn package_ids(&self) -> Vec<&PackageId> {
262 self.all_packages.iter().map(|p| &p.id).collect()
263 }
264
265 pub fn to_manifest(&self) -> std::collections::HashMap<String, String> {
267 let mut manifest = std::collections::HashMap::new();
268 for pkg in &self.all_packages {
269 if let Ok(yaml) = serde_yml::to_string(&pkg.abi_file) {
270 manifest.insert(pkg.id.package_name.clone(), yaml);
271 }
272 }
273 manifest
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_package_id_display() {
283 let id = PackageId::new("thru.common.primitives", "1.0.0");
284 assert_eq!(id.to_string(), "thru.common.primitives@1.0.0");
285 }
286
287 #[test]
288 fn test_package_id_conflicts() {
289 let id_a = PackageId::new("thru.common", "1.0.0");
290 let id_b = PackageId::new("thru.common", "2.0.0");
291 let id_c = PackageId::new("thru.other", "1.0.0");
292
293 assert!(id_a.conflicts_with(&id_b));
294 assert!(!id_a.conflicts_with(&id_c));
295 assert!(!id_a.conflicts_with(&id_a));
296 }
297
298 #[test]
299 fn test_resolve_error_display() {
300 let err = ResolveError::VersionConflict {
301 package_name: "thru.common".to_string(),
302 version_a: "1.0.0".to_string(),
303 version_b: "2.0.0".to_string(),
304 };
305 let msg = err.to_string();
306 assert!(msg.contains("thru.common"));
307 assert!(msg.contains("1.0.0"));
308 assert!(msg.contains("2.0.0"));
309 }
310}