crn-craft-cli 0.3.0

Command-line interface of CRN-craft: check, simulate, analyze and verify reaction-network JSON documents
#![forbid(unsafe_code)]
use crn_craft::{
    CheckReport, analyze_files, analyze_legacy_files, check_files, describe,
    diagnostic::Diagnostic, export_gillespy2_file, schema,
};
use serde_json::Value;
use std::{
    env,
    io::{self, Write},
    path::Path,
    process::ExitCode,
};

const HELP: &str = "crn-craft — CRN solvers and network analysis\n\nUsage:\n  crn-craft describe [model|run|result|result-v1|archive|archive-check|check|state|analysis|analysis-v1|export|fsp-result|tau-request|tau-result|ssa-run|ssa-result|rules-model|rules-check|rules-run|rules-result]\n  crn-craft check MODEL [RUN]\n  crn-craft analyze MODEL [STATE]\n  crn-craft analyze-v1 MODEL [STATE]\n  crn-craft export MODEL gillespy2\n  crn-craft simulate MODEL RUN NEW_DIRECTORY [--cancel-file PATH]\n  crn-craft verify DIRECTORY\n  crn-craft simulate-cached MODEL RUN NEW_DIRECTORY [--cancel-file PATH]\n  crn-craft fsp MODEL RUN [--cancel-file PATH]\n  crn-craft tau MODEL REQUEST [--cancel-file PATH]\n  crn-craft ssa MODEL REQUEST [--cancel-file PATH]\n  crn-craft rules-check RULES_MODEL\n  crn-craft rules RULES_MODEL REQUEST [--cancel-file PATH]\n  crn-craft --version\n\nAll data output is JSON. simulate exits 0 only for a completed ensemble.\nverify exits 0 for a valid archive, including an incomplete simulation.\nOther errors and incomplete simulations exit 1. Optional ODE uses crn-diffsol.\n";

fn emit(value: &Value) -> io::Result<()> {
    let mut out = io::stdout().lock();
    serde_json::to_writer_pretty(&mut out, value)?;
    out.write_all(b"\n")
}

/// FSP and tau results are checked against max_output_bytes in compact form,
/// so they are written compactly.
fn emit_compact(value: &Value) -> io::Result<()> {
    let mut out = io::stdout().lock();
    serde_json::to_writer(&mut out, value)?;
    out.write_all(b"\n")
}

fn main() -> ExitCode {
    let args: Vec<_> = env::args_os().skip(1).collect();
    if args.len() == 1 && (args[0] == "--help" || args[0] == "-h") {
        return match io::stdout().write_all(HELP.as_bytes()) {
            Ok(()) => ExitCode::SUCCESS,
            Err(_) => ExitCode::FAILURE,
        };
    }
    if args.len() == 1 && args[0] == "--version" {
        return match emit(
            &serde_json::json!({"version": env!("CARGO_PKG_VERSION"), "milestone": "SSA-acceleration"}),
        ) {
            Ok(()) => ExitCode::SUCCESS,
            Err(_) => ExitCode::FAILURE,
        };
    }
    let mut compact = false;
    let (value, success) = match args.first().and_then(|arg| arg.to_str()) {
        Some("describe") if args.len() == 1 => {
            // The native CLI adds the rules engine to the core description.
            let mut value = describe();
            value["schema"] = "crn-craft.describe/0.12".into();
            if let Some(commands) = value["commands"].as_array_mut() {
                commands.extend(["rules-check".into(), "rules".into()]);
            }
            value["dynamic_species"] = crn_craft_rules::describe();
            (value, true)
        }
        Some("describe") if args.len() == 2 => match args[1]
            .to_str()
            .and_then(|kind| schema(kind).or_else(|| crn_craft_rules::schema(kind)))
        {
            Some(value) => (value, true),
            None => usage_error(),
        },
        Some("check") if args.len() == 2 || args.len() == 3 => {
            let report = check_files(Path::new(&args[1]), args.get(2).map(Path::new));
            let success = report.valid;
            (
                serde_json::to_value(report).expect("finite check report"),
                success,
            )
        }
        Some("analyze") if args.len() == 2 || args.len() == 3 => {
            let report = analyze_files(Path::new(&args[1]), args.get(2).map(Path::new));
            let success = report.success;
            (
                serde_json::to_value(report).expect("finite analysis report"),
                success,
            )
        }
        Some("analyze-v1") if args.len() == 2 || args.len() == 3 => {
            let report = analyze_legacy_files(Path::new(&args[1]), args.get(2).map(Path::new));
            let success = report.success;
            (
                serde_json::to_value(report).expect("finite legacy report"),
                success,
            )
        }
        Some("export") if args.len() == 3 && args[2] == "gillespy2" => {
            let report = export_gillespy2_file(Path::new(&args[1]));
            let success = report.success;
            (
                serde_json::to_value(report).expect("finite export report"),
                success,
            )
        }
        Some("simulate" | "simulate-cached")
            if args.len() == 4 || (args.len() == 6 && args[4] == "--cancel-file") =>
        {
            let mut control = crn_craft::simulation::Control::default();
            control.cancel_file = args.get(5).map(std::path::PathBuf::from);
            let runner = if args[0] == "simulate-cached" {
                crn_craft::archive::simulate_cached_files
            } else {
                crn_craft::archive::simulate_files
            };
            let report = runner(
                Path::new(&args[1]),
                Path::new(&args[2]),
                Path::new(&args[3]),
                &control,
            );
            let success = report.valid && report.simulation_complete;
            (
                serde_json::to_value(report).expect("finite archive report"),
                success,
            )
        }
        Some("ssa") if args.len() == 3 || (args.len() == 5 && args[3] == "--cancel-file") => {
            let mut control = crn_craft::simulation::Control::default();
            control.cancel_file = args.get(4).map(std::path::PathBuf::from);
            match crn_craft::ssa::solve_files(Path::new(&args[1]), Path::new(&args[2]), &control) {
                Ok(result) => {
                    let complete = result.termination == crn_craft::ssa::Termination::Completed;
                    compact = true;
                    (serde_json::to_value(result).unwrap(), complete)
                }
                Err(errors) => (
                    serde_json::json!({"success":false,"diagnostics":errors}),
                    false,
                ),
            }
        }
        Some("rules-check") if args.len() == 2 => {
            let report = crn_craft_rules::check_model_file(Path::new(&args[1]));
            let success = report.valid;
            (
                serde_json::to_value(report).expect("finite rules check report"),
                success,
            )
        }
        Some("rules") if args.len() == 3 || (args.len() == 5 && args[3] == "--cancel-file") => {
            let mut control = crn_craft_rules::Control::default();
            control.cancel_file = args.get(4).map(std::path::PathBuf::from);
            match crn_craft_rules::solve_files(Path::new(&args[1]), Path::new(&args[2]), &control) {
                Ok(result) => {
                    let complete = result.termination == crn_craft_rules::Termination::Completed;
                    compact = true;
                    (serde_json::to_value(result).unwrap(), complete)
                }
                Err(errors) => (
                    serde_json::json!({"success":false,"diagnostics":errors}),
                    false,
                ),
            }
        }
        Some("tau") if args.len() == 3 || (args.len() == 5 && args[3] == "--cancel-file") => {
            let mut control = crn_craft::simulation::Control::default();
            control.cancel_file = args.get(4).map(std::path::PathBuf::from);
            match crn_craft::tau::solve_files(Path::new(&args[1]), Path::new(&args[2]), &control) {
                Ok(result) => {
                    let complete = result.termination == crn_craft::simulation::Stop::Completed;
                    compact = true;
                    (serde_json::to_value(result).unwrap(), complete)
                }
                Err(errors) => (
                    serde_json::json!({"success":false,"diagnostics":errors}),
                    false,
                ),
            }
        }
        Some("fsp") if args.len() == 3 || (args.len() == 5 && args[3] == "--cancel-file") => {
            let mut control = crn_craft::simulation::Control::default();
            control.cancel_file = args.get(4).map(std::path::PathBuf::from);
            let check = check_files(Path::new(&args[1]), Some(Path::new(&args[2])));
            if !check.valid {
                (serde_json::to_value(check).unwrap(), false)
            } else {
                match (std::fs::read(&args[1]), std::fs::read(&args[2])) {
                    (Ok(m), Ok(r)) => match crn_craft::fsp::solve(&m, &r, &control) {
                        Ok(result) => {
                            compact = true;
                            (serde_json::to_value(result).unwrap(), true)
                        }
                        Err(errors) => (
                            serde_json::json!({"success":false,"diagnostics":errors}),
                            false,
                        ),
                    },
                    _ => usage_error(),
                }
            }
        }
        Some("verify") if args.len() == 2 => {
            let report = crn_craft::archive::verify_archive(Path::new(&args[1]));
            let success = report.valid;
            (
                serde_json::to_value(report).expect("finite archive report"),
                success,
            )
        }
        _ => usage_error(),
    };
    let written = if compact {
        emit_compact(&value)
    } else {
        emit(&value)
    };
    match written {
        Ok(()) if success => ExitCode::SUCCESS,
        Ok(()) => ExitCode::FAILURE,
        Err(error) => {
            eprintln!("CRN-E030: cannot write JSON output: {error}");
            ExitCode::FAILURE
        }
    }
}

fn usage_error() -> (Value, bool) {
    let report = CheckReport::from_error(Diagnostic::error(
        "CRN-E031",
        "",
        "unknown command or wrong argument count",
        "Use --help. Supported: describe, check, analyze, analyze-v1, export, simulate, simulate-cached, fsp, tau and verify.",
    ));
    (
        serde_json::to_value(report).expect("finite usage report"),
        false,
    )
}