Skip to main content

abi_loader/
package.rs

1//! Package Identity and Resolution Types
2//!
3//! This module provides types for tracking package identity during import resolution,
4//! enabling version conflict detection and cycle detection.
5
6use crate::file::{AbiFile, ImportSource};
7
8/* ============================================================================
9   Package Identity
10   ============================================================================ */
11
12/* Unique identifier for an ABI package, used for version conflict detection */
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct PackageId {
15    /* Fully qualified package name (e.g., "thru.common.primitives") */
16    pub package_name: String,
17    /* Package semantic version */
18    pub version: String,
19}
20
21impl PackageId {
22    /* Create a new package ID */
23    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    /* Create a PackageId from an AbiFile */
31    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    /* Check if two packages have the same name but different versions (conflict) */
39    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/* ============================================================================
51   Resolved Package
52   ============================================================================ */
53
54/* A fully resolved ABI package with its source and dependencies */
55#[derive(Debug, Clone)]
56pub struct ResolvedPackage {
57    /* Unique identifier for this package */
58    pub id: PackageId,
59    /* The import source this package was resolved from */
60    pub source: ImportSource,
61    /* The resolved ABI file contents */
62    pub abi_file: AbiFile,
63    /* IDs of packages this package depends on */
64    pub dependencies: Vec<PackageId>,
65    /* Whether this package was fetched from a remote source */
66    pub is_remote: bool,
67}
68
69impl ResolvedPackage {
70    /* Create a new resolved package */
71    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    /* Get the package name */
87    pub fn package_name(&self) -> &str {
88        &self.id.package_name
89    }
90
91    /* Get the package version */
92    pub fn version(&self) -> &str {
93        &self.id.version
94    }
95}
96
97/* ============================================================================
98   Resolution Error Types
99   ============================================================================ */
100
101/* Errors that can occur during import resolution */
102#[derive(Debug, Clone)]
103pub enum ResolveError {
104    /* Circular dependency detected */
105    CyclicDependency {
106        /* Package that was encountered twice */
107        package_id: PackageId,
108        /* Chain of packages leading to the cycle */
109        cycle_chain: Vec<PackageId>,
110    },
111
112    /* Version conflict: same package imported with different versions */
113    VersionConflict {
114        /* Package name that has conflicting versions */
115        package_name: String,
116        /* First version encountered */
117        version_a: String,
118        /* Second (conflicting) version encountered */
119        version_b: String,
120    },
121
122    /* Local import attempted from a remote package */
123    LocalImportFromRemote {
124        /* The remote package that tried to import locally */
125        remote_package: PackageId,
126        /* The local import that was attempted */
127        local_import: ImportSource,
128    },
129
130    /* Import source type not allowed by configuration */
131    ImportTypeNotAllowed {
132        /* The disallowed import source */
133        source: ImportSource,
134        /* Description of why it's not allowed */
135        reason: String,
136    },
137
138    /* Failed to fetch import content */
139    FetchError {
140        /* The import that failed to fetch */
141        source: ImportSource,
142        /* Error message */
143        message: String,
144    },
145
146    /* Failed to parse ABI file */
147    ParseError {
148        /* Location of the ABI file */
149        location: String,
150        /* Parse error message */
151        message: String,
152    },
153
154    /* Failed to initialize resolver infrastructure (e.g. HTTP client) */
155    InitError {
156        /* Error message */
157        message: String,
158    },
159
160    /* Revision requirement not satisfied for on-chain import */
161    RevisionMismatch {
162        /* The import that had a revision mismatch */
163        source: ImportSource,
164        /* Required revision specifier */
165        required: String,
166        /* Actual revision found */
167        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/* ============================================================================
237   Resolution Result Type
238   ============================================================================ */
239
240/* Result of a full import resolution */
241#[derive(Debug, Clone)]
242pub struct ResolutionResult {
243    /* The root package that was resolved */
244    pub root: ResolvedPackage,
245    /* All resolved packages (including transitive dependencies) */
246    pub all_packages: Vec<ResolvedPackage>,
247}
248
249impl ResolutionResult {
250    /* Get the total number of packages resolved */
251    pub fn package_count(&self) -> usize {
252        self.all_packages.len()
253    }
254
255    /* Get a package by its ID */
256    pub fn get_package(&self, id: &PackageId) -> Option<&ResolvedPackage> {
257        self.all_packages.iter().find(|p| p.id == *id)
258    }
259
260    /* Get all package IDs */
261    pub fn package_ids(&self) -> Vec<&PackageId> {
262        self.all_packages.iter().map(|p| &p.id).collect()
263    }
264
265    /* Create a manifest map (package_name -> ABI YAML) for WASM consumption */
266    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}