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(source: ImportSource, abi_file: AbiFile, dependencies: Vec<PackageId>) -> Self {
72 let is_remote = source.is_remote();
73 Self {
74 id: PackageId::from_abi_file(&abi_file),
75 source,
76 abi_file,
77 dependencies,
78 is_remote,
79 }
80 }
81
82 pub fn package_name(&self) -> &str {
84 &self.id.package_name
85 }
86
87 pub fn version(&self) -> &str {
89 &self.id.version
90 }
91}
92
93#[derive(Debug, Clone)]
99pub enum ResolveError {
100 CyclicDependency {
102 package_id: PackageId,
104 cycle_chain: Vec<PackageId>,
106 },
107
108 VersionConflict {
110 package_name: String,
112 version_a: String,
114 version_b: String,
116 },
117
118 LocalImportFromRemote {
120 remote_package: PackageId,
122 local_import: ImportSource,
124 },
125
126 ImportTypeNotAllowed {
128 source: ImportSource,
130 reason: String,
132 },
133
134 FetchError {
136 source: ImportSource,
138 message: String,
140 },
141
142 ParseError {
144 location: String,
146 message: String,
148 },
149
150 InitError {
152 message: String,
154 },
155
156 RevisionMismatch {
158 source: ImportSource,
160 required: String,
162 actual: u64,
164 },
165}
166
167impl std::fmt::Display for ResolveError {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 match self {
170 ResolveError::CyclicDependency {
171 package_id,
172 cycle_chain,
173 } => {
174 write!(
175 f,
176 "Circular dependency detected: {} (chain: {})",
177 package_id,
178 cycle_chain
179 .iter()
180 .map(|p| p.to_string())
181 .collect::<Vec<_>>()
182 .join(" -> ")
183 )
184 }
185 ResolveError::VersionConflict {
186 package_name,
187 version_a,
188 version_b,
189 } => {
190 write!(
191 f,
192 "Version conflict for package '{}': {} vs {}",
193 package_name, version_a, version_b
194 )
195 }
196 ResolveError::LocalImportFromRemote {
197 remote_package,
198 local_import,
199 } => {
200 write!(
201 f,
202 "Remote package '{}' cannot have local import: {:?}",
203 remote_package, local_import
204 )
205 }
206 ResolveError::ImportTypeNotAllowed { source, reason } => {
207 write!(f, "Import type not allowed: {:?} - {}", source, reason)
208 }
209 ResolveError::FetchError { source, message } => {
210 write!(f, "Failed to fetch {:?}: {}", source, message)
211 }
212 ResolveError::ParseError { location, message } => {
213 write!(f, "Failed to parse ABI at '{}': {}", location, message)
214 }
215 ResolveError::InitError { message } => {
216 write!(f, "Initialization error: {}", message)
217 }
218 ResolveError::RevisionMismatch {
219 source,
220 required,
221 actual,
222 } => {
223 write!(
224 f,
225 "Revision mismatch for {:?}: required {}, got {}",
226 source, required, actual
227 )
228 }
229 }
230 }
231}
232
233impl std::error::Error for ResolveError {}
234
235#[derive(Debug, Clone)]
241pub struct ResolutionResult {
242 pub root: ResolvedPackage,
244 pub all_packages: Vec<ResolvedPackage>,
246}
247
248impl ResolutionResult {
249 pub fn package_count(&self) -> usize {
251 self.all_packages.len()
252 }
253
254 pub fn get_package(&self, id: &PackageId) -> Option<&ResolvedPackage> {
256 self.all_packages.iter().find(|p| p.id == *id)
257 }
258
259 pub fn package_ids(&self) -> Vec<&PackageId> {
261 self.all_packages.iter().map(|p| &p.id).collect()
262 }
263
264 pub fn to_manifest(&self) -> std::collections::HashMap<String, String> {
266 let mut manifest = std::collections::HashMap::new();
267 for pkg in &self.all_packages {
268 if let Ok(yaml) = serde_yml::to_string(&pkg.abi_file) {
269 manifest.insert(pkg.id.package_name.clone(), yaml);
270 }
271 }
272 manifest
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_package_id_display() {
282 let id = PackageId::new("thru.common.primitives", "1.0.0");
283 assert_eq!(id.to_string(), "thru.common.primitives@1.0.0");
284 }
285
286 #[test]
287 fn test_package_id_conflicts() {
288 let id_a = PackageId::new("thru.common", "1.0.0");
289 let id_b = PackageId::new("thru.common", "2.0.0");
290 let id_c = PackageId::new("thru.other", "1.0.0");
291
292 assert!(id_a.conflicts_with(&id_b));
293 assert!(!id_a.conflicts_with(&id_c));
294 assert!(!id_a.conflicts_with(&id_a));
295 }
296
297 #[test]
298 fn test_resolve_error_display() {
299 let err = ResolveError::VersionConflict {
300 package_name: "thru.common".to_string(),
301 version_a: "1.0.0".to_string(),
302 version_b: "2.0.0".to_string(),
303 };
304 let msg = err.to_string();
305 assert!(msg.contains("thru.common"));
306 assert!(msg.contains("1.0.0"));
307 assert!(msg.contains("2.0.0"));
308 }
309}