///|
fn json_object(
value : Json,
allowed : Array[String],
) -> Map[String, Json] raise BeliefError {
let object = match value {
Object(object) => object
_ => raise Invalid("expected JSON object")
}
for key in object.keys() {
let mut known = false
for candidate in allowed {
if key == candidate {
known = true
break
}
}
if !known {
raise Invalid("unknown JSON field")
}
}
object
}
///|
fn required(object : Map[String, Json], key : String) -> Json raise BeliefError {
match object.get(key) {
Some(value) => value
None => raise Invalid("missing required JSON field")
}
}
///|
fn json_string(value : Json) -> String raise BeliefError {
match value {
String(text) => text
_ => raise Invalid("expected JSON string")
}
}
///|
fn json_number(value : Json) -> Double raise BeliefError {
match value {
Number(number, ..) => {
if !finite(number) {
raise Invalid("JSON number must be finite")
}
number
}
_ => raise Invalid("expected JSON number")
}
}
///|
fn json_array(value : Json, maximum : Int32) -> Array[Json] raise BeliefError {
match value {
Array(values) => {
if values.length() > maximum {
raise Budget("JSON array exceeds shape budget")
}
values
}
_ => raise Invalid("expected JSON array")
}
}
///|
fn json_strings(
value : Json,
maximum : Int32,
) -> Array[String] raise BeliefError {
let values = json_array(value, maximum)
let result : Array[String] = []
for item in values {
result.push(json_string(item))
}
result
}
///|
fn bounded_parse(text : String) -> Json raise BeliefError {
if text.length() > 262144 {
raise Budget("JSON text exceeds 262144 code units")
}
// Bound nesting before the standard parser can allocate a recursive tree.
// Brackets inside quoted strings, including escaped quotes, do not count.
let mut depth = 0
let mut quoted = false
let mut escaped = false
for character in text {
if quoted {
if escaped {
escaped = false
} else if character == '\\' {
escaped = true
} else if character == '"' {
quoted = false
}
} else if character == '"' {
quoted = true
} else if character == '{' || character == '[' {
depth = depth + 1
if depth > 12 {
raise Budget("JSON nesting exceeds twelve containers")
}
} else if character == '}' || character == ']' {
depth = depth - 1
if depth < 0 {
raise Invalid("unbalanced JSON containers")
}
}
}
@json.parse(text) catch {
_ => raise Invalid("invalid JSON text or nesting depth")
}
}
///|
fn decode_rows(
f : Frame,
value : Json,
budget : Limits,
) -> Mass raise BeliefError {
let rows = json_array(value, budget.focals)
let entries : Array[(Int32, Double)] = []
for row in rows {
let object = json_object(row, ["subset", "mass"])
let labels = json_strings(required(object, "subset"), 16)
let mask = f.subset(labels)
let weight = json_number(required(object, "mass"))
entries.push((mask, weight))
}
mass(f, entries, budget~)
}
///|
fn encode_strings(values : Array[String]) -> Json {
Json::array(values.map(value => Json::string(value)))
}
///|
fn encode_rows(m : Mass) -> Json raise BeliefError {
let rows : Array[Json] = []
for (mask, weight) in m.focals {
rows.push(
Json::object({
"subset": encode_strings(m.domain.members(mask)),
"mass": Json::number(weight),
}),
)
}
Json::array(rows)
}
///|
pub fn mass_from_json(
value : Json,
budget~ : Limits = default_limits(),
) -> Mass raise BeliefError {
let object = json_object(value, ["schema", "frame", "masses"])
if json_string(required(object, "schema")) != "moonbelief.mass.v1" {
raise Invalid("unsupported mass schema")
}
let f = frame(json_strings(required(object, "frame"), 16))
decode_rows(f, required(object, "masses"), budget)
}
///|
pub fn parse_mass(
text : String,
budget~ : Limits = default_limits(),
) -> Mass raise BeliefError {
mass_from_json(bounded_parse(text), budget~)
}
///|
pub fn Mass::to_json(self : Mass) -> Json raise BeliefError {
Json::object({
"schema": Json::string("moonbelief.mass.v1"),
"frame": encode_strings(self.domain.labels()),
"masses": encode_rows(self),
})
}
///|
pub fn Mass::json_text(self : Mass) -> String raise BeliefError {
self.to_json().stringify()
}