cargo-mend 0.1.0

Opinionated visibility auditing for Rust crates and workspaces
use std::fs;
use std::hash::Hash;
use std::hash::Hasher;
use std::path::Path;
use std::path::PathBuf;

use anyhow::Context;
use anyhow::Result;
use serde::Deserialize;
use serde::Serialize;

#[derive(Debug, Default, Deserialize)]
struct ConfigFile {
    #[serde(default)]
    visibility: VisibilityConfig,
}

#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct VisibilityConfig {
    #[serde(default)]
    pub allow_pub_mod:   Vec<String>,
    #[serde(default)]
    pub allow_pub_items: Vec<String>,
}

#[derive(Debug)]
pub struct LoadedConfig {
    pub config:      VisibilityConfig,
    pub root:        PathBuf,
    pub fingerprint: String,
}

pub fn load_config(
    manifest_dir: &Path,
    workspace_root: &Path,
    explicit: Option<&Path>,
) -> Result<LoadedConfig> {
    let candidates = explicit.map_or_else(
        || {
            let mut result = Vec::new();
            for root in [manifest_dir, workspace_root] {
                result.push(root.join("mend.toml"));
            }
            result
        },
        |path| vec![path.to_path_buf()],
    );

    for path in candidates {
        if path.exists() {
            let text = fs::read_to_string(&path)
                .with_context(|| format!("failed to read config {}", path.display()))?;
            let file: ConfigFile = toml::from_str(&text)
                .with_context(|| format!("failed to parse config {}", path.display()))?;
            let root = path
                .parent()
                .map_or_else(|| manifest_dir.to_path_buf(), Path::to_path_buf)
                .canonicalize()
                .with_context(|| {
                    format!("failed to canonicalize config root for {}", path.display())
                })?;
            return Ok(LoadedConfig {
                fingerprint: fingerprint_for(&root, &file.visibility)?,
                config: file.visibility,
                root,
            });
        }
    }

    Ok(LoadedConfig {
        fingerprint: fingerprint_for(manifest_dir, &VisibilityConfig::default())?,
        config:      VisibilityConfig::default(),
        root:        manifest_dir.to_path_buf(),
    })
}

fn fingerprint_for(root: &Path, config: &VisibilityConfig) -> Result<String> {
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    root.to_string_lossy().hash(&mut hasher);
    serde_json::to_string(config)
        .context("failed to serialize mend config for fingerprinting")?
        .hash(&mut hasher);
    Ok(format!("{:016x}", hasher.finish()))
}