///|
/// Encode one Run record as a self-contained JSON line. JSON escaping preserves
/// tabs, control characters, and arbitrary UTF-8 payloads without a custom
/// delimiter ambiguity.
pub fn encode_run_record(record : SortRecord) -> String {
let (kind, key) = match record.key {
TextValue(value) => ("text", Json::string(value).stringify())
IntegerValue(value) => ("integer", value.to_string())
DecimalValue(value) =>
("decimal", Json::string(value.to_canonical_string()).stringify())
}
"{" +
"\"kind\":" +
Json::string(kind).stringify() +
"," +
"\"key\":" +
key +
"," +
"\"position\":" +
record.input_position.to_string() +
"," +
"\"payload\":" +
Json::string(record.payload).stringify() +
"}"
}
///|
/// Decode and validate one Run record.
pub fn decode_run_record(line : String) -> SortRecord raise SortError {
let value = @json.parse(line) catch {
error => raise MalformedRun("invalid JSON: " + error.to_string())
}
guard value is Object(fields) else {
raise MalformedRun("record must be a JSON object")
}
let kind = required_string(fields, "kind")
let key = match kind {
"text" => TextValue(required_string(fields, "key"))
"integer" => IntegerValue(required_int64(fields, "key"))
"decimal" =>
DecimalValue(
parse_decimal_key(required_string(fields, "key")) catch {
_ => raise MalformedRun("invalid canonical decimal key")
},
)
_ => raise MalformedRun("unknown key kind: " + kind)
}
let input_position = required_int64(fields, "position")
if input_position < 0L {
raise MalformedRun("position must not be negative")
}
{ key, input_position, payload: required_string(fields, "payload"), }
}
///|
fn required_string(
fields : Map[String, Json],
name : String,
) -> String raise SortError {
match fields.get(name) {
Some(String(value)) => value
Some(_) => raise MalformedRun(name + " must be a string")
None => raise MalformedRun("missing field: " + name)
}
}
///|
fn required_int64(
fields : Map[String, Json],
name : String,
) -> Int64 raise SortError {
match fields.get(name) {
Some(Number(number, repr~)) =>
match repr {
Some(text) =>
@string.parse_int64(text) catch {
_ => raise MalformedRun(name + " must be a signed 64-bit integer")
}
None => {
if number != number.floor() ||
number < -9007199254740991.0 ||
number > 9007199254740991.0 {
raise MalformedRun(name + " must be an exact integer")
}
number.to_int64()
}
}
Some(_) => raise MalformedRun(name + " must be an integer")
None => raise MalformedRun("missing field: " + name)
}
}