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/* ============================================================================
9Package 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/* ============================================================================
51Resolved 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(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    /* Get the package name */
83    pub fn package_name(&self) -> &str {
84        &self.id.package_name
85    }
86
87    /* Get the package version */
88    pub fn version(&self) -> &str {
89        &self.id.version
90    }
91}
92
93/* ============================================================================
94Resolution Error Types
95============================================================================ */
96
97/* Errors that can occur during import resolution */
98#[derive(Debug, Clone)]
99pub enum ResolveError {
100    /* Circular dependency detected */
101    CyclicDependency {
102        /* Package that was encountered twice */
103        package_id: PackageId,
104        /* Chain of packages leading to the cycle */
105        cycle_chain: Vec<PackageId>,
106    },
107
108    /* Version conflict: same package imported with different versions */
109    VersionConflict {
110        /* Package name that has conflicting versions */
111        package_name: String,
112        /* First version encountered */
113        version_a: String,
114        /* Second (conflicting) version encountered */
115        version_b: String,
116    },
117
118    /* Local import attempted from a remote package */
119    LocalImportFromRemote {
120        /* The remote package that tried to import locally */
121        remote_package: PackageId,
122        /* The local import that was attempted */
123        local_import: ImportSource,
124    },
125
126    /* Import source type not allowed by configuration */
127    ImportTypeNotAllowed {
128        /* The disallowed import source */
129        source: ImportSource,
130        /* Description of why it's not allowed */
131        reason: String,
132    },
133
134    /* Failed to fetch import content */
135    FetchError {
136        /* The import that failed to fetch */
137        source: ImportSource,
138        /* Error message */
139        message: String,
140    },
141
142    /* Failed to parse ABI file */
143    ParseError {
144        /* Location of the ABI file */
145        location: String,
146        /* Parse error message */
147        message: String,
148    },
149
150    /* Failed to initialize resolver infrastructure (e.g. HTTP client) */
151    InitError {
152        /* Error message */
153        message: String,
154    },
155
156    /* Revision requirement not satisfied for on-chain import */
157    RevisionMismatch {
158        /* The import that had a revision mismatch */
159        source: ImportSource,
160        /* Required revision specifier */
161        required: String,
162        /* Actual revision found */
163        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/* ============================================================================
236Resolution Result Type
237============================================================================ */
238
239/* Result of a full import resolution */
240#[derive(Debug, Clone)]
241pub struct ResolutionResult {
242    /* The root package that was resolved */
243    pub root: ResolvedPackage,
244    /* All resolved packages (including transitive dependencies) */
245    pub all_packages: Vec<ResolvedPackage>,
246}
247
248impl ResolutionResult {
249    /* Get the total number of packages resolved */
250    pub fn package_count(&self) -> usize {
251        self.all_packages.len()
252    }
253
254    /* Get a package by its ID */
255    pub fn get_package(&self, id: &PackageId) -> Option<&ResolvedPackage> {
256        self.all_packages.iter().find(|p| p.id == *id)
257    }
258
259    /* Get all package IDs */
260    pub fn package_ids(&self) -> Vec<&PackageId> {
261        self.all_packages.iter().map(|p| &p.id).collect()
262    }
263
264    /* Create a manifest map (package_name -> ABI YAML) for WASM consumption */
265    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}