///|
fn json_integer(json : Json) -> Int raise XdrError {
check_json_integral(json)
@json.from_json(json) catch {
_ => raise Invalid(0, "expected signed 32-bit JSON integer")
}
}
///|
fn json_input_at(
schema : Schema,
ty : Type,
json : Json,
budget : Budget,
depth : Int,
) -> Value raise XdrError {
budget.visit(depth, 0)
match (ty, json) {
(TRef(name), j) =>
json_input_at(schema, schema.lookup(name), j, budget, depth + 1)
(TVoid, Null) => VVoid
(TInt, j) => VInt(json_integer(j))
(TUInt, j) => {
check_json_integral(j)
let n : UInt = @json.from_json(j) catch {
_ => raise Invalid(0, "expected unsigned 32-bit JSON integer")
}
VUInt(n)
}
(THyper, String(s)) => VHyper(parse_signed_decimal(s))
(TUHyper, String(s)) => VUHyper(parse_unsigned_decimal(s))
(TBool, True) => VBool(true)
(TBool, False) => VBool(false)
(TFloat, Number(n, ..)) => {
if n.is_nan() || n.is_inf() || n.abs() > 3.4028234663852886e38 {
raise Invalid(0, "JSON float out of finite range")
}
VFloat(Float::from_double(n))
}
(TDouble, Number(n, ..)) => {
if n.is_nan() || n.is_inf() {
raise Invalid(0, "JSON double out of finite range")
}
VDouble(n)
}
(TString(_), String(s)) => VString(s)
(TOpaque(_, _), String(s)) => VOpaque(from_hex(s, limits=schema.limits))
(TArray(inner, bound, variable), Array(items)) => {
if items.length() > budget.left ||
(variable && items.length() > bound) ||
(!variable && items.length() != bound) {
raise Invalid(0, "JSON array length limit")
}
let out : Array[Value] = []
for item in items {
out.push(json_input_at(schema, inner, item, budget, depth + 1))
}
VArray(out)
}
(TOptional(_), Null) => VOptional(None)
(TOptional(inner), Object(object)) => {
if object.length() != 1 {
raise Invalid(0, "optional JSON requires one some member")
}
let item = match object.get("some") {
Some(j) => j
None => raise Invalid(0, "missing optional some member")
}
VOptional(Some(json_input_at(schema, inner, item, budget, depth + 1)))
}
(TEnum(labels), String(name)) => {
for pair in labels {
if pair.0 == name {
return VEnum(pair.1)
}
}
raise Invalid(0, "unknown enum name")
}
(TEnum(_), j) => VEnum(json_integer(j))
(TStruct(fields), Object(object)) => {
if object.length() != fields.length() {
raise Invalid(0, "JSON record has missing or extra members")
}
let out : Array[(String, Value)] = []
for field in fields {
let item = match object.get(field.name) {
Some(j) => j
None => raise Invalid(0, "missing member: " + field.name)
}
out.push(
(field.name, json_input_at(schema, field.ty, item, budget, depth + 1)),
)
}
VStruct(out)
}
(TUnion(tag, arms, fallback), Object(object)) => {
if object.length() != 2 {
raise Invalid(0, "union JSON requires tag and value")
}
let n = match object.get("tag") {
Some(j) => json_integer(j)
None => raise Invalid(0, "missing union tag")
}
validate_tag(schema.resolve(tag), n)
let item = match object.get("value") {
Some(j) => j
None => raise Invalid(0, "missing union value")
}
VUnion(
n,
json_input_at(
schema,
union_arm(arms, fallback, n),
item,
budget,
depth + 1,
),
)
}
_ => raise Invalid(0, "JSON shape does not match XDR type")
}
}
///|
/// Local JSON convention, not an RFC-defined JSON wire format.
pub fn from_json(
schema : Schema,
type_name : String,
json : Json,
) -> Value raise XdrError {
let budget : Budget = {
left: schema.limits.max_items,
limits: schema.limits,
}
let value = json_input_at(schema, schema.lookup(type_name), json, budget, 0)
// Reuse exact wire checks for bounds, ASCII, enum membership and fixed byte counts.
ignore(encode(schema, type_name, value))
value
}
///|
fn check_json_integral(json : Json) -> Unit raise XdrError {
match json {
Number(n, repr~) => {
if n.is_nan() || n.is_inf() || n.floor() != n {
raise Invalid(0, "expected integral JSON number")
}
// Plain decimal integer lexemes prevent a rounded fractional input becoming valid.
if repr is Some(text) {
if parse_signed_decimal(text).to_double() != n {
raise Invalid(0, "inexact JSON integer")
}
}
}
_ => raise Invalid(0, "expected JSON number")
}
}