boxology-cli-core 0.1.0

Reusable command-line boundary
Documentation
use boxology_cli_core::{GenerationPlan, PlanError, plan};
use boxology_contract::BoxId;
use boxology_manifest::RelativePath;
use boxology_workspace::{FileEntry, Workspace, WorkspaceInputs};

#[rustfmt::skip]
mod tests {
use super::*;

const ROOT_MANIFEST: &str = "schema = 1\nid = \"platform\"\nkind = \"platform\"\nowned = [\"Cargo.toml\", \"boxology.toml\"]\n\n[[derived]]\nid = \"lockfile\"\ngenerator = \"cargo\"\ninputs = [\"Cargo.toml\"]\noutputs = [\"Cargo.lock\"]\n";

fn implementation_crate(id: &str, suffix: &str, path: &str) -> String { format!("[[crates]]\ncargo_package = \"{id}-implementation{suffix}\"\npath = \"{path}\"\nrole = \"box-implementation\"\n") }

fn candidate_manifest_with(id: &str, generator: &str, implementation: bool, implementation_path: &str, extra_implementation: bool, import_ids: &[&str], duplicate: bool) -> String {
    let owned = match (implementation, extra_implementation) {
        (false, _) => "\"boxology.toml\", \"fixtures/**\"".to_owned(),
        (true, false) => format!("\"boxology.toml\", \"{implementation_path}/**\", \"fixtures/**\""),
        (true, true) => format!("\"boxology.toml\", \"{implementation_path}/**\", \"alternate/**\", \"fixtures/**\""),
    };
    let implementations = if implementation { let mut value = implementation_crate(id, "", implementation_path); if extra_implementation { value.push_str(&implementation_crate(id, "-alternate", "alternate")); } value } else { String::new() };
    let imports = import_ids.iter().map(|id| format!("[[imports]]\npackage = \"{id}\"\ncontract = \"{id}\"\n")).collect::<String>();
    let (outputs, second) = if duplicate { ("[\"generated/contract/**\"]", "[[derived]]\nid = \"second\"\ngenerator = \"boxology-contract\"\ninputs = [\"**\"]\noutputs = [\"generated/other/**\"]\n") } else { ("[\"generated/**\"]", "") };
    format!("schema = 1\nid = {id:?}\nkind = \"box\"\nowned = [{owned}]\n\n{implementations}{imports}[[crates]]\ncargo_package = \"{id}-contract\"\npath = \"generated/contract\"\nrole = \"box-contract\"\n\n[[derived]]\nid = \"contract\"\ngenerator = {generator:?}\ninputs = [\"**\"]\noutputs = {outputs}\n{second}")
}

fn candidate_manifest(id: &str, generator: &str, implementation: bool, implementation_path: &str, extra_implementation: bool, imports: bool, duplicate: bool) -> String {
    let import_ids = if imports { vec!["foreign"] } else { Vec::new() };
    candidate_manifest_with(id, generator, implementation, implementation_path, extra_implementation, &import_ids, duplicate)
}

const FOREIGN_MANIFEST: &str = "schema = 1\nid = \"foreign\"\nkind = \"box\"\nowned = [\"boxology.toml\", \"implementation/**\"]\n\n[[crates]]\ncargo_package = \"foreign-implementation\"\npath = \"implementation\"\nrole = \"box-implementation\"\n";

fn metadata_package(directory: &str, name: &str) -> String { let id = format!("path+file:///w/{directory}#0.0.0"); format!(r#"{{"id":{id:?},"name":{name:?},"manifest_path":"/w/{directory}/Cargo.toml","dependencies":[]}}"#) }

fn metadata(foreign: bool, implementation: bool, implementation_path: &str, extra_implementation: bool, second_candidate: bool) -> String {
    let mut members = vec![(String::from("ping/generated/contract"), String::from("ping-contract"))];
    if implementation { members.push((format!("ping/{implementation_path}"), String::from("ping-implementation"))); if extra_implementation { members.push((String::from("ping/alternate"), String::from("ping-implementation-alternate"))); } }
    if second_candidate { members.extend([(String::from("pong/generated/contract"), String::from("alpha-contract")), (String::from("pong/implementation"), String::from("alpha-implementation"))]); }
    if foreign { members.push((String::from("ping/nested/implementation"), String::from("foreign-implementation"))); }
    let ids = members.iter().map(|(directory, _)| format!("{:?}", format!("path+file:///w/{directory}#0.0.0"))).collect::<Vec<_>>().join(",");
    let packages = members.iter().map(|(directory, name)| metadata_package(directory, name)).collect::<Vec<_>>().join(",");
    format!(r#"{{"workspace_root":"/w","workspace_members":[{ids}],"packages":[{packages}]}}"#)
}

const SECOND_ID: &str = "alpha";
const SECOND_ROOT: &str = "pong";

struct WorkspaceConfig<'a> { generator: &'a str, implementation: bool, imports: bool, duplicate: bool, foreign: bool, second_candidate: bool, implementation_path: &'a str, extra_implementation: bool }

fn workspace_config<'a>(generator: &'a str, implementation: bool, imports: bool, duplicate: bool, foreign: bool) -> WorkspaceConfig<'a> { WorkspaceConfig { generator, implementation, imports, duplicate, foreign, second_candidate: false, implementation_path: "implementation", extra_implementation: false } }

fn workspace(generator: &str, implementation: bool, imports: bool, duplicate: bool, foreign: bool) -> Workspace { workspace_with(workspace_config(generator, implementation, imports, duplicate, foreign)) }

fn workspace_with(config: WorkspaceConfig<'_>) -> Workspace {
    let WorkspaceConfig { generator, implementation, imports, duplicate, foreign, second_candidate, implementation_path, extra_implementation } = config;
    let mut files: Vec<String> = ["Cargo.toml", "Cargo.lock", "boxology.toml", "ping/boxology.toml", "ping/fixtures/data.txt", "ping/generated/contract/Cargo.toml"].into_iter().map(String::from).collect();
    if implementation { files.extend([format!("ping/{implementation_path}/Cargo.toml"), format!("ping/{implementation_path}/src/lib.rs")]); if extra_implementation { files.extend([String::from("ping/alternate/Cargo.toml"), String::from("ping/alternate/src/lib.rs")]); } }
    let mut manifests = vec![(String::from("boxology.toml"), ROOT_MANIFEST.as_bytes().to_vec()), (String::from("ping/boxology.toml"), candidate_manifest("ping", generator, implementation, implementation_path, extra_implementation, imports, duplicate).into_bytes())];
    if duplicate { files.push(String::from("ping/generated/other/old.json")); }
    if second_candidate { files.extend([format!("{SECOND_ROOT}/boxology.toml"), format!("{SECOND_ROOT}/implementation/Cargo.toml"), format!("{SECOND_ROOT}/implementation/src/lib.rs"), format!("{SECOND_ROOT}/generated/contract/Cargo.toml")]); manifests.push((format!("{SECOND_ROOT}/boxology.toml"), candidate_manifest(SECOND_ID, generator, true, "implementation", false, false, false).into_bytes())); }
    if foreign { files.extend([String::from("ping/nested/boxology.toml"), String::from("ping/nested/implementation/Cargo.toml"), String::from("ping/nested/implementation/src/lib.rs")]); manifests.push((String::from("ping/nested/boxology.toml"), FOREIGN_MANIFEST.as_bytes().to_vec())); }
    let files = files.into_iter().map(|name| FileEntry::file(path(&name))).collect();
    let manifests = manifests.into_iter().map(|(name, bytes)| (path(&name), bytes)).collect();
    WorkspaceInputs::new(files, manifests, &metadata(foreign, implementation, implementation_path, extra_implementation, second_candidate)).unwrap().check().unwrap()
}

fn path(value: &str) -> RelativePath { RelativePath::new(value).unwrap() }
fn id(value: &str) -> BoxId { BoxId::new(value).unwrap() }

fn metadata_for(packages: &[&str]) -> String {
    let members = packages.iter().flat_map(|id| [(format!("{id}/generated/contract"), format!("{id}-contract")), (format!("{id}/implementation"), format!("{id}-implementation"))]).collect::<Vec<_>>();
    let ids = members.iter().map(|(directory, _)| format!("{:?}", format!("path+file:///w/{directory}#0.0.0"))).collect::<Vec<_>>().join(",");
    let packages = members.iter().map(|(directory, name)| metadata_package(directory, name)).collect::<Vec<_>>().join(",");
    format!(r#"{{"workspace_root":"/w","workspace_members":[{ids}],"packages":[{packages}]}}"#)
}

fn imported_workspace(imports: &[&str], targets: &[(&str, &str)]) -> Workspace {
    let mut files: Vec<String> = ["Cargo.toml", "Cargo.lock", "boxology.toml", "greeter/boxology.toml", "greeter/implementation/Cargo.toml", "greeter/implementation/src/lib.rs", "greeter/generated/contract/Cargo.toml"].into_iter().map(String::from).collect();
    let mut manifests = vec![(String::from("boxology.toml"), ROOT_MANIFEST.as_bytes().to_vec()), (String::from("greeter/boxology.toml"), candidate_manifest_with("greeter", "boxology-contract", true, "implementation", false, imports, false).into_bytes())];
    for (id, generator) in targets {
        files.extend([format!("{id}/boxology.toml"), format!("{id}/implementation/Cargo.toml"), format!("{id}/implementation/src/lib.rs"), format!("{id}/generated/contract/Cargo.toml")]);
        manifests.push((format!("{id}/boxology.toml"), candidate_manifest(id, generator, true, "implementation", false, false, false).into_bytes()));
    }
    let mut packages = vec!["greeter"];
    packages.extend(targets.iter().map(|(id, _)| *id));
    let files = files.into_iter().map(|name| FileEntry::file(path(&name))).collect();
    let manifests = manifests.into_iter().map(|(name, bytes)| (path(&name), bytes)).collect();
    WorkspaceInputs::new(files, manifests, &metadata_for(&packages)).unwrap().check().unwrap()
}

fn error_is(result: Result<Vec<GenerationPlan>, PlanError>, code: &str, at: &str, detail: &str) {
    let error = result.expect_err("planning must reject the unsatisfied fixture");
    assert_eq!(error.code(), code);
    assert_eq!(error.path().as_str(), at);
    assert_eq!(error.detail(), detail);
    assert_eq!(error.source(), "specs/s5-manifest-and-validation.md D5");
    assert_eq!(error.to_string(), format!("{code} {at:?}: {detail}"));
    assert_eq!(
        error.render_json(),
        format!(
            "{{\n  \"schema\": \"boxology.plan-error@1\",\n  \"code\": {code:?},\n  \"path\": {at:?},\n  \"detail\": {detail:?},\n  \"source\": \"specs/s5-manifest-and-validation.md D5\"\n}}\n"
        )
    );
}

fn input_names(plan: &GenerationPlan) -> Vec<&str> { plan.inputs().iter().map(RelativePath::as_str).collect() }
fn plan_ids(plans: &[GenerationPlan]) -> Vec<&str> { plans.iter().map(|plan| plan.package_id().as_str()).collect() }

#[test]
fn plan_resolves_import_by_identity_to_target_schema() {
    let workspace = imported_workspace(&["hello"], &[("hello", "boxology-contract")]);
    let plans = plan(&workspace, None).unwrap();
    let greeter = plans.iter().find(|plan| plan.package_id().as_str() == "greeter").unwrap();
    let [import] = greeter.imports() else { panic!("one resolved import is required") };
    assert_eq!(import.package().as_str(), "hello");
    assert_eq!(import.schema().as_str(), "hello/generated/schema.json");
    assert_eq!(greeter.schema_path().as_str(), "greeter/generated/schema.json");
}

#[test]
fn plan_orders_importers_after_their_import_targets() {
    // greeter < hello by package-id, so package-id order would emit the importer first.
    let workspace = imported_workspace(&["hello"], &[("hello", "boxology-contract")]);
    assert_eq!(plan_ids(&plan(&workspace, None).unwrap()), ["hello", "greeter"]);
}

#[test]
fn imports_must_resolve_to_generation_candidates() {
    error_is(plan(&imported_workspace(&["missing"], &[]), None), "BXW0084", "greeter/boxology.toml", "a declared import must name a discovered workspace package");
    error_is(plan(&imported_workspace(&["hello"], &[("hello", "cargo")]), None), "BXW0085", "greeter/boxology.toml", "an imported package must declare a contract-generation output");
}

#[test]
fn import_cycles_are_rejected() {
    let mut files: Vec<String> = ["Cargo.toml", "Cargo.lock", "boxology.toml"].into_iter().map(String::from).collect();
    let mut manifests = vec![(String::from("boxology.toml"), ROOT_MANIFEST.as_bytes().to_vec())];
    for (id, imports) in [("alpha", &["zulu"][..]), ("zulu", &["alpha"][..])] {
        files.extend([format!("{id}/boxology.toml"), format!("{id}/implementation/Cargo.toml"), format!("{id}/implementation/src/lib.rs"), format!("{id}/generated/contract/Cargo.toml")]);
        manifests.push((format!("{id}/boxology.toml"), candidate_manifest_with(id, "boxology-contract", true, "implementation", false, imports, false).into_bytes()));
    }
    let files = files.into_iter().map(|name| FileEntry::file(path(&name))).collect();
    let manifests = manifests.into_iter().map(|(name, bytes)| (path(&name), bytes)).collect();
    let workspace = WorkspaceInputs::new(files, manifests, &metadata_for(&["alpha", "zulu"])).unwrap().check().unwrap();
    error_is(plan(&workspace, None), "BXW0086", "alpha/boxology.toml", "generation candidates must not form an import cycle");
}

#[test]
fn plan_preserves_import_declaration_order() {
    let workspace = imported_workspace(&["zulu", "alpha"], &[("alpha", "boxology-contract"), ("zulu", "boxology-contract")]);
    let plans = plan(&workspace, None).unwrap();
    let greeter = plans.iter().find(|plan| plan.package_id().as_str() == "greeter").unwrap();
    assert_eq!(greeter.imports().iter().map(|import| import.package().as_str()).collect::<Vec<_>>(), ["zulu", "alpha"]);
}

#[test]
fn plan_error_json_escapes_a_hostile_manifest_path_exactly() {
    let root = "hostile-\"package";
    let files = [
        "Cargo.toml", "Cargo.lock", "boxology.toml", "boxology.toml",
        "implementation/Cargo.toml", "implementation/src/lib.rs",
        "generated/contract/Cargo.toml",
    ];
    let files = files.into_iter().enumerate().map(|(index, path)| {
        let name = if index < 3 { path.to_owned() } else { format!("{root}/{path}") };
        FileEntry::file(RelativePath::new(name).unwrap())
    }).collect();
    let manifests = vec![
        (path("boxology.toml"), ROOT_MANIFEST.as_bytes().to_vec()),
        (path(&format!("{root}/boxology.toml")), candidate_manifest("ping", "other-tool", true, "implementation", false, false, false).into_bytes()),
    ];
    let escaped = root.replace('"', "\\\"");
    let metadata = metadata(false, true, "implementation", false, false)
        .replace("/w/ping/", &format!("/w/{escaped}/"));
    let workspace = WorkspaceInputs::new(files, manifests, &metadata).unwrap().check().unwrap();
    let error = plan(&workspace, None).unwrap_err();
    assert_eq!(
        error.render_json(),
        "{\n  \"schema\": \"boxology.plan-error@1\",\n  \"code\": \"BXW0064\",\n  \"path\": \"hostile-\\\"package/boxology.toml\",\n  \"detail\": \"only the boxology-contract generator is supported by generate\",\n  \"source\": \"specs/s5-manifest-and-validation.md D5\"\n}\n"
    );
}

#[test]
fn plan_is_complete_sorted_and_excludes_cargo_and_foreign_or_derived_files() {
    let plans = plan(&workspace("boxology-contract", true, false, false, true), None).unwrap();
    assert_eq!(plans.len(), 1, "the platform cargo output is not a plan");
    let [plan] = plans.as_slice() else { panic!("one ping plan is required") };
    assert_eq!(plan.package_id().as_str(), "ping");
    assert_eq!(plan.manifest_path().as_str(), "ping/boxology.toml");
    assert_eq!(plan.package_root().map(RelativePath::as_str), Some("ping"));
    assert_eq!(plan.derived_output_id().as_str(), "contract");
    assert_eq!(plan.crate_root().as_str(), "implementation/src/lib.rs");
    assert_eq!(input_names(plan), ["boxology.toml", "fixtures/data.txt", "implementation/Cargo.toml", "implementation/src/lib.rs"]);
    assert_eq!(plan.outputs().iter().map(|pattern| pattern.as_str()).collect::<Vec<_>>(), ["generated/**"]);
}

#[test]
fn package_selection_is_live_and_deterministic() {
    let workspace = workspace_with(WorkspaceConfig { second_candidate: true, ..workspace_config("boxology-contract", true, false, false, false) });
    assert_eq!(plan_ids(&plan(&workspace, None).unwrap()), [SECOND_ID, "ping"]);
    assert_eq!(plan_ids(&plan(&workspace, Some(&id("ping"))).unwrap()), ["ping"]);
}

#[test]
fn planning_rejections_are_stable() {
    for (workspace, selection, code, at, detail) in [
        (workspace("other-tool", true, false, false, false), None, "BXW0064", "ping/boxology.toml", "only the boxology-contract generator is supported by generate"),
        (workspace("boxology-contract", true, false, false, false), Some(id("absent")), "BXW0065", "<request>", "the requested package must be a discovered workspace package"),
        (workspace("cargo", true, false, false, false), Some(id("ping")), "BXW0066", "ping/boxology.toml", "the selected package must declare a contract-generation output"),
        (workspace("boxology-contract", true, false, true, false), None, "BXW0069", "ping/boxology.toml", "a package must declare at most one contract-generation output"),
    ] { error_is(plan(&workspace, selection.as_ref()), code, at, detail); }
}

#[test]
fn implementation_crate_root_must_be_exactly_derivable() {
    for (implementation, path, extra) in [(false, "implementation", false), (true, "implementation", true)] {
        let config = WorkspaceConfig { implementation_path: path, extra_implementation: extra, ..workspace_config("boxology-contract", implementation, false, false, false) };
        error_is(plan(&workspace_with(config), None), "BXW0067", "ping/boxology.toml", "a generation candidate must declare exactly one box-implementation crate");
    }
    let config = WorkspaceConfig { implementation_path: "engine", ..workspace_config("boxology-contract", true, false, false, false) };
    let plans = plan(&workspace_with(config), None).unwrap();
    assert_eq!(plans[0].crate_root().as_str(), "engine/src/lib.rs");
}

#[test]
fn package_root_implementation_projects_to_src_lib() {
    let candidate = "schema = 1\nid = \"ping\"\nkind = \"box\"\nowned = [\"Cargo.toml\", \"boxology.toml\", \"src/**\"]\n\n[[crates]]\ncargo_package = \"ping-implementation\"\npath = \".\"\nrole = \"box-implementation\"\n\n[[crates]]\ncargo_package = \"ping-contract\"\npath = \"generated/contract\"\nrole = \"box-contract\"\n\n[[derived]]\nid = \"contract\"\ngenerator = \"boxology-contract\"\ninputs = [\"**\"]\noutputs = [\"generated/**\"]\n";
    let files = [
        "Cargo.toml", "Cargo.lock", "boxology.toml", "ping/boxology.toml",
        "ping/Cargo.toml", "ping/src/lib.rs", "ping/generated/contract/Cargo.toml",
    ].into_iter().map(|name| FileEntry::file(path(name))).collect();
    let manifests = vec![
        (path("boxology.toml"), ROOT_MANIFEST.as_bytes().to_vec()),
        (path("ping/boxology.toml"), candidate.as_bytes().to_vec()),
    ];
    let members = [("ping", "ping-implementation"), ("ping/generated/contract", "ping-contract")];
    let ids = members.iter().map(|(directory, _)| format!("{:?}", format!("path+file:///w/{directory}#0.0.0"))).collect::<Vec<_>>().join(",");
    let packages = members.iter().map(|(directory, name)| metadata_package(directory, name)).collect::<Vec<_>>().join(",");
    let metadata = format!(r#"{{"workspace_root":"/w","workspace_members":[{ids}],"packages":[{packages}]}}"#);
    let workspace = WorkspaceInputs::new(files, manifests, &metadata).unwrap().check().unwrap();
    let plans = plan(&workspace, None).unwrap();
    let [plan] = plans.as_slice() else { panic!("one plan is required") };
    assert_eq!(plan.crate_root().as_str(), "src/lib.rs");
    assert_eq!(input_names(plan), ["Cargo.toml", "boxology.toml", "src/lib.rs"]);
}
}