codecraft 0.1.2

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, a yakui-drawn UI, audio and gamepad haptics; its binary maps any folder, and the symbols of its Rust files, as a 3D wall of boxes
Documentation
//! The crates of a Cargo workspace, read with `cargo metadata`.
use std::collections::HashSet;
use std::path::{Path, PathBuf};

use cargo_metadata::{DependencyKind, MetadataCommand};

#[derive(Clone, Debug)]
pub struct Crate {
    pub name: String,
    pub dir: PathBuf,
    /// The `lib.rs` and `main.rs` files, library first.
    pub roots: Vec<PathBuf>,
    /// Names of the workspace members it depends on.
    pub dependencies: Vec<String>,
}

#[derive(Clone, Debug)]
pub struct Workspace {
    pub name: String,
    pub root: PathBuf,
    /// Sorted by name.
    pub crates: Vec<Crate>,
}

/// Reads the workspace containing `dir`; a single package counts as a workspace of one.
pub fn read(dir: &Path) -> Result<Workspace, String> {
    let metadata = MetadataCommand::new()
        .current_dir(dir)
        .no_deps()
        .exec()
        .map_err(|error| format!("cargo metadata failed: {error}"))?;

    let members: HashSet<_> = metadata.workspace_members.iter().collect();
    let mut packages: Vec<_> = metadata
        .packages
        .iter()
        .filter(|package| members.contains(&package.id))
        .collect();
    packages.sort_by(|a, b| a.name.cmp(&b.name));
    let names: HashSet<String> = packages.iter().map(|p| p.name.to_string()).collect();

    let crates = packages
        .iter()
        .map(|package| {
            let mut targets: Vec<_> = package
                .targets
                .iter()
                .filter(|target| target.is_lib() || target.is_bin())
                .collect();
            targets.sort_by_key(|target| !target.is_lib());
            let mut dependencies: Vec<String> = package
                .dependencies
                .iter()
                .filter(|dep| dep.kind == DependencyKind::Normal && names.contains(&dep.name))
                .map(|dep| dep.name.clone())
                .collect();
            dependencies.sort();
            dependencies.dedup();
            Crate {
                name: package.name.to_string(),
                dir: package
                    .manifest_path
                    .parent()
                    .map(|dir| dir.as_std_path().to_path_buf())
                    .unwrap_or_default(),
                roots: targets
                    .iter()
                    .map(|target| target.src_path.as_std_path().to_path_buf())
                    .collect(),
                dependencies,
            }
        })
        .collect();

    let root = metadata.workspace_root.as_std_path().to_path_buf();
    let name = root
        .file_name()
        .map(|name| name.to_string_lossy().into_owned())
        .unwrap_or_else(|| "workspace".into());
    Ok(Workspace { name, root, crates })
}

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

    fn this_workspace() -> Workspace {
        let dir = Path::new(env!("CARGO_MANIFEST_DIR"));
        read(dir).expect("this repository is a workspace")
    }

    #[test]
    fn it_lists_the_members_sorted_by_name() {
        let workspace = this_workspace();
        let names: Vec<&str> = workspace.crates.iter().map(|c| c.name.as_str()).collect();
        assert!(names.contains(&"codecraft"), "{names:?}");
        assert!(names.contains(&"tanks"), "{names:?}");
        let mut sorted = names.clone();
        sorted.sort();
        assert_eq!(names, sorted);
        assert_eq!(workspace.name, "codecraft");
    }

    #[test]
    fn examples_depend_on_the_engine_and_have_a_main() {
        let workspace = this_workspace();
        let tanks = workspace
            .crates
            .iter()
            .find(|c| c.name == "tanks")
            .expect("tanks");
        assert_eq!(tanks.dependencies, vec!["codecraft"]);
        assert!(tanks.roots.iter().any(|r| r.ends_with("main.rs")));

        let engine = workspace
            .crates
            .iter()
            .find(|c| c.name == "codecraft")
            .expect("codecraft");
        assert!(engine.roots[0].ends_with("lib.rs"), "library first");
        assert!(engine.dependencies.is_empty());
    }
}