///|
/// Validated version-one request. Parsing never constructs private models unchecked.
pub struct Request {
dna : Dna
enzymes : Array[Enzyme]
policy : AmbiguityPolicy
} derive(Debug)
///|
fn json_object(
value : Json,
keys : Array[String],
) -> Map[String, Json] raise RestrictError {
match value {
Object(fields) => {
for key in fields.keys() {
if !keys.contains(key) {
raise InvalidInput("JSON_UNKNOWN_FIELD")
}
}
fields
}
_ => raise InvalidInput("JSON_OBJECT")
}
}
///|
fn required(
fields : Map[String, Json],
key : String,
) -> Json raise RestrictError {
match fields.get(key) {
Some(value) => value
None => raise InvalidInput("JSON_REQUIRED_" + key)
}
}
///|
fn json_string(value : Json) -> String raise RestrictError {
match value {
String(s) => s
_ => raise InvalidInput("JSON_STRING")
}
}
///|
fn json_int(value : Json) -> Int raise RestrictError {
match value {
Number(n, ..) => {
if n < -2147483648.0 ||
n > 2147483647.0 ||
n != n ||
n.to_int().to_double() != n {
raise InvalidInput("JSON_INTEGER")
}
n.to_int()
}
_ => raise InvalidInput("JSON_INTEGER")
}
}
///|
fn parse_json(text : String) -> Json raise RestrictError {
if text.length() > 2000000 {
raise LimitExceeded("JSON_BYTES")
}
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 > 32 {
raise LimitExceeded("JSON_DEPTH_32")
}
} else if ch == ']' || ch == '}' {
depth -= 1
}
}
@json.parse(text[:]) catch {
_ => raise InvalidInput("JSON_SYNTAX")
}
}
///|
/// Strict request schema; omitted policy defaults to certain-only recognition.
pub fn read_request(text : String) -> Request raise RestrictError {
let fields = json_object(parse_json(text), [
"version", "sequence", "topology", "enzymes", "policy",
])
if json_int(required(fields, "version")) != 1 {
raise InvalidInput("JSON_VERSION")
}
let topology = match json_string(required(fields, "topology")) {
"linear" => Topology::Linear
"circular" => Topology::Circular
_ => raise InvalidInput("JSON_TOPOLOGY")
}
let policy = match fields.get("policy") {
None => CertainOnly
Some(String("certain")) => CertainOnly
Some(String("possible")) => IncludePossible
Some(String("reject")) => RejectPossible
_ => raise InvalidInput("JSON_POLICY")
}
let dna = Dna::new(json_string(required(fields, "sequence")), topology~)
let models = match required(fields, "enzymes") {
Array(values) => values
_ => raise InvalidInput("JSON_ENZYMES")
}
if models.length() > 32 {
raise LimitExceeded("ENZYME_MAX_32")
}
let enzymes : Array[Enzyme] = []
for model in models {
let f = json_object(model, ["name", "motif", "top", "bottom"])
enzymes.push(
Enzyme::new(
json_string(required(f, "name")),
json_string(required(f, "motif")),
json_int(required(f, "top")),
json_int(required(f, "bottom")),
),
)
}
{ dna, enzymes, policy }
}
///|
pub fn Request::run(self : Request) -> Digest raise RestrictError {
digest_many(self.dna, self.enzymes, policy=self.policy)
}
///|
pub fn Request::verify(
self : Request,
report : Digest,
) -> Unit raise RestrictError {
if self.policy != report.policy {
raise InvalidInput("VERIFY_POLICY")
}
verify_digest(self.dna, self.enzymes, report)
}
///|
/// Versioned report wrapper around explicit typed coordinate evidence.
pub fn write_report(report : Digest) -> String {
let value : Json = {
"version": 1,
"kind": "moonrestrict.digest",
"digest": report.to_json(),
}
value.stringify()
}
///|
/// Decode does not establish authenticity; Request::verify is mandatory for untrusted reports.
pub fn read_report(text : String) -> Digest raise RestrictError {
let fields = json_object(parse_json(text), ["version", "kind", "digest"])
if json_int(required(fields, "version")) != 1 ||
json_string(required(fields, "kind")) != "moonrestrict.digest" {
raise InvalidInput("REPORT_VERSION")
}
let encoded = required(fields, "digest")
let report : Digest = @json.from_json(encoded) catch {
_ => raise InvalidInput("REPORT_SCHEMA")
}
// Derived integer decoders truncate fractions; exact schema roundtrip rejects those and unknown fields.
if report.to_json() != encoded {
raise InvalidInput("REPORT_NONCANONICAL_SCHEMA")
}
report
}
///|
/// End-to-end strict JSON request -> computed and independently verified report.
pub fn run_json(text : String) -> String raise RestrictError {
let request = read_request(text)
let report = request.run()
request.verify(report)
let output = write_report(report)
if output.length() > 2000000 {
raise LimitExceeded("REPORT_BYTES")
}
output
}