///|
pub fn help_text() -> String {
"MoonCutStock 0.1.0\n" +
"Commands: solve-json, solve-csv, normalize-json, normalize-csv, audit-json\n" +
"Options: --format=json|text --kerf=N --min-remnant=N --patterns=N --nodes=N\n" +
"CSV kerf/remnant metadata comes from options. audit-json takes {problem,plan}.\n" +
"Optimality is bounded: inspect status and completion flags.\n"
}
///|
priv struct Options {
format : String
kerf : Int32
min_remnant : Int32
limits : Limits
}
///|
fn command_options(args : Array[String]) -> Options raise CodecError {
if args.length() > 5 {
raise Invalid("at most five command options")
}
let mut format = "json"
let mut kerf = 0
let mut min_remnant = 1
let mut patterns = 50000
let mut nodes = 200000
let seen : Map[String, Bool] = Map([])
for arg in args {
let pair = arg.split("=").map(v => v.to_owned()).to_array()
if pair.length() != 2 {
raise Invalid("options must use --key=value")
}
if seen.contains(pair[0]) {
raise Invalid("duplicate command option")
}
seen[pair[0]] = true
match pair[0] {
"--format" => {
format = pair[1]
if format != "json" && format != "text" {
raise Invalid("format must be json or text")
}
}
"--kerf" => kerf = csv_int(pair[1])
"--min-remnant" => min_remnant = csv_int(pair[1])
"--patterns" => patterns = csv_int(pair[1])
"--nodes" => nodes = csv_int(pair[1])
_ => raise Invalid("unknown command option")
}
}
let limits = Limits::{ patterns, nodes, }
match validate_limits(limits) {
Ok(_) => ()
Err(e) => raise Invalid(e)
}
if kerf < 0 || min_remnant < 1 {
raise Invalid("invalid kerf or remnant threshold")
}
{ format, kerf, min_remnant, limits, }
}
///|
fn audit_bundle(input : String) -> String raise CodecError {
let f = codec_fields(parse_json(input), ["problem", "plan"])
let p = problem_json(required(f, "problem"))
let plan = plan_json(p, required(f, "plan"))
totals_json(audit(p, plan).unwrap()).stringify()
}
///|
fn execute(
command : String,
input : String,
args : Array[String],
) -> String raise CodecError {
if command == "help" {
return help_text()
}
let options = command_options(args)
if command == "audit-json" {
if args.length() > 0 {
raise Invalid("audit-json does not accept options")
}
return audit_bundle(input)
}
if !["solve-json", "solve-csv", "normalize-json", "normalize-csv"].contains(
command,
) {
raise Invalid("unknown command")
}
let csv = command == "solve-csv" || command == "normalize-csv"
if !csv &&
(
args.any(a => a.has_prefix("--kerf=")) ||
args.any(a => a.has_prefix("--min-remnant="))
) {
raise Invalid("JSON kerf and min_remnant must come from the document")
}
let p = if csv {
csv_problem(input, options.kerf, options.min_remnant)
} else {
problem_json(parse_json(input))
}
if command == "normalize-json" {
return encode_problem(p)
}
if command == "normalize-csv" {
return encode_csv(p)
}
let s = match solve(p, limits=options.limits) {
Ok(s) => s
Err(e) => raise Invalid(e)
}
let text = if options.format == "text" {
text_report(p, s)
} else {
encode_solution(p, s)
}
match text {
Ok(v) => v
Err(e) => raise Invalid(e)
}
}
///|
/// Pure command boundary for embedding in native, web, CI or editor hosts.
pub fn run_command(
command : String,
input : String,
args~ : Array[String] = [],
) -> Result[String, String] {
if input.length() > 65536 {
return Err("command input exceeds 65536 code units")
}
if command.length() > 32 || args.any(a => a.length() > 64) {
return Err("command arguments too long")
}
Ok(execute(command, input, args)) catch {
Invalid(e) => Err(e)
}
}