use std::collections::{HashSet, HashMap, BinaryHeap};
use anyhow::{Result, bail};
use pyo3::prelude::*;
use super::{Package, Id, manifest::{Dependency, Version}};
#[derive(Debug, Default, Clone)]
#[pyclass(module = "merlon.package.registry")]
pub struct Registry {
packages: HashMap<Id, Package>,
}
#[pymethods]
impl Registry {
#[new]
pub fn new() -> Self {
Self {
packages: Default::default(),
}
}
pub fn register(&mut self, package: Package) -> Result<Id> {
let id = package.id()?;
if self.packages.contains_key(&id) {
bail!("package {} already in registry", package);
}
self.packages.insert(id, package);
Ok(id)
}
pub fn take(&mut self, id: Id) -> Result<Package> {
match self.packages.remove(&id) {
Some(package) => Ok(package),
None => bail!("package {} not in registry", id),
}
}
pub fn has(&self, id: Id) -> bool {
self.packages.contains_key(&id)
}
}
impl Registry {
pub fn get(&self, id: Id) -> Option<&Package> {
self.packages.get(&id)
}
pub fn get_or_error(&self, id: Id) -> Result<&Package> {
match self.get(id) {
Some(package) => Ok(package),
None => bail!("package {id} not found in registry"),
}
}
pub fn edit<F, T>(&mut self, id: Id, f: F) -> Result<T>
where
F: FnOnce(&mut Package) -> Result<T>
{
let mut package = self.take(id)?;
let ret = f(&mut package)?;
self.register(package)?;
Ok(ret)
}
pub fn package_ids(&self) -> impl Iterator<Item = Id> + '_ {
self.packages.keys().copied()
}
}
#[pymethods]
impl Registry {
pub fn get_direct_dependencies(&self, id: Id) -> Result<HashSet<Dependency>> {
let package = self.get_or_error(id)?;
let manifest = package.manifest()?;
Ok(manifest.iter_direct_dependencies()
.map(|dep| dep.clone()) .collect())
}
pub fn get_dependencies(&self, id: Id) -> Result<HashSet<Dependency>> {
let mut dependencies = HashSet::new();
let mut stack: Vec<Dependency> = self.get_direct_dependencies(id)?
.into_iter()
.collect();
while let Some(popped_dep) = stack.pop() {
if dependencies.contains(&popped_dep) {
continue;
}
if let Dependency::Package { id: dep_id, .. } = &popped_dep {
if *dep_id == id {
bail!("found circular dependency");
}
for dependency in self.get_direct_dependencies(*dep_id)? {
stack.push(dependency);
}
}
dependencies.insert(popped_dep);
}
Ok(dependencies)
}
pub fn all_dependencies(&self) -> Result<HashSet<Dependency>> {
let mut dependencies = HashSet::new();
for (id, _) in self.packages.iter() {
for dependency in self.get_dependencies(*id)? {
dependencies.insert(dependency);
}
}
Ok(dependencies)
}
pub fn has_dependency(&self, id: Id, dependency_id: Id) -> Result<bool> {
let dependencies = self.get_dependencies(id)?;
Ok(dependencies.iter().any(|dep| {
if let Dependency::Package { id: dep_id, .. } = dep {
*dep_id == dependency_id
} else {
false
}
}))
}
pub fn add_direct_dependency(&mut self, id: Id, dependency_id: Id) -> Result<()> {
let package = self.get_or_error(id)?;
let dependency_package = self.get_or_error(dependency_id)?;
let dependency_manifest = dependency_package.manifest()?;
let dependency_metadata = dependency_manifest.metadata();
let dependency = dependency_metadata.into();
package.edit_manifest(|manifest| {
manifest.declare_direct_dependency(dependency)
})
}
pub fn check_version_compatibility(&self) -> Result<()> {
let map = self.package_version_map()?;
for dependency in self.all_dependencies()? {
if let Dependency::Package { id, version } = dependency {
match map.get(&id) {
None => bail!("dependency exists for {id} {version}, but it is not in registry"),
Some(actual_version) => {
if !version.matches(actual_version) {
bail!(
"a package depends on {} {} which is incompatible with its actual version {}",
self.get(id).unwrap(), version,
actual_version,
);
}
}
}
}
}
Ok(())
}
pub fn calc_dependency_patch_order(&self, root: Id) -> Result<Vec<Id>> {
if !self.get_orphans(root)?.is_empty() {
bail!("there are orphaned packages");
}
let ordering = self.topological_ordering()?;
if ordering.last() != Some(&root) {
bail!("root package is not the final package in the topological ordering");
}
Ok(ordering)
}
pub fn topological_ordering(&self) -> Result<Vec<Id>> {
let mut topological_ordering = Vec::new();
let mut not_visited: BinaryHeap<Id> = self.packages.keys().map(|&k| k).collect();
let mut visit_in_progress = HashSet::new(); let mut visited = HashSet::new(); while let Some(id) = not_visited.pop() {
self.topological_ordering_visit(id, &mut topological_ordering, &mut visit_in_progress, &mut visited)?;
}
Ok(topological_ordering)
}
pub fn get_orphans(&self, root: Id) -> Result<HashSet<Id>> {
let dependency_ids: HashSet<Id> = self.get_dependencies(root)?
.into_iter()
.filter_map(|dep| {
if let Dependency::Package { id, .. } = dep {
Some(id)
} else {
None
}
})
.collect();
Ok(self.packages.keys()
.map(|&id| id)
.filter(|&id| id != root && !dependency_ids.contains(&id))
.collect())
}
pub fn delete_orphans(&mut self, root: Id) -> Result<()> {
for id in self.get_orphans(root)? {
let package = self.take(id)?;
std::fs::remove_dir_all(package.path())?;
}
Ok(())
}
}
impl Registry {
pub fn package_version_map(&self) -> Result<HashMap<Id, Version>> {
let mut map = HashMap::new();
for (id, package) in self.packages.iter() {
let id = *id;
let manifest = package.manifest()?;
let metadata = manifest.metadata();
if metadata.id() != id {
bail!("package id mismatch: {id}");
}
let version = metadata.version();
if let Some(other_version) = map.insert(id, version.clone()) {
if other_version != *version {
bail!("package {id} has multiple versions: {version} and {other_version}");
}
}
}
Ok(map)
}
fn topological_ordering_visit(
&self,
id: Id,
topological_ordering: &mut Vec<Id>,
visit_in_progress: &mut HashSet<Id>,
visited: &mut HashSet<Id>,
) -> Result<()> {
if !visited.contains(&id) {
if visit_in_progress.contains(&id) {
bail!("found circular dependency {}", id);
}
visit_in_progress.insert(id);
let package = self.get_or_error(id)?;
let manifest = package.manifest()?;
for dependency in manifest.iter_direct_dependencies() {
if let Dependency::Package { id, .. } = dependency {
self.topological_ordering_visit(*id, topological_ordering, visit_in_progress, visited)?;
}
}
visit_in_progress.remove(&id);
visited.insert(id);
topological_ordering.push(id);
}
Ok(())
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use temp_dir::TempDir;
use anyhow::Result;
use super::{Registry, Package, Dependency, Version};
#[test]
fn dependency_graph() -> Result<()> {
let dir = TempDir::new()?;
let mut registry = Registry::new();
let base = Package::new("Base", dir.path().join("base.merlon"))?;
let a = Package::new("A", dir.path().join("a.merlon"))?;
let b = Package::new("B", dir.path().join("b.merlon"))?;
let c = Package::new("C", dir.path().join("c.merlon"))?;
let base = registry.register(base)?;
let a = registry.register(a)?;
let b = registry.register(b)?;
let c = registry.register(c)?;
dbg!(&base, &a, &b, &c);
registry.add_direct_dependency(a, b)?;
registry.add_direct_dependency(b, c)?;
for parent in [a, b, c] {
registry.add_direct_dependency(parent, base)?;
}
let base_as_dep: Dependency = registry.get_or_error(base)?.try_into()?;
let b_as_dep: Dependency = registry.get_or_error(b)?.try_into()?;
let c_as_dep: Dependency = registry.get_or_error(c)?.try_into()?;
let deps = registry.get_dependencies(a)?;
let expected: HashSet<Dependency> = vec![b_as_dep.clone(), c_as_dep.clone(), base_as_dep.clone()].into_iter().collect();
assert_eq!(deps, expected);
let deps = registry.get_dependencies(b)?;
let expected: HashSet<Dependency> = vec![c_as_dep.clone(), base_as_dep.clone()].into_iter().collect();
assert_eq!(deps, expected);
let deps = registry.get_dependencies(c)?;
let expected: HashSet<Dependency> = vec![base_as_dep.clone()].into_iter().collect();
assert_eq!(deps, expected);
let base_deps = registry.get_dependencies(base)?;
assert!(base_deps.is_empty());
registry.check_version_compatibility()?;
registry.edit(base, |package| {
package.edit_manifest(|manifest| {
manifest.metadata_mut().set_version(Version::new(2, 0, 0));
Ok(())
})
})?;
assert!(registry.check_version_compatibility().is_err());
let sorted = registry.topological_ordering()?;
assert_eq!(sorted, vec![base, c, b, a]);
assert!(registry.get_orphans(a)?.is_empty());
let orphan_path = dir.path().join("orphan.merlon");
let orphan = Package::new("Orphan", orphan_path.clone())?;
let orphan = registry.register(orphan)?;
let sorted = registry.topological_ordering()?;
assert!(sorted[sorted.len() - 2] == orphan || sorted[sorted.len() - 1] == orphan);
assert_eq!(registry.get_orphans(a)?, vec![orphan].into_iter().collect());
registry.delete_orphans(a)?;
assert!(!registry.has(orphan));
assert!(!orphan_path.exists());
registry.add_direct_dependency(c, a)?;
assert!(registry.topological_ordering().is_err());
Ok(())
}
}