// JSON in, JSON out. The only place in the library that mentions `Json`.
//
// Three JSON objects are not objects:
//
// {"$link": "bafy..."} a CID
// {"$bytes": "AQID"} a byte string
// {"$type": "blob", ...} a blob reference (handled in blob.mbt, on top of
// the object this file produces)
//
// The first two are recognised by having that key AND NO OTHER. That rule is
// upstream's and it matters: a record is allowed to contain a genuine map whose
// only key happens to be `$link`, and treating it as a CID would corrupt it.
// The extra-key check is what keeps the two apart.
///|
fn of_json(json : Json, path : String) -> LexValue raise DecodeError {
match json {
Null => LexValue::Null
True => Bool(true)
False => Bool(false)
String(s) => Str(s)
Number(value, repr~) => Int(integer_of(value, repr, path))
Array(items) => {
let out = Array::new(capacity=items.length())
for i, item in items {
out.push(of_json(item, index_path(path, i)))
}
Arr(out)
}
Object(fields) => of_json_object(fields, path)
}
}
///|
fn of_json_object(
fields : Map[String, Json],
path : String,
) -> LexValue raise DecodeError {
if fields.length() == 1 {
if fields.get("$link") is Some(String(text)) {
let cid = @syntax.Cid::parse(text) catch {
e => raise DecodeError(path~, reason="invalid $link: \{e.reason()}")
}
return Link(cid)
}
if fields.get("$bytes") is Some(String(text)) {
guard base64_decode(text) is Some(bytes) else {
raise DecodeError(path~, reason="invalid $bytes: not base64")
}
return Bytes(bytes)
}
}
let out : Map[String, LexValue] = Map([])
for key, value in fields {
out[key] = of_json(value, field_path(path, key))
}
Obj(out)
}
///|
/// JSON numbers arrive as `Double`, which cannot hold every 53-bit integer's
/// identity through arithmetic -- so the original text is used when it is
/// available, and the `Double` only as a fallback.
///
/// A non-integer is an error rather than something that rounds. DAG-CBOR
/// forbids floats, so a value this accepted could not be written back.
fn integer_of(
value : Double,
repr : String?,
path : String,
) -> Int64 raise DecodeError {
if repr is Some(text) {
try {
return @string.parse_int64(text)
} catch {
_ => raise DecodeError(path~, reason="not an integer: \{text}")
}
}
guard value == value.trunc() && !value.is_inf() && value == value else {
raise DecodeError(path~, reason="not an integer: \{value}")
}
value.to_int64()
}
///|
fn to_json(value : LexValue) -> Json {
match value {
Null => Json::null()
Bool(b) => Json::boolean(b)
Int(i) =>
// `repr` carries the exact digits, so a 53-bit integer survives even
// though the `Double` beside it may not represent it exactly.
Json::number(i.to_double(), repr=i.to_string())
Str(s) => Json::string(s)
Bytes(b) => Json::object({ "$bytes": Json::string(base64_encode(b)) })
Link(cid) => Json::object({ "$link": Json::string(cid.to_string()) })
Arr(items) => Json::array(items.map(to_json))
Obj(fields) => {
let out : Map[String, Json] = Map([])
for key, item in fields {
out[key] = to_json(item)
}
Json::object(out)
}
}
}