sim-nest 0.1.5

The SIM constellation umbrella facade: one crate that re-exports the runtime kernel, codecs, number domains, and libraries behind features.
Documentation
use std::{
    collections::{BTreeMap, BTreeSet},
    fs,
    path::{Path, PathBuf},
};

pub fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

pub fn collect_declared_features(cargo_toml: &str) -> BTreeSet<String> {
    let mut features = BTreeSet::new();
    let mut in_features = false;
    for line in cargo_toml.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') {
            in_features = trimmed == "[features]";
            continue;
        }
        if !in_features || trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        if let Some((name, _)) = trimmed.split_once('=') {
            let name = name.trim();
            if name != "default" {
                features.insert(name.to_owned());
            }
        }
    }
    features
}

pub fn collect_feature_dependencies(cargo_toml: &str) -> BTreeMap<String, BTreeSet<String>> {
    let mut features = BTreeMap::new();
    let mut in_features = false;
    let mut current: Option<String> = None;
    for line in cargo_toml.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') {
            in_features = trimmed == "[features]";
            current = None;
            continue;
        }
        if !in_features || trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        if let Some(name) = current.as_ref() {
            extract_quoted_items(trimmed, features.entry(name.clone()).or_default());
            if trimmed.contains(']') {
                current = None;
            }
            continue;
        }
        let Some((name, rhs)) = trimmed.split_once('=') else {
            continue;
        };
        let name = name.trim().to_owned();
        let deps = features.entry(name.clone()).or_default();
        extract_quoted_items(rhs, deps);
        if rhs.contains('[') && !rhs.contains(']') {
            current = Some(name);
        }
    }
    features
}

fn extract_quoted_items(text: &str, out: &mut BTreeSet<String>) {
    let mut rest = text;
    while let Some(start) = rest.find('"') {
        rest = &rest[start + 1..];
        let Some(end) = rest.find('"') else {
            break;
        };
        out.insert(rest[..end].to_owned());
        rest = &rest[end + 1..];
    }
}

pub fn collect_cfg_features(root: &Path) -> BTreeSet<String> {
    let mut features = BTreeSet::new();
    collect_cfg_features_in_dir(&root.join("src"), &mut features);
    collect_cfg_features_in_dir(&root.join("tests"), &mut features);
    features
}

fn collect_cfg_features_in_dir(dir: &Path, features: &mut BTreeSet<String>) {
    let Ok(entries) = fs::read_dir(dir) else {
        return;
    };
    for entry in entries.filter_map(Result::ok) {
        let path = entry.path();
        if path.is_dir() {
            collect_cfg_features_in_dir(&path, features);
            continue;
        }
        let Some(extension) = path.extension() else {
            continue;
        };
        if extension != "rs"
            && path.file_name().and_then(|name| name.to_str()) != Some("Cargo.toml")
        {
            continue;
        }
        let Ok(contents) = fs::read_to_string(&path) else {
            continue;
        };
        let pattern = "feature = \"";
        let mut rest = contents.as_str();
        while let Some(index) = rest.find(pattern) {
            let start = index + pattern.len();
            rest = &rest[start..];
            let Some(end) = rest.find('"') else {
                break;
            };
            features.insert(rest[..end].to_owned());
            rest = &rest[end + 1..];
        }
    }
}

pub fn assert_feature_includes(
    features: &BTreeMap<String, BTreeSet<String>>,
    feature: &str,
    expected: &[&str],
) {
    let actual = features
        .get(feature)
        .unwrap_or_else(|| panic!("missing feature {feature}"));
    let missing = expected
        .iter()
        .filter(|item| !actual.contains(**item))
        .copied()
        .collect::<Vec<_>>();
    assert!(
        missing.is_empty(),
        "feature {feature} is missing implications: {missing:?}"
    );
}

pub fn assert_crate_cargo_tomls_do_not_contain(root: &Path, prefix: &str, forbidden: &[&str]) {
    let crates_dir = root.join("crates");
    let entries = fs::read_dir(&crates_dir)
        .unwrap_or_else(|err| panic!("failed to read {}: {err}", crates_dir.display()));
    for entry in entries.filter_map(Result::ok) {
        let path = entry.path();
        let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
            continue;
        };
        if !name.starts_with(prefix) {
            continue;
        }
        let cargo_toml = path.join("Cargo.toml");
        let contents = fs::read_to_string(&cargo_toml)
            .unwrap_or_else(|err| panic!("failed to read {}: {err}", cargo_toml.display()));
        for forbidden_dep in forbidden {
            assert!(
                !contents.contains(forbidden_dep),
                "{} must not depend on {forbidden_dep}",
                cargo_toml.display()
            );
        }
    }
}