///|
/// Execute the command-line protocol without touching process-global state.
/// This small pure boundary keeps the CLI testable and lets another host embed
/// the same commands without copying argument handling.
pub fn run_cli(args : Array[String]) -> (Bool, String) {
let offset = cli_program_offset(args)
if args.length() <= offset {
return (true, cli_help())
}
let command = args[offset]
if command == "help" || command == "--help" || command == "-h" {
return (true, cli_help())
}
if command == "version" || command == "--version" {
return (true, "moonbit-json-jcs " + library_version())
}
let json_mode = (command == "inspect" || command == "validate") &&
args.length() > offset + 1 &&
args[offset + 1] == "--json"
let input_start = if json_mode { offset + 2 } else { offset + 1 }
let input = join_cli_args(args, input_start)
if input == "" {
return (
false, "missing JSON input; pass one JSON document after the command",
)
}
match command {
"canonicalize" => cli_canonicalize(input)
"validate" => cli_report(input, json_mode)
"inspect" => cli_report(input, json_mode)
"digest" => cli_digest(input)
"diff" => cli_diff(args, offset)
"pretty" => cli_pretty(input)
"tokens" => cli_tokens(input)
"diagnose" => cli_diagnose(input)
"profile" => cli_profile(input)
"compare" => cli_compare(args, offset)
_ => (false, "unknown command '" + command + "'; try 'help'")
}
}
///|
/// `moon run` exposes the generated executable path as argv[0]. Native
/// launchers do the same, while library callers normally pass only commands.
fn cli_program_offset(args : Array[String]) -> Int {
if args.length() == 0 {
0
} else if args[0].has_suffix(".wasm") ||
args[0].has_suffix(".exe") ||
args[0].has_suffix(".js") {
1
} else {
0
}
}
///|
pub fn cli_help() -> String {
"moonbit-json-jcs - RFC 8785 JSON Canonicalization Scheme\n\n" +
"Usage:\n" +
" moonbit-json-jcs canonicalize ''\n" +
" moonbit-json-jcs validate ''\n" +
" moonbit-json-jcs inspect ''\n" +
" moonbit-json-jcs digest ''\n" +
" moonbit-json-jcs diff '' ''\n\n" +
"Commands:\n" +
" canonicalize print deterministic RFC 8785 JSON\n" +
" validate print validity and parser diagnostics\n" +
" inspect print shape metrics and canonical digest\n" +
" digest print SHA-256 of canonical UTF-8 bytes\n" +
" diff compare two documents by JSON Pointer paths\n" +
" pretty print an indented deterministic representation\n" +
" tokens print source token spans\n" +
" diagnose print semantic canonicalization issues\n" +
" profile print detailed audit metrics as JSON\n" +
" compare compare two documents and print a JSON report\n" +
" help print this message\n"
}
///|
fn join_cli_args(args : Array[String], start : Int) -> String {
let mut output = ""
let mut index = start
while index < args.length() {
if output != "" {
output = output + " "
}
output = output + args[index]
index = index + 1
}
output
}
///|
fn cli_canonicalize(input : String) -> (Bool, String) {
match parse(input) {
Err(error) => (false, error.message())
Ok(value) =>
match canonicalize(value) {
Ok(serialized) => (true, serialized)
Err(error) => (false, error.message())
}
}
}
///|
fn cli_report(input : String, json_mode : Bool) -> (Bool, String) {
let report = inspect_json(input)
let output = if json_mode { report.to_json() } else { report.to_text() }
(report.valid, output)
}
///|
fn cli_digest(input : String) -> (Bool, String) {
match digest_json(input) {
Ok(digest) => (true, digest)
Err(error) => (false, error)
}
}
///|
fn cli_diff(args : Array[String], offset : Int) -> (Bool, String) {
if args.length() < offset + 3 {
return (false, "diff requires two JSON documents")
}
match diff_documents(args[offset + 1], args[offset + 2]) {
Err(error) => (false, error)
Ok(differences) => {
if differences.length() == 0 {
return (true, "no differences")
}
let mut output = ""
for difference in differences {
if output != "" {
output = output + "\n"
}
output = output + difference.to_text()
}
(true, output)
}
}
}
///|
fn cli_pretty(input : String) -> (Bool, String) {
match pretty_document(input, 2) {
Ok(output) => (true, output)
Err(error) => (false, error)
}
}
///|
fn cli_tokens(input : String) -> (Bool, String) {
match tokenize(input) {
Ok(tokens) => (true, token_summary(tokens))
Err(error) => (false, error.message())
}
}
///|
fn cli_diagnose(input : String) -> (Bool, String) {
match diagnose_document(input) {
Ok(issues) => (true, issues_text(issues))
Err(error) => (false, error)
}
}
///|
fn cli_profile(input : String) -> (Bool, String) {
match profile_document(input) {
Ok(profile) => (true, profile.to_json())
Err(error) => (false, error)
}
}
///|
fn cli_compare(args : Array[String], offset : Int) -> (Bool, String) {
if args.length() < offset + 3 {
return (false, "compare requires two JSON documents")
}
let report = compare_documents(args[offset + 1], args[offset + 2])
(report.valid, report.to_json())
}