///|
fn object_fields(j : Json, path : String) -> Map[String, Json] raise {
match j {
Object(m) => m
_ => fail(path + " must be an object")
}
}
///|
fn known_fields(
m : Map[String, Json],
allowed : Array[String],
path : String,
) -> Unit raise {
for k, _ in m {
if !allowed.contains(k) {
fail(path + ": unknown field " + k)
}
}
}
///|
fn required(m : Map[String, Json], k : String, path : String) -> Json raise {
match m.get(k) {
Some(v) => v
None => fail(path + ": missing field " + k)
}
}
///|
fn identifier(j : Json, path : String) -> String raise {
match j {
String(s) => {
if !valid_identifier(s) {
fail(path + " must have 1..256 UTF-16 code units")
}
s
}
_ => fail(path + " must be a string")
}
}
///|
fn timestamp(m : Map[String, Json], k : String, path : String) -> Int64? raise {
match m.get(k) {
None | Some(Null) => None
Some(Number(n, ..)) => {
if n < 0.0 || n > 9007199254740991.0 || n.to_int64().to_double() != n {
fail(path + "." + k + " must be a non-negative safe integer")
}
Some(n.to_int64())
}
_ => fail(path + "." + k + " must be an integer or null")
}
}
///|
/// Parse the versioned-register input schema. Unknown fields are rejected.
pub fn parse_history(text : String) -> Result[History, String] {
if text.length() > 1048576 {
return Err("Input exceeds 1,048,576 UTF-16 code units")
}
try {
let j = guarded_json(text)
let root = object_fields(j, "history")
known_fields(
root,
[
"schemaVersion", "name", "model", "initial", "versionOrders", "transactions",
],
"history",
)
match root.get("schemaVersion") {
None => ()
Some(Number(n, ..)) if n == 1.0 => ()
_ => fail("schemaVersion must be 1")
}
match root.get("name") {
None => ()
Some(String(_)) => ()
_ => fail("name must be a string")
}
let model = match root.get("model") {
None | Some(String("serializable")) => Serializable
Some(String("strict-serializable")) => StrictSerializable
_ => fail("model must be serializable or strict-serializable")
}
let raw_initial = object_fields(
required(root, "initial", "history"),
"initial",
)
let initial : Map[String, String] = Map([])
for k, v in raw_initial {
let _ = identifier(k.to_json(), "initial key")
initial[k] = identifier(v, "initial." + k)
}
let orders : Map[String, Array[String]] = Map([])
match root.get("versionOrders") {
None => ()
Some(v) => {
let raw_orders = object_fields(v, "versionOrders")
for k, value in raw_orders {
let items = match value {
Array(a) => a
_ => fail("versionOrders." + k + " must be an array")
}
let order : Array[String] = []
for item in items {
order.push(identifier(item, "versionOrders." + k))
}
orders[k] = order
}
}
}
let txs = match required(root, "transactions", "history") {
Array(a) => a
_ => fail("transactions must be an array")
}
if txs.length() > 512 {
fail("At most 512 transactions are supported in v0.0.1")
}
let transactions : Array[Transaction] = []
let mut total_ops = 0
for i, raw_tx in txs {
let path = "transactions[" + i.to_string() + "]"
let tx = object_fields(raw_tx, path)
known_fields(tx, ["id", "status", "ops", "start", "end"], path)
let id = identifier(required(tx, "id", path), path + ".id")
let status = match required(tx, "status", path) {
String("committed") => Committed
String("aborted") => Aborted
String("unknown") => Unknown
_ => fail(path + ".status must be committed, aborted or unknown")
}
let raw_ops = match required(tx, "ops", path) {
Array(a) => a
_ => fail(path + ".ops must be an array")
}
total_ops += raw_ops.length()
if total_ops > 10000 {
fail("At most 10,000 operations are supported")
}
let ops : Array[Operation] = []
for oi, raw_op in raw_ops {
let op_path = path + ".ops[" + oi.to_string() + "]"
let op = object_fields(raw_op, op_path)
known_fields(op, ["type", "key", "version"], op_path)
let kind = match required(op, "type", op_path) {
String("read") => Read
String("write") => Write
_ => fail(op_path + ".type must be read or write")
}
ops.push({
kind,
key: identifier(required(op, "key", op_path), op_path + ".key"),
version: identifier(
required(op, "version", op_path),
op_path + ".version",
),
})
}
transactions.push({
id,
status,
ops,
start: timestamp(tx, "start", path),
end: timestamp(tx, "end", path),
})
}
Ok({ model, initial, version_orders: orders, transactions, })
} catch {
e => Err(e.to_string())
}
}
///|
/// Validate typed inputs too, so constructing History directly cannot bypass constraints.
fn validate_history(h : History) -> Array[Finding] {
let errors : Array[Finding] = []
if h.transactions.length() > 512 {
errors.push(
issue("resource_limit", "error", "At most 512 transactions are supported"),
)
}
let ids : Map[String, Bool] = Map([])
let writes : Map[String, Map[String, Bool]] = Map([])
let mut op_count = 0
for tx in h.transactions {
if !valid_identifier(tx.id) {
errors.push(
issue("invalid_identifier", "error", "Invalid transaction identifier"),
)
}
if ids.contains(tx.id) {
errors.push(
issue(
"duplicate_transaction",
"error",
"Transaction identifiers must be unique",
txs=[tx.id],
),
)
}
ids[tx.id] = true
op_count += tx.ops.length()
match (tx.start, tx.end) {
(Some(s), Some(e)) if s < 0L || e < s || e > 9007199254740991L =>
errors.push(
issue("invalid_interval", "error", "Invalid transaction interval", txs=[
tx.id,
]),
)
(Some(s), None) if s < 0L || s > 9007199254740991L =>
errors.push(
issue("invalid_interval", "error", "Invalid start timestamp", txs=[
tx.id,
]),
)
(None, Some(e)) if e < 0L || e > 9007199254740991L =>
errors.push(
issue("invalid_interval", "error", "Invalid end timestamp", txs=[
tx.id,
]),
)
_ => ()
}
for oi, op in tx.ops {
if !valid_identifier(op.key) || !valid_identifier(op.version) {
errors.push(
issue(
"invalid_identifier",
"error",
"Invalid operation key or version",
txs=[tx.id],
ops=[oi],
),
)
}
if !h.initial.contains(op.key) {
errors.push(
issue(
"missing_initial",
"error",
"Every operated key requires an initial version",
txs=[tx.id],
key=Some(op.key),
ops=[oi],
),
)
}
if op.kind == Write {
let versions = writes.get_or_default(op.key, Map([]))
if versions.contains(op.version) {
errors.push(
issue(
"duplicate_write_version",
"error",
"A version must identify exactly one write on its key",
txs=[tx.id],
key=Some(op.key),
ops=[oi],
),
)
}
if h.initial.get(op.key) == Some(op.version) {
errors.push(
issue(
"initial_version_reused",
"error",
"An initial version cannot be written again",
txs=[tx.id],
key=Some(op.key),
ops=[oi],
),
)
}
versions[op.version] = true
writes[op.key] = versions
}
}
}
if op_count > 10000 {
errors.push(
issue(
"resource_limit", "error", "At most 10,000 operations are supported",
),
)
}
for k, v in h.initial {
if !valid_identifier(k) || !valid_identifier(v) {
errors.push(
issue(
"invalid_identifier",
"error",
"Invalid initial key or version",
key=Some(k),
),
)
}
}
errors
}