///|
/// JSON string boundary shared by the Node CLI and browser demo.
/// Request: {command, old, new} / {command, document, patch} /
/// {command, base, ours, theirs} / {command, document, path}.
pub fn run_json(request_text : String) -> String {
let response : Map[String, Json] = Map([])
try {
if request_text.length() > 4000000 {
raise LimitExceeded("request exceeds 4000000 code units")
}
let request = @json.parse(request_text)
guard request is Object(o) else {
raise InvalidPatch("request must be an object")
}
let result = match required_string(o, "command") {
"diff" =>
patch_json(diff(input_document(o, "old"), input_document(o, "new")))
"apply" =>
apply(
input_document(o, "document"),
parse_patch(input_document(o, "patch")),
)
"merge" =>
merge_json(
merge(
input_document(o, "base"),
input_document(o, "ours"),
input_document(o, "theirs"),
),
)
"resolve" =>
resolved_json(
resolve(
input_document(o, "base"),
input_document(o, "ours"),
input_document(o, "theirs"),
parse_decisions(input_document(o, "decisions")),
),
)
"get" => get(input_document(o, "document"), required_string(o, "path"))
other => raise InvalidPatch("unknown command: " + other)
}
response["ok"] = Json::boolean(true)
response["result"] = result
} catch {
error => {
let (code, message) = explain_error(error)
response["ok"] = Json::boolean(false)
response["code"] = Json::string(code)
response["error"] = Json::string(message)
}
}
Json::object(response).stringify()
}
///|
// Text adapters encode documents as strings and parse them separately. This
// prevents malformed input from injecting fields into the request envelope.
fn input_document(o : Map[String, Json], field : String) -> Json raise {
match o.get(field + "_text") {
Some(String(text)) => @json.parse(text)
Some(_) => raise InvalidPatch(field + "_text must be a string")
None => required_field(o, field)
}
}
///|
fn explain_error(error : Error) -> (String, String) {
match error {
ConfigError::InvalidPointer(path) =>
("invalid_pointer", "Invalid JSON Pointer: " + path)
ConfigError::InvalidIndex(index) =>
("invalid_index", "Invalid or out-of-range array index: " + index)
ConfigError::MissingPath(path) =>
("missing_path", "Path does not exist: " + path)
ConfigError::InvalidPatch(message) => ("invalid_patch", message)
ConfigError::TestFailed(path) => ("test_failed", "Test failed at: " + path)
ConfigError::InvalidMove(message) => ("invalid_move", message)
ConfigError::LimitExceeded(message) => ("limit_exceeded", message)
ConfigError::InvalidResolution(message) => ("invalid_resolution", message)
_ => ("invalid_json", "Invalid JSON input: " + error.to_string())
}
}
///|
/// Extract without rounding large numbers in JavaScript.
pub fn result_json(response : String) -> String {
try {
guard @json.parse(response) is Object(o) else { return response }
required_field(o, "result").stringify(indent=2)
} catch {
_ => response
}
}
///|
/// Project a review's final value or guarded patch without JS numeric rounding.
pub fn result_field_json(response : String, field : String) -> String {
try {
guard @json.parse(response) is Object(o) else { return response }
guard required_field(o, "result") is Object(result) else { return response }
required_field(result, field).stringify(indent=2)
} catch {
_ => response
}
}
///|
/// Read display values from trusted engine output, preserving numeric text.
pub fn result_path_json(response : String, path : String) -> String {
try {
guard @json.parse(response) is Object(o) else { return response }
get_at(required_field(o, "result"), pointer_tokens(path), 0, path).stringify(
indent=2,
)
} catch {
_ => response
}
}