///|
fn kv3_pack(value : KV3Value) -> Json {
let (kind, payload) = match value {
KV3Null => ("null", Json::null())
KV3Bool(b) => ("bool", Json::boolean(b))
KV3Integer(s) => ("integer", Json::string(s))
KV3Float(s) => ("float", Json::string(s))
KV3String(s) => ("string", Json::string(s))
KV3Blob(bytes) => {
let output = StringBuilder()
for byte in bytes {
let digits = "0123456789abcdef"
output.write_char(digits[byte.to_int() / 16].to_char().unwrap())
output.write_char(digits[byte.to_int() % 16].to_char().unwrap())
}
("blob", Json::string(output.to_string()))
}
KV3Array(values) => ("array", Json::array(values.map(kv3_pack)))
KV3Object(entries) =>
(
"object",
Json::array(
entries.map(e => {
Json::object({
"key": Json::string(e.key),
"value": kv3_pack(e.value),
})
}),
),
)
KV3Flag(flag, value) =>
return Json::object({
"type": Json::string("flag"),
"flag": Json::string(flag),
"value": kv3_pack(value),
})
}
Json::object({ "type": Json::string(kind), "value": payload })
}
///|
/// Typed ordered JSON preserves integer precision, float spelling and flags.
pub fn kv3_to_json(document : KV3Document) -> Json raise {
stringify_kv3(document) |> ignore
Json::object({
"header": match document.header {
None => Json::null()
Some(h) =>
Json::object({
"format_name": Json::string(h.format_name),
"format_id": Json::string(h.format_id),
})
},
"root": kv3_pack(document.root),
})
}
///|
fn kv3_unpack(json : Json, depth : Int, count : Ref[Int]) -> KV3Value raise {
count.val += 1
if depth > 128 || count.val > 100000 {
raise InvalidDocument("KV3 JSON exceeds resource limit")
}
guard json is Object(fields) &&
fields.get("type") is Some(String(kind)) &&
fields.get("value") is Some(value) else {
raise InvalidDocument("Expected typed KV3 value")
}
if fields.length() != (if kind == "flag" { 3 } else { 2 }) {
raise InvalidDocument("Unexpected typed KV3 fields")
}
match (kind, value) {
("null", Null) => KV3Null
("bool", True) => KV3Bool(true)
("bool", False) => KV3Bool(false)
("string", String(s)) => KV3String(s)
("integer", String(s)) => {
guard kv3_number(s) is KV3Integer(_) else {
raise InvalidDocument("Invalid typed integer")
}
KV3Integer(s)
}
("float", String(s)) => {
guard kv3_number(s) is KV3Float(_) else {
raise InvalidDocument("Invalid typed float")
}
KV3Float(s)
}
("blob", String(s)) => {
let chars = s.to_array()
if chars.length() % 2 != 0 || chars.length() > 2097152 {
raise InvalidDocument("Invalid hexadecimal blob length")
}
let output = @buffer.Buffer()
let mut i = 0
while i < chars.length() {
guard kv3_hex(chars[i]) is Some(a) && kv3_hex(chars[i + 1]) is Some(b) else {
raise InvalidDocument("Invalid blob hex digit")
}
output.write_byte((a * 16 + b).to_byte())
i += 2
}
KV3Blob(output.to_bytes())
}
("array", Array(values)) =>
KV3Array(values.map(v => kv3_unpack(v, depth + 1, count)))
("object", Array(rows)) =>
KV3Object(
rows.map(row => {
guard row is Object(entry) &&
entry.length() == 2 &&
entry.get("key") is Some(String(key)) &&
entry.get("value") is Some(value) else {
raise InvalidDocument("Expected KV3 object entry")
}
{ key, value: kv3_unpack(value, depth + 1, count), }
}),
)
("flag", _) => {
guard fields.get("flag") is Some(String(flag)) && kv3_flag(flag) else {
raise InvalidDocument("Unknown KV3 flag")
}
let inner = kv3_unpack(value, depth + 1, count)
if inner is KV3Flag(_, _) {
raise InvalidDocument("Stacked flags are not supported")
}
KV3Flag(flag.to_lower(), inner)
}
_ => raise InvalidDocument("KV3 type and payload do not match")
}
}
///|
pub fn kv3_from_json(json : Json) -> KV3Document raise {
guard json is Object(fields) &&
fields.length() == 2 &&
fields.get("header") is Some(header) &&
fields.get("root") is Some(root) else {
raise InvalidDocument("Expected KV3 document JSON")
}
let header = match header {
Null => None
Object(h) if h.length() == 2 &&
h.get("format_name") is Some(String(format_name)) &&
h.get("format_id") is Some(String(format_id)) =>
Some(KV3Header::{ format_name, format_id, })
_ => raise InvalidDocument("Invalid KV3 JSON header")
}
let document = KV3Document::{
header,
root: kv3_unpack(root, 0, { val: 0, }),
}
stringify_kv3(document) |> ignore
document
}