///|
/// Bounded parse: at most 1 Mi code units, with at most eight nested containers.
pub fn parse_document(text : String) -> Json raise ChoiceError {
if text.length() > 1048576 {
raise Budget("JSON document exceeds 1 Mi code units")
}
let mut depth = 0
let mut in_string = false
let mut escaped = false
for c in text.iter() {
if in_string {
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == '"' {
in_string = false
}
} else if c == '"' {
in_string = true
} else if c == '[' || c == '{' {
depth += 1
if depth > 8 {
raise Budget("JSON nesting exceeds eight containers")
}
} else if c == ']' || c == '}' {
depth -= 1
}
}
@json.parse(text[:]) catch {
_ => raise Invalid("invalid JSON or excessive nesting")
}
}
///|
fn object_fields(
value : Json,
allowed : Array[String],
) -> Map[String, Json] raise ChoiceError {
guard value is Object(fields) else { raise Invalid("expected a JSON object") }
for key, _ in fields {
if !allowed.contains(key) {
raise Invalid("unknown JSON field: " + key)
}
}
fields
}
///|
fn required(fields : Map[String, Json], key : String) -> Json raise ChoiceError {
match fields.get(key) {
Some(value) => value
None => raise Invalid("missing JSON field: " + key)
}
}
///|
fn json_string(value : Json) -> String raise ChoiceError {
guard value is String(s) else { raise Invalid("expected JSON string") }
s
}
///|
fn json_number(value : Json) -> Double raise ChoiceError {
guard value is Number(x, ..) else { raise Invalid("expected JSON number") }
input_number(x)
x
}
///|
fn json_array(value : Json, limit : Int32) -> Array[Json] raise ChoiceError {
guard value is Array(items) else { raise Invalid("expected JSON array") }
if items.length() > limit {
raise Budget("JSON array exceeds supported length")
}
items
}
///|
fn json_numbers(value : Json, limit : Int32) -> Array[Double] raise ChoiceError {
json_array(value, limit).map(json_number)
}
///|
fn matrix_from_json(value : Json) -> DecisionMatrix raise ChoiceError {
let f = object_fields(value, [
"schema", "alternatives", "criteria", "directions", "values",
])
if json_string(required(f, "schema")) != "moonchoice.matrix/1" {
raise Invalid("unsupported matrix schema")
}
let names = json_array(required(f, "alternatives"), 256).map(json_string)
let criteria = json_array(required(f, "criteria"), 32).map(json_string)
let directions = json_array(required(f, "directions"), 32).map(v => {
match json_string(v) {
"benefit" => Benefit
"cost" => Cost
_ => raise Invalid("direction must be benefit or cost")
}
})
let values = json_array(required(f, "values"), 256).map(row => {
json_numbers(row, 32)
})
decision_matrix(names, criteria, directions, values)
}
///|
pub fn decode_matrix(text : String) -> DecisionMatrix raise ChoiceError {
matrix_from_json(parse_document(text))
}
///|
pub fn DecisionMatrix::to_json(self : DecisionMatrix) -> Json {
Json::object({
"schema": Json::string("moonchoice.matrix/1"),
"alternatives": Json(self.names),
"criteria": Json(self.criteria),
"directions": Json(
self.directions.map(d => if d == Benefit { "benefit" } else { "cost" }),
),
"values": Json(self.cells),
})
}
///|
pub fn encode_matrix(m : DecisionMatrix) -> String {
m.to_json().stringify()
}
///|
/// Validates externally mutable Ranking arrays before serializing them.
pub fn ranking_json(ranking : Ranking) -> Json raise ChoiceError {
let r = validated_ranking(ranking)
Json::object({
"schema": Json::string("moonchoice.ranking/1"),
"labels": Json(r.labels),
"scores": Json(r.scores),
"order": Json(r.order),
"ranks": Json(r.ranks),
"higher_is_better": Json(r.higher_is_better),
"tie_tolerance": Json(r.tie_tolerance),
"winners": Json(r.winners()),
})
}