///|
priv suberror CodecError {
Invalid(String)
}
///|
fn parse_json(text : String) -> Json raise CodecError {
if text.length() > 65536 {
raise Invalid("JSON exceeds 65536 code units")
}
let mut depth = 0
let mut quoted = false
let mut escaped = false
for c in text.iter() {
if quoted {
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == '"' {
quoted = false
}
} else if c == '"' {
quoted = true
} else if c == '{' || c == '[' {
depth += 1
if depth > 8 {
raise Invalid("JSON nesting exceeds eight containers")
}
} else if c == '}' || c == ']' {
depth -= 1
}
}
@json.parse(text[:]) catch {
_ => raise Invalid("invalid JSON")
}
}
///|
fn codec_fields(
value : Json,
allowed : Array[String],
) -> Map[String, Json] raise CodecError {
guard value is Object(fields) else { raise Invalid("expected object") }
for key, _ in fields {
if !allowed.contains(key) {
raise Invalid("unknown field: " + key)
}
}
fields
}
///|
fn required(fields : Map[String, Json], key : String) -> Json raise CodecError {
match fields.get(key) {
Some(v) => v
None => raise Invalid("missing field: " + key)
}
}
///|
fn codec_string(value : Json) -> String raise CodecError {
guard value is String(s) else { raise Invalid("expected string") }
s
}
///|
fn codec_int(value : Json) -> Int32 raise CodecError {
guard value is Number(x, ..) else { raise Invalid("expected integer") }
if x != x || x < -1.0 || x > 1000000.0 {
raise Invalid("integer outside -1..1000000")
}
let n = x.to_int()
if n.to_double() != x {
raise Invalid("fractional integer")
}
n
}
///|
fn codec_array(value : Json, limit : Int32) -> Array[Json] raise CodecError {
guard value is Array(items) else { raise Invalid("expected array") }
if items.length() > limit {
raise Invalid("array exceeds schema bound")
}
items
}
///|
fn demand_json(value : Json) -> Demand raise CodecError {
let f = codec_fields(value, ["id", "material", "length", "quantity"])
demand(
codec_string(required(f, "id")),
codec_string(required(f, "material")),
codec_int(required(f, "length")),
codec_int(required(f, "quantity")),
)
}
///|
fn stock_json(value : Json) -> Stock raise CodecError {
let f = codec_fields(value, [
"id", "material", "length", "cost", "quantity", "trim_start", "trim_end",
])
stock(
codec_string(required(f, "id")),
codec_string(required(f, "material")),
codec_int(required(f, "length")),
codec_int(required(f, "cost")),
codec_int(required(f, "quantity")),
trim_start=codec_int(required(f, "trim_start")),
trim_end=codec_int(required(f, "trim_end")),
)
}
///|
fn problem_json(value : Json) -> Problem raise CodecError {
let f = codec_fields(value, [
"schema", "demands", "stocks", "kerf", "min_remnant",
])
if codec_string(required(f, "schema")) != "mooncutstock.problem/1" {
raise Invalid("unsupported problem schema")
}
let ds = codec_array(required(f, "demands"), 12).map(demand_json)
let ss = codec_array(required(f, "stocks"), 16).map(stock_json)
match
problem(
ds,
ss,
kerf=codec_int(required(f, "kerf")),
min_remnant=codec_int(required(f, "min_remnant")),
) {
Ok(p) => p
Err(e) => raise Invalid(e)
}
}
///|
pub fn decode_problem(text : String) -> Result[Problem, String] {
Ok(problem_json(parse_json(text))) catch {
Invalid(e) => Err(e)
}
}