///|
fn text(value : Json, field : String) -> String raise StnError {
guard value is String(s) else {
raise StnError("invalid_input", field + " must be a string")
}
s
}
///|
fn required(obj : Map[String, Json], field : String) -> Json raise StnError {
guard obj.get(field) is Some(v) else {
raise StnError("invalid_input", "missing field: " + field)
}
v
}
///|
fn bound(obj : Map[String, Json], field : String) -> Int32? raise StnError {
match obj.get(field) {
None | Some(Null) => None
Some(Number(v, ..)) if v >= -1000000.0 && v <= 1000000.0 && v.floor() == v =>
Some(v.to_int())
_ =>
raise StnError(
"invalid_input",
field + " must be null or an integer in -1000000..1000000",
)
}
}
///|
fn fields(
obj : Map[String, Json],
allowed : Array[String],
) -> Unit raise StnError {
for key in obj.keys() {
if !allowed.contains(key) {
raise StnError("invalid_input", "unknown field: " + key)
}
}
}
///|
fn parse_network(value : Json) -> Network raise StnError {
guard value is Object(obj) else {
raise StnError("invalid_input", "network must be an object")
}
fields(obj, ["timepoints", "constraints", "queries", "max_steps"])
guard required(obj, "timepoints") is Array(ns) else {
raise StnError("invalid_input", "timepoints must be an array")
}
guard required(obj, "constraints") is Array(cs) else {
raise StnError("invalid_input", "constraints must be an array")
}
if ns.length() > 64 || cs.length() > 512 {
raise StnError("limit_exceeded", "network size limit")
}
let names = ns.map(v => text(v, "timepoint"))
let constraints : Array[Constraint] = []
for v in cs {
guard v is Object(c) else {
raise StnError("invalid_input", "constraint must be an object")
}
fields(c, ["id", "from", "to", "lower", "upper"])
constraints.push({
id: text(required(c, "id"), "id"),
from: text(required(c, "from"), "from"),
to: text(required(c, "to"), "to"),
lower: bound(c, "lower"),
upper: bound(c, "upper"),
})
}
network(names, constraints)
}
///|
fn number(value : Int64?) -> Json {
match value {
Some(v) => Json::number(v.to_double())
None => Json::null()
}
}
///|
/// One JSON request. Inconsistency is a valid result, while malformed input raises.
pub fn analyze(value : Json) -> Json raise StnError {
let net = parse_network(value)
guard value is Object(obj) else { abort("validated object") }
let steps = match obj.get("max_steps") {
None => 2000000
Some(Number(v, ..)) if v >= 1.0 && v <= 10000000.0 && v.floor() == v =>
v.to_int()
_ =>
raise StnError(
"invalid_input", "max_steps must be an integer in 1..10000000",
)
}
let queries : Array[(String, String)] = []
match obj.get("queries") {
None => ()
Some(Array(qs)) => {
if qs.length() > 256 {
raise StnError("limit_exceeded", "at most 256 queries")
}
for q in qs {
guard q is Object(q) else {
raise StnError("invalid_input", "query must be an object")
}
fields(q, ["from", "to"])
let a = text(required(q, "from"), "from")
let b = text(required(q, "to"), "to")
find_name(net.names, a) |> ignore
find_name(net.names, b) |> ignore
queries.push((a, b))
}
}
_ => raise StnError("invalid_input", "queries must be an array")
}
match solve(net, max_steps=steps) {
Feasible(solution) => {
let assignment : Map[String, Json] = Map([])
for (name, value) in solution.assignment() {
assignment[name] = number(Some(value))
}
let ranges = queries.map(q => {
let (lower, upper) = solution.bounds(q.0, q.1)
Json::object(
Map([
("from", Json::string(q.0)),
("to", Json::string(q.1)),
("lower", number(lower)),
("upper", number(upper)),
]),
)
})
Json::object(
Map([
("status", Json::string("feasible")),
("assignment", Json::object(assignment)),
("bounds", Json::array(ranges)),
]),
)
}
Inconsistent(cycle) => {
let edges = cycle.map(e => {
Json::object(
Map([
("id", Json::string(e.id)),
("from", Json::string(e.from)),
("to", Json::string(e.to)),
("bound", number(Some(e.bound))),
("side", Json::string(e.side)),
]),
)
})
Json::object(
Map([
("status", Json::string("inconsistent")),
("cycle", Json::array(edges)),
]),
)
}
}
}
///|
fn error_json(code : String, message : String) -> Json {
Json::object(
Map([
("status", Json::string("error")),
("code", Json::string(code)),
("message", Json::string(message)),
]),
)
}
///|
pub fn analyze_batch(requests : Json) -> Json raise StnError {
guard requests is Array(items) else {
raise StnError("invalid_input", "batch must be an array")
}
if items.length() > 64 {
raise StnError("limit_exceeded", "at most 64 batch items")
}
Json::array(
items.map(item => {
analyze(item) catch {
StnError(code, msg) => error_json(code, msg)
}
}),
)
}
///|
/// Preflight bounds decoder work before delegating exact JSON grammar to core.
pub fn parse_json(source : String) -> Json raise StnError {
if source.length() > 1048576 {
raise StnError("limit_exceeded", "JSON exceeds 1 Mi UTF-16 units")
}
let mut depth = 0
let mut quoted = false
let mut escaped = false
let mut tokens = 0
let mut string_units = 0
for c in source.iter() {
if quoted {
string_units += 1
if string_units > 256 {
raise StnError(
"limit_exceeded", "JSON string exceeds 256 source characters",
)
}
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == '"' {
quoted = false
}
} else {
if c == '"' {
quoted = true
string_units = 0
}
if c == '{' || c == '[' {
depth += 1
}
if c == '}' || c == ']' {
depth -= 1
}
if c != ' ' && c != '\n' && c != '\r' && c != '\t' {
tokens += 1
}
if depth > 16 || tokens > 200000 {
raise StnError(
"limit_exceeded", "JSON nesting or token budget exceeded",
)
}
}
}
@json.parse(source, max_nesting_depth=16, reject_duplicate_keys=true) catch {
_ => raise StnError("invalid_json", "invalid JSON text")
}
}