turtle-build 0.4.10

Ninja-compatible build system for high-level programming languages
use petgraph::{Graph, algo::is_cyclic_directed};
use std::{
    collections::HashMap,
    error::Error,
    fmt::{self, Display, Formatter},
    path::{Path, PathBuf},
};

pub type ModuleDependencyMap = HashMap<PathBuf, HashMap<String, PathBuf>>;

pub fn validate(modules: &ModuleDependencyMap) -> Result<(), ModuleDependencyError> {
    if is_module_dependency_circular(modules) {
        return Err(ModuleDependencyError::CircularDependency);
    }

    Ok(())
}

fn is_module_dependency_circular(modules: &ModuleDependencyMap) -> bool {
    let mut graph = Graph::<&Path, ()>::new();
    let mut indices = HashMap::<&Path, _>::new();

    for output in modules.keys() {
        indices.insert(output, graph.add_node(output));
    }

    for (output, build) in modules {
        for input in build.values() {
            graph.add_edge(indices[output.as_path()], indices[input.as_path()], ());
        }
    }

    is_cyclic_directed(&graph)
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ModuleDependencyError {
    CircularDependency,
}

impl Error for ModuleDependencyError {}

impl Display for ModuleDependencyError {
    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
        match self {
            Self::CircularDependency => {
                write!(formatter, "build file dependency cycle detected")
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn validate_empty() {
        assert_eq!(validate(&Default::default()), Ok(()));
    }

    #[test]
    fn validate_module() {
        assert_eq!(
            validate(&[("foo".into(), Default::default())].into_iter().collect()),
            Ok(())
        );
    }

    #[test]
    fn validate_circular_module() {
        assert_eq!(
            validate(
                &[(
                    "foo".into(),
                    [("foo".into(), "foo".into())].into_iter().collect()
                )]
                .into_iter()
                .collect()
            ),
            Err(ModuleDependencyError::CircularDependency)
        );
    }

    #[test]
    fn validate_two_modules() {
        assert_eq!(
            validate(
                &[
                    (
                        "foo".into(),
                        [("bar".into(), "bar".into())].into_iter().collect()
                    ),
                    ("bar".into(), Default::default(),)
                ]
                .into_iter()
                .collect()
            ),
            Ok(())
        );
    }

    #[test]
    fn validate_two_circular_modules() {
        assert_eq!(
            validate(
                &[
                    (
                        "foo".into(),
                        [("bar".into(), "bar".into())].into_iter().collect()
                    ),
                    (
                        "bar".into(),
                        [("foo".into(), "foo".into())].into_iter().collect()
                    ),
                ]
                .into_iter()
                .collect()
            ),
            Err(ModuleDependencyError::CircularDependency)
        );
    }
}