///|
fn usage() -> String {
(
#|moon-lot-recall: offline food-lot recall impact tracing
#|
#|Usage:
#| moon run cmd/main --target js -- validate LEDGER.json
#| moon run cmd/main --target js -- analyze LEDGER.json RECALL.json [json|markdown|csv]
#| moon run cmd/main --target js -- actions LEDGER.json RECALL.json
#| moon run cmd/main --target js -- inventory LEDGER.json AS_OF
#| moon run cmd/main --target js -- upstream LEDGER.json LOT_ID AS_OF
#| moon run cmd/main --target js -- reconcile LEDGER.json COUNTS.csv AS_OF [json|markdown]
#| moon run cmd/main --target js -- consignees LEDGER.json RECALL.json [json|markdown|csv]
#| moon run cmd/main --target js -- responses LEDGER.json RECALL.json EVENTS.csv [json|markdown]
#| moon run cmd/main --target js -- delta LEDGER.json RECALL.json NEW_AS_OF [json|markdown]
#| moon run cmd/main --target js -- readiness LEDGER.json RECALL.json COUNTS.csv EVENTS.csv [json|markdown]
#| moon run cmd/main --target js -- import-csv LOTS.csv TRANSFORMATIONS.csv SHIPMENTS.csv OUT.json
#|
#|All timestamps are Unix seconds; all quantities are integer grams.
#|An impact path indicates possible exposure, not proven contamination.
#|
)
}
///|
fn fail_cli(message : String) -> Unit {
println("ERROR: \{message}")
@sys.exit(2)
}
///|
fn read_text(path : String) -> String? {
let text = @fs.read_file_to_string(path) catch {
error => {
fail_cli("cannot read \{path}: \{Repr(error)}")
return None
}
}
Some(text)
}
///|
fn read_ledger(path : String) -> @lib.Ledger? {
let text = match read_text(path) {
None => return None
Some(value) => value
}
match @lib.decode_ledger(text) {
Ok(ledger) => Some(ledger)
Err(error) => {
fail_cli("\{error.code}: \{error.message}")
None
}
}
}
///|
fn read_request(path : String) -> @lib.RecallRequest? {
let text = match read_text(path) {
None => return None
Some(value) => value
}
match @lib.decode_request(text) {
Ok(request) => Some(request)
Err(error) => {
fail_cli("\{error.code}: \{error.message}")
None
}
}
}
///|
fn parse_time(text : String) -> Int64? {
let value = @string.parse_int64(text) catch {
_ => {
fail_cli("AS_OF must be an integer Unix timestamp")
return None
}
}
Some(value)
}
///|
fn run_validate(args : Array[String]) -> Unit {
if args.length() != 2 {
fail_cli("validate requires LEDGER.json")
return
}
match read_ledger(args[1]) {
None => ()
Some(ledger) => {
let validation = @lib.validate_ledger(ledger)
println(@lib.render_validation(validation))
if !validation.valid {
@sys.exit(2)
}
}
}
}
///|
fn run_analyze(args : Array[String]) -> Unit {
if args.length() < 3 || args.length() > 4 {
fail_cli("analyze requires LEDGER.json RECALL.json [json|markdown|csv]")
return
}
let ledger = match read_ledger(args[1]) {
None => return
Some(value) => value
}
let request = match read_request(args[2]) {
None => return
Some(value) => value
}
let format = if args.length() == 4 { args[3] } else { "markdown" }
match @lib.analyze(ledger, request) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
}
Ok(report) =>
match format {
"json" => println(@lib.encode_report(report))
"markdown" => println(@lib.render_markdown(report))
"csv" => println(@lib.render_shipments_csv(report))
_ => fail_cli("format must be json, markdown, or csv")
}
}
}
///|
fn run_actions(args : Array[String]) -> Unit {
if args.length() != 3 {
fail_cli("actions requires LEDGER.json RECALL.json")
return
}
let ledger = match read_ledger(args[1]) {
None => return
Some(value) => value
}
let request = match read_request(args[2]) {
None => return
Some(value) => value
}
match @lib.plan_actions(ledger, request) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
}
Ok(plan) => println(@lib.render_actions_markdown(plan))
}
}
///|
fn run_inventory(args : Array[String]) -> Unit {
if args.length() != 3 {
fail_cli("inventory requires LEDGER.json AS_OF")
return
}
let ledger = match read_ledger(args[1]) {
None => return
Some(value) => value
}
let as_of = match parse_time(args[2]) {
None => return
Some(value) => value
}
match @lib.inventory_at(ledger, as_of) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
}
Ok(report) => println(@json.to_json(report).stringify(indent=2))
}
}
///|
fn run_upstream(args : Array[String]) -> Unit {
if args.length() != 4 {
fail_cli("upstream requires LEDGER.json LOT_ID AS_OF")
return
}
let ledger = match read_ledger(args[1]) {
None => return
Some(value) => value
}
let as_of = match parse_time(args[3]) {
None => return
Some(value) => value
}
match @lib.trace_upstream(ledger, args[2], as_of) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
}
Ok(report) => println(@lib.render_upstream_markdown(report))
}
}
///|
fn run_import_csv(args : Array[String]) -> Unit {
if args.length() != 5 {
fail_cli(
"import-csv requires LOTS.csv TRANSFORMATIONS.csv SHIPMENTS.csv OUT.json",
)
return
}
let lots = match read_text(args[1]) {
None => return
Some(value) => value
}
let transformations = match read_text(args[2]) {
None => return
Some(value) => value
}
let shipments = match read_text(args[3]) {
None => return
Some(value) => value
}
match @lib.import_csv(lots, transformations, shipments) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
}
Ok(ledger) => {
@fs.write_string_to_file(args[4], @lib.encode_ledger(ledger)) catch {
error => fail_cli("cannot write \{args[4]}: \{Repr(error)}")
}
println("Wrote validated ledger to \{args[4]}")
}
}
}
///|
fn run_reconcile(args : Array[String]) -> Unit {
if args.length() < 4 || args.length() > 5 {
fail_cli("reconcile requires LEDGER.json COUNTS.csv AS_OF [json|markdown]")
return
}
let ledger = match read_ledger(args[1]) {
None => return
Some(value) => value
}
let text = match read_text(args[2]) {
None => return
Some(value) => value
}
let cutoff = match parse_time(args[3]) {
None => return
Some(value) => value
}
let format = if args.length() == 5 { args[4] } else { "markdown" }
let counts = match @lib.parse_stock_counts_csv(text) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
return
}
Ok(value) => value
}
match @lib.reconcile_stock(ledger, counts, cutoff) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
}
Ok(report) =>
match format {
"json" => println(@json.to_json(report).stringify(indent=2))
"markdown" => println(@lib.render_reconciliation_markdown(report))
_ => fail_cli("format must be json or markdown")
}
}
}
///|
fn run_consignees(args : Array[String]) -> Unit {
if args.length() < 3 || args.length() > 4 {
fail_cli("consignees requires LEDGER.json RECALL.json [json|markdown|csv]")
return
}
let ledger = match read_ledger(args[1]) {
None => return
Some(value) => value
}
let request = match read_request(args[2]) {
None => return
Some(value) => value
}
let format = if args.length() == 4 { args[3] } else { "markdown" }
match @lib.group_consignees(ledger, request) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
}
Ok(report) =>
match format {
"json" => println(@json.to_json(report).stringify(indent=2))
"markdown" => println(@lib.render_consignees_markdown(report))
"csv" => println(@lib.render_consignees_csv(report))
_ => fail_cli("format must be json, markdown, or csv")
}
}
}
///|
fn run_responses(args : Array[String]) -> Unit {
if args.length() < 4 || args.length() > 5 {
fail_cli(
"responses requires LEDGER.json RECALL.json EVENTS.csv [json|markdown]",
)
return
}
let ledger = match read_ledger(args[1]) {
None => return
Some(value) => value
}
let request = match read_request(args[2]) {
None => return
Some(value) => value
}
let source = match read_text(args[3]) {
None => return
Some(value) => value
}
let format = if args.length() == 5 { args[4] } else { "markdown" }
let events = match @lib.parse_response_events_csv(source) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
return
}
Ok(value) => value
}
match @lib.review_responses(ledger, request, events) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
}
Ok(report) =>
match format {
"json" => println(@json.to_json(report).stringify(indent=2))
"markdown" => println(@lib.render_responses_markdown(report))
_ => fail_cli("format must be json or markdown")
}
}
}
///|
fn run_delta(args : Array[String]) -> Unit {
if args.length() < 4 || args.length() > 5 {
fail_cli("delta requires LEDGER.json RECALL.json NEW_AS_OF [json|markdown]")
return
}
let ledger = match read_ledger(args[1]) {
None => return
Some(value) => value
}
let request = match read_request(args[2]) {
None => return
Some(value) => value
}
let cutoff = match parse_time(args[3]) {
None => return
Some(value) => value
}
let format = if args.length() == 5 { args[4] } else { "markdown" }
match @lib.compare_cutoffs(ledger, request, cutoff) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
}
Ok(delta) =>
match format {
"json" => println(@json.to_json(delta).stringify(indent=2))
"markdown" => println(@lib.render_delta_markdown(delta))
_ => fail_cli("format must be json or markdown")
}
}
}
///|
fn run_readiness(args : Array[String]) -> Unit {
if args.length() < 5 || args.length() > 6 {
fail_cli(
"readiness requires LEDGER.json RECALL.json COUNTS.csv EVENTS.csv [json|markdown]",
)
return
}
let ledger = match read_ledger(args[1]) {
None => return
Some(value) => value
}
let request = match read_request(args[2]) {
None => return
Some(value) => value
}
let count_text = match read_text(args[3]) {
None => return
Some(value) => value
}
let event_text = match read_text(args[4]) {
None => return
Some(value) => value
}
let counts = match @lib.parse_stock_counts_csv(count_text) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
return
}
Ok(value) => value
}
let events = match @lib.parse_response_events_csv(event_text) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
return
}
Ok(value) => value
}
let format = if args.length() == 6 { args[5] } else { "markdown" }
match @lib.review_readiness(ledger, request, counts, events) {
Err(validation) => {
println(@lib.render_validation(validation))
@sys.exit(2)
}
Ok(report) =>
match format {
"json" => println(@json.to_json(report).stringify(indent=2))
"markdown" => println(@lib.render_readiness_markdown(report))
_ => fail_cli("format must be json or markdown")
}
}
}
///|
fn main {
// The Node.js backend exposes [node, compiled-script, user-arguments...].
let raw = @env.args()
let args = raw[2:].to_owned()
if args.is_empty() || args[0] == "--help" || args[0] == "help" {
println(usage())
return
}
match args[0] {
"validate" => run_validate(args)
"analyze" => run_analyze(args)
"actions" => run_actions(args)
"inventory" => run_inventory(args)
"upstream" => run_upstream(args)
"reconcile" => run_reconcile(args)
"consignees" => run_consignees(args)
"responses" => run_responses(args)
"delta" => run_delta(args)
"readiness" => run_readiness(args)
"import-csv" => run_import_csv(args)
_ => {
println(usage())
@sys.exit(2)
}
}
}