knave 0.14.1

A Lisp programming language for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use super::build_plan::BuildPlan;
use crate::language::Language;
use crate::project::CrateDependency;

pub struct CargoManifest<'p> {
    kind: CargoTargetKind,
    target_name: String,
    dependencies: &'p [CrateDependency],
    dev_dependencies: &'p [CrateDependency],
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CargoTargetKind {
    Bin,
    Test,
}

struct TargetShape {
    flag: &'static str,
    table: &'static str,
    harness: &'static str,
    name_part: &'static str,
}

impl CargoTargetKind {
    const BIN: TargetShape = TargetShape {
        flag: "--bin",
        table: "[[bin]]",
        harness: "",
        name_part: "",
    };
    const TEST: TargetShape = TargetShape {
        flag: "--test",
        table: "[[test]]",
        harness: "harness = false\n",
        name_part: "-tests",
    };

    pub fn target_name(self, name_stem: &str, root_hash: &str) -> String {
        format!("{name_stem}{}-{root_hash}", self.shape().name_part)
    }

    fn shape(self) -> &'static TargetShape {
        match self {
            Self::Bin => &Self::BIN,
            Self::Test => &Self::TEST,
        }
    }
}

impl<'p> CargoManifest<'p> {
    pub fn new(
        kind: CargoTargetKind,
        target_name: String,
        dependencies: &'p [CrateDependency],
        dev_dependencies: &'p [CrateDependency],
    ) -> Self {
        Self {
            kind,
            target_name,
            dependencies,
            dev_dependencies,
        }
    }

    pub fn target_arguments(&self) -> [&str; 2] {
        [self.kind.shape().flag, &self.target_name]
    }

    pub fn names_target(&self, name: &str) -> bool {
        self.target_name == name
    }
}

impl CargoManifest<'_> {
    const PACKAGE_SUFFIX: &'static str = "-program";
    const SOURCE_PATH: &'static str = "src/main.rs";

    pub fn package_name() -> String {
        format!("{}{}", Language::NAME, Self::PACKAGE_SUFFIX)
    }

    pub fn render(&self, plan: &BuildPlan) -> String {
        let mut text = format!(
            "[package]\nname = {}\nversion = \"0.1.0\"\nedition = {}\npublish = false\n\
             autobins = false\nautotests = false\nautoexamples = false\nautobenches = false\n\n\
             {}\nname = {}\npath = {}\n{}\n[workspace]\n\n{}\n",
            Self::quoted(&Self::package_name()),
            Self::quoted(BuildPlan::EDITION),
            self.kind.shape().table,
            Self::quoted(&self.target_name),
            Self::quoted(Self::SOURCE_PATH),
            self.kind.shape().harness,
            plan.manifest_profile(),
        );
        Self::write_section(&mut text, "dependencies", self.dependencies);
        Self::write_section(&mut text, "dev-dependencies", self.dev_dependencies);
        text
    }

    fn write_section(text: &mut String, section: &str, crates: &[CrateDependency]) {
        if crates.is_empty() {
            return;
        }
        text.push_str(&format!("[{section}]\n"));
        crates
            .iter()
            .for_each(|dependency| text.push_str(&Self::entry(dependency)));
        text.push('\n');
    }

    fn entry(dependency: &CrateDependency) -> String {
        let mut fields = Vec::new();
        if let Some(version) = &dependency.version {
            fields.push(format!("version = {}", Self::quoted(version)));
        }
        if let Some(path) = &dependency.path {
            fields.push(format!(
                "path = {}",
                Self::quoted(&path.display().to_string())
            ));
        }
        if !dependency.features.is_empty() {
            let features: Vec<String> = dependency
                .features
                .iter()
                .map(|feature| Self::quoted(feature))
                .collect();
            fields.push(format!("features = [{}]", features.join(", ")));
        }
        format!(
            "{} = {{ {} }}\n",
            Self::quoted(&dependency.name),
            fields.join(", ")
        )
    }

    fn quoted(text: &str) -> String {
        let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
        format!("\"{escaped}\"")
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::{CargoManifest, CargoTargetKind};
    use crate::build::{BuildPlan, BuildProfile};
    use crate::project::CrateDependency;
    use crate::source::{SourceMap, Span};

    fn dependency(
        name: &str,
        version: Option<&str>,
        features: &[&str],
        path: Option<&str>,
    ) -> CrateDependency {
        let mut sources = SourceMap::default();
        let source = sources.add("project", "(fake)");
        CrateDependency {
            name: name.to_string(),
            name_span: Span::new(source, 1..5),
            version: version.map(str::to_string),
            features: features.iter().map(|feature| feature.to_string()).collect(),
            path: path.map(PathBuf::from),
        }
    }

    #[test]
    fn the_manifest_writes_each_field_of_an_entry() {
        let dependencies = [
            dependency("fake-json", Some("1.2"), &["derive", "std"], None),
            dependency("fake_local", None, &[], Some("/fixtures/fake \"local\"")),
        ];
        let dev_dependencies = [dependency("fake-check", Some("0.3"), &[], None)];
        let manifest = CargoManifest::new(
            CargoTargetKind::Bin,
            "demo-00000000".to_string(),
            &dependencies,
            &dev_dependencies,
        );
        let text = manifest.render(&BuildPlan::new(BuildProfile::Debug));
        assert!(text.contains("[dependencies]\n\"fake-json\" = { version = \"1.2\", features = [\"derive\", \"std\"] }\n"));
        assert!(text.contains("\"fake_local\" = { path = \"/fixtures/fake \\\"local\\\"\" }\n"));
        assert!(text.contains("[dev-dependencies]\n\"fake-check\" = { version = \"0.3\" }\n"));
        assert!(text.contains("[[bin]]\nname = \"demo-00000000\"\n"));
    }

    #[test]
    fn the_manifest_leaves_out_an_empty_section() {
        let manifest =
            CargoManifest::new(CargoTargetKind::Bin, "demo-00000000".to_string(), &[], &[]);
        let text = manifest.render(&BuildPlan::new(BuildProfile::Release));
        assert!(!text.contains("dependencies]"));
        assert!(text.contains("[profile.dev]\ndebug = false\n"));
    }

    #[test]
    fn a_target_name_joins_the_project_and_the_root_hash() {
        assert_eq!(
            CargoTargetKind::Bin.target_name("demo", "0000abcd"),
            "demo-0000abcd"
        );
        assert_eq!(
            CargoTargetKind::Test.target_name("demo", "0000abcd"),
            "demo-tests-0000abcd"
        );
    }

    #[test]
    fn a_test_target_runs_its_own_main() {
        let manifest = CargoManifest::new(
            CargoTargetKind::Test,
            "demo-tests-00000000".to_string(),
            &[],
            &[],
        );
        let text = manifest.render(&BuildPlan::new(BuildProfile::Debug));
        assert!(text.contains(
            "[[test]]\nname = \"demo-tests-00000000\"\npath = \"src/main.rs\"\nharness = false\n\n[workspace]\n"
        ));
        assert!(!text.contains("[[bin]]"));
    }
}