///|
fn object_fields(
value : Json,
keys : Array[String],
) -> Map[String, Json] raise DesignError {
let obj = match value {
Object(x) => x
_ => raise Invalid("JSON object required")
}
if obj.length() != keys.length() {
raise Invalid("JSON object schema mismatch")
}
for key in keys {
if obj.get(key) is None {
raise Invalid("JSON required field missing")
}
}
obj
}
///|
fn json_field(obj : Map[String, Json], name : String) -> Json raise DesignError {
match obj.get(name) {
Some(x) => x
None => raise Invalid("missing JSON field")
}
}
///|
fn json_string(value : Json) -> String raise DesignError {
match value {
String(x) => x
_ => raise Invalid("JSON string required")
}
}
///|
fn json_number(value : Json) -> Double raise DesignError {
let x = match value {
Number(n, ..) => n
_ => raise Invalid("JSON number required")
}
require_number(x)
x
}
///|
fn json_array(value : Json, limit : Int32) -> Array[Json] raise DesignError {
let a = match value {
Array(x) => x
_ => raise Invalid("JSON array required")
}
if a.length() > limit {
raise Budget("JSON array exceeds schema budget")
}
a
}
///|
pub fn plan_json(p : Plan) -> String raise DesignError {
validate_plan(p)
let fs : Array[Json] = [
for f in p.factors => {
Json::object({
"name": Json(f.name),
"low": Json(f.low),
"high": Json(f.high),
})
}
]
let rs : Array[Json] = [
for r in p.runs => {
Json::object({
"id": Json(r.id),
"block": Json(r.block),
"point": Json(r.point),
})
}
]
Json::object({
"schema": Json("moondoe/1"),
"factors": Json::array(fs),
"runs": Json::array(rs),
}).stringify()
}
///|
pub fn plan_from_json(text : String) -> Plan raise DesignError {
if text.length() > 2000000 {
raise Budget("JSON exceeds two million UTF-16 units")
}
check_json_depth(text)
let value = @json.parse(text) catch {
_ => raise Invalid("invalid or deeply nested JSON")
}
let root = object_fields(value, ["schema", "factors", "runs"])
if json_string(json_field(root, "schema")) != "moondoe/1" {
raise Invalid("unsupported plan schema")
}
let fs = []
for row in json_array(json_field(root, "factors"), 16) {
let obj = object_fields(row, ["name", "low", "high"])
fs.push(
factor(
json_string(json_field(obj, "name")),
json_number(json_field(obj, "low")),
json_number(json_field(obj, "high")),
),
)
}
let rs = []
for row in json_array(json_field(root, "runs"), 4096) {
let obj = object_fields(row, ["id", "block", "point"])
let block = json_number(json_field(obj, "block"))
if block < 0.0 ||
block > 2147483647.0 ||
block != block.to_int().to_double() {
raise Invalid("block must be nonnegative Int")
}
let point = [
for x in json_array(json_field(obj, "point"), 16) => json_number(x)
]
rs.push({
id: json_string(json_field(obj, "id")),
block: block.to_int(),
point,
})
}
plan(fs, rs)
}
///|
pub fn diagnostics_json(
p : Plan,
model : Array[Term],
) -> String raise DesignError {
let d = diagnose(p, model)
let aliases : Array[Json] = [
for pair in d.exact_aliases => {
Json::object({
"left": Json(pair.left),
"right": Json(pair.right),
"correlation": Json(pair.correlation),
})
}
]
Json::object({
"runs": Json(d.runs),
"terms": Json(d.terms),
"rank": Json(d.rank),
"estimable": Json(d.estimable),
"residual_dof": Json(d.residual_dof),
"zero_columns": Json(d.zero_columns),
"exact_aliases": Json::array(aliases),
"max_abs_correlation": Json(d.max_abs_correlation),
"log_determinant": match d.log_determinant {
Some(x) => Json(x)
None => Json::null()
},
}).stringify()
}
///|
// Preflight bounds nesting without depending on deprecated core parser options.
fn check_json_depth(text : String) -> Unit raise DesignError {
let mut depth = 0
let mut quoted = false
let mut escaped = false
for ch in text {
if quoted {
if escaped {
escaped = false
} else if ch == '\\' {
escaped = true
} else if ch == '"' {
quoted = false
}
} else if ch == '"' {
quoted = true
} else if ch == '{' || ch == '[' {
depth += 1
if depth > 8 {
raise Budget("JSON nesting exceeds eight")
}
} else if ch == '}' || ch == ']' {
depth -= 1
}
}
}