use crate::file::{AbiFile, ImportSource};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PackageId {
pub package_name: String,
pub version: String,
}
impl PackageId {
pub fn new(package_name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
package_name: package_name.into(),
version: version.into(),
}
}
pub fn from_abi_file(abi_file: &AbiFile) -> Self {
Self {
package_name: abi_file.package().to_string(),
version: abi_file.package_version().to_string(),
}
}
pub fn conflicts_with(&self, other: &PackageId) -> bool {
self.package_name == other.package_name && self.version != other.version
}
}
impl std::fmt::Display for PackageId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}@{}", self.package_name, self.version)
}
}
#[derive(Debug, Clone)]
pub struct ResolvedPackage {
pub id: PackageId,
pub source: ImportSource,
pub abi_file: AbiFile,
pub dependencies: Vec<PackageId>,
pub is_remote: bool,
}
impl ResolvedPackage {
pub fn new(
source: ImportSource,
abi_file: AbiFile,
dependencies: Vec<PackageId>,
) -> Self {
let is_remote = source.is_remote();
Self {
id: PackageId::from_abi_file(&abi_file),
source,
abi_file,
dependencies,
is_remote,
}
}
pub fn package_name(&self) -> &str {
&self.id.package_name
}
pub fn version(&self) -> &str {
&self.id.version
}
}
#[derive(Debug, Clone)]
pub enum ResolveError {
CyclicDependency {
package_id: PackageId,
cycle_chain: Vec<PackageId>,
},
VersionConflict {
package_name: String,
version_a: String,
version_b: String,
},
LocalImportFromRemote {
remote_package: PackageId,
local_import: ImportSource,
},
ImportTypeNotAllowed {
source: ImportSource,
reason: String,
},
FetchError {
source: ImportSource,
message: String,
},
ParseError {
location: String,
message: String,
},
InitError {
message: String,
},
RevisionMismatch {
source: ImportSource,
required: String,
actual: u64,
},
}
impl std::fmt::Display for ResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResolveError::CyclicDependency { package_id, cycle_chain } => {
write!(
f,
"Circular dependency detected: {} (chain: {})",
package_id,
cycle_chain
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(" -> ")
)
}
ResolveError::VersionConflict {
package_name,
version_a,
version_b,
} => {
write!(
f,
"Version conflict for package '{}': {} vs {}",
package_name, version_a, version_b
)
}
ResolveError::LocalImportFromRemote {
remote_package,
local_import,
} => {
write!(
f,
"Remote package '{}' cannot have local import: {:?}",
remote_package, local_import
)
}
ResolveError::ImportTypeNotAllowed { source, reason } => {
write!(f, "Import type not allowed: {:?} - {}", source, reason)
}
ResolveError::FetchError { source, message } => {
write!(f, "Failed to fetch {:?}: {}", source, message)
}
ResolveError::ParseError { location, message } => {
write!(f, "Failed to parse ABI at '{}': {}", location, message)
}
ResolveError::InitError { message } => {
write!(f, "Initialization error: {}", message)
}
ResolveError::RevisionMismatch {
source,
required,
actual,
} => {
write!(
f,
"Revision mismatch for {:?}: required {}, got {}",
source, required, actual
)
}
}
}
}
impl std::error::Error for ResolveError {}
#[derive(Debug, Clone)]
pub struct ResolutionResult {
pub root: ResolvedPackage,
pub all_packages: Vec<ResolvedPackage>,
}
impl ResolutionResult {
pub fn package_count(&self) -> usize {
self.all_packages.len()
}
pub fn get_package(&self, id: &PackageId) -> Option<&ResolvedPackage> {
self.all_packages.iter().find(|p| p.id == *id)
}
pub fn package_ids(&self) -> Vec<&PackageId> {
self.all_packages.iter().map(|p| &p.id).collect()
}
pub fn to_manifest(&self) -> std::collections::HashMap<String, String> {
let mut manifest = std::collections::HashMap::new();
for pkg in &self.all_packages {
if let Ok(yaml) = serde_yml::to_string(&pkg.abi_file) {
manifest.insert(pkg.id.package_name.clone(), yaml);
}
}
manifest
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_package_id_display() {
let id = PackageId::new("thru.common.primitives", "1.0.0");
assert_eq!(id.to_string(), "thru.common.primitives@1.0.0");
}
#[test]
fn test_package_id_conflicts() {
let id_a = PackageId::new("thru.common", "1.0.0");
let id_b = PackageId::new("thru.common", "2.0.0");
let id_c = PackageId::new("thru.other", "1.0.0");
assert!(id_a.conflicts_with(&id_b));
assert!(!id_a.conflicts_with(&id_c));
assert!(!id_a.conflicts_with(&id_a));
}
#[test]
fn test_resolve_error_display() {
let err = ResolveError::VersionConflict {
package_name: "thru.common".to_string(),
version_a: "1.0.0".to_string(),
version_b: "2.0.0".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("thru.common"));
assert!(msg.contains("1.0.0"));
assert!(msg.contains("2.0.0"));
}
}