///|
/// JSON helpers shared by the policy parser, the protocol layer and the
/// ledger. Two serializers are provided on purpose:
///
/// - `json_render` keeps insertion order and is used for wire messages, where
/// field order is irrelevant but readability helps debugging.
/// - `json_canonical` sorts object keys recursively and is used for digests
/// and for the hash chain, so semantically identical messages produce
/// identical bytes regardless of how the caller built them.
pub fn json_str(value : String) -> Json {
Json::string(value)
}
///|
pub fn json_int(value : Int32) -> Json {
Json::number(value.to_double())
}
///|
pub fn json_i64(value : Int64) -> Json {
Json::number(value.to_double())
}
///|
pub fn json_bool(value : Bool) -> Json {
if value {
Json::boolean(true)
} else {
Json::boolean(false)
}
}
///|
pub fn json_null() -> Json {
Json::null()
}
///|
pub fn json_obj(pairs : Array[(String, Json)]) -> Json {
let map : Map[String, Json] = Map([])
for pair in pairs {
map.set(pair.0, pair.1)
}
Json::object(map)
}
///|
pub fn json_arr(items : Array[Json]) -> Json {
Json::array(items)
}
///|
pub fn json_strings(items : Array[String]) -> Json {
let out : Array[Json] = []
for item in items {
out.push(Json::string(item))
}
Json::array(out)
}
///|
pub fn json_get(value : Json, key : String) -> Json? {
match value {
Object(fields) => fields.get(key)
_ => None
}
}
///|
pub fn json_as_string(value : Json) -> String? {
match value {
String(s) => Some(s)
_ => None
}
}
///|
pub fn json_as_int(value : Json) -> Int32? {
match value {
Number(n, ..) =>
if n == n.to_int().to_double() {
Some(n.to_int())
} else {
None
}
_ => None
}
}
///|
pub fn json_as_bool(value : Json) -> Bool? {
match value {
True => Some(true)
False => Some(false)
_ => None
}
}
///|
pub fn json_as_array(value : Json) -> Array[Json]? {
match value {
Array(items) => Some(items)
_ => None
}
}
///|
pub fn json_as_object(value : Json) -> Map[String, Json]? {
match value {
Object(fields) => Some(fields)
_ => None
}
}
///|
pub fn string_field(value : Json, key : String) -> String? {
match json_get(value, key) {
Some(field) => json_as_string(field)
None => None
}
}
///|
pub fn int_field(value : Json, key : String) -> Int32? {
match json_get(value, key) {
Some(field) => json_as_int(field)
None => None
}
}
///|
pub fn bool_field(value : Json, key : String, fallback~ : Bool = false) -> Bool {
match json_get(value, key) {
Some(field) =>
match json_as_bool(field) {
Some(flag) => flag
None => fallback
}
None => fallback
}
}
///|
pub fn string_array(value : Json) -> Array[String] {
let out : Array[String] = []
match value {
Array(items) =>
for item in items {
match item {
String(s) => out.push(s)
_ => ()
}
}
_ => ()
}
out
}
///|
fn escape_json_into(out : StringBuilder, text : String) -> Unit {
out.write_string("\"")
let len = text.length()
let mut start = 0
let mut i = 0
for ch in text {
let needs_escape = ch == '"' || ch == '\\' || ch.to_int() < 0x20
if needs_escape {
if i > start {
out.write_string(text[start:i].to_owned())
}
match ch {
'"' => out.write_string("\\\"")
'\\' => out.write_string("\\\\")
'\n' => out.write_string("\\n")
'\r' => out.write_string("\\r")
'\t' => out.write_string("\\t")
'\u0008' => out.write_string("\\b")
'\u000C' => out.write_string("\\f")
_ => {
out.write_string("\\u00")
out.write_string(hex_uint(ch.to_int().reinterpret_as_uint(), width=2))
}
}
start = i + 1
}
i += 1
}
if start < len {
out.write_string(text[start:len].to_owned())
}
out.write_string("\"")
}
///|
fn number_text(n : Double) -> String {
if n == n.floor() && n.abs() < 1.0e15 {
n.to_int64().to_string()
} else {
n.to_string()
}
}
///|
/// Code-unit ordering for canonical keys. MoonBit's `>` on `String` is not a
/// lexicographic comparison, so the serializer owns the order explicitly; the
/// same function therefore runs on every backend and canonical bytes are
/// reproducible.
fn string_less(left : String, right : String) -> Bool {
let limit = if left.length() < right.length() {
left.length()
} else {
right.length()
}
for i in 0.. Unit {
match value {
Null => out.write_string("null")
True => out.write_string("true")
False => out.write_string("false")
Number(n, ..) => out.write_string(number_text(n))
String(s) => escape_json_into(out, s)
Array(items) => {
out.write_string("[")
let mut first = true
for item in items {
if !first {
out.write_string(",")
}
first = false
render_into(out, item, canonical)
}
out.write_string("]")
}
Object(fields) => {
out.write_string("{")
let keys : Array[String] = []
for key, _ in fields {
keys.push(key)
}
if canonical {
// Insertion sort: object key sets here are tiny (protocol envelopes,
// policy rules, ledger entries) and this avoids a comparator import.
for i in 1.. 0 && string_less(keys[j], keys[j - 1]) {
let hold = keys[j - 1]
keys[j - 1] = keys[j]
keys[j] = hold
j -= 1
}
}
}
let mut first = true
for key in keys {
if !first {
out.write_string(",")
}
first = false
escape_json_into(out, key)
out.write_string(":")
match fields.get(key) {
Some(item) => render_into(out, item, canonical)
None => out.write_string("null")
}
}
out.write_string("}")
}
}
}
///|
pub fn json_render(value : Json) -> String {
let out = StringBuilder()
render_into(out, value, false)
out.to_string()
}
///|
pub fn json_canonical(value : Json) -> String {
let out = StringBuilder()
render_into(out, value, true)
out.to_string()
}
///|
/// Parse a JSON document, returning `None` instead of raising so protocol
/// callers can answer a malformed line with a JSON-RPC error.
pub fn json_parse_opt(text : String) -> Json? {
Some(@json.parse(text)) catch {
_ => None
}
}
///|
/// Depth of an object/array tree, used by the policy parser to reject
/// pathological nesting before the engine keeps it in memory.
pub fn json_depth(value : Json) -> Int32 {
match value {
Array(items) => {
let mut best = 0
for item in items {
let d = json_depth(item)
if d > best {
best = d
}
}
best + 1
}
Object(fields) => {
let mut best = 0
for _, item in fields {
let d = json_depth(item)
if d > best {
best = d
}
}
best + 1
}
_ => 1
}
}