///|
fn archive_fields(
value : Json,
location : String,
) -> Map[String, Json] raise TemporalError {
match value {
Object(fields) => fields
_ => raise InvalidArchive(location + " must be an object")
}
}
///|
fn archive_string(
fields : Map[String, Json],
name : String,
location : String,
) -> String raise TemporalError {
match fields.get(name) {
Some(String(value)) => value
_ => raise InvalidArchive(location + "." + name + " must be a string")
}
}
///|
fn archive_optional_string(
fields : Map[String, Json],
name : String,
location : String,
) -> String? raise TemporalError {
match fields.get(name) {
Some(String(value)) => Some(value)
Some(Null) => None
_ => raise InvalidArchive(location + "." + name + " must be string or null")
}
}
///|
fn check_archive_fields(
fields : Map[String, Json],
allowed : Array[String],
location : String,
) -> Unit raise TemporalError {
for field, _ in fields {
let mut found = false
for name in allowed {
if field == name {
found = true
}
}
if !found {
raise InvalidArchive(location + " has unknown field " + field)
}
}
}
///|
fn archive_revision(
value : Json,
index : Int32,
) -> (Int64, Change) raise TemporalError {
let location = "revisions[" + index.to_string() + "]"
let fields = archive_fields(value, location)
check_archive_fields(
fields,
["knownAt", "key", "validFrom", "validTo", "value"],
location,
)
let known_at = parse_instant(archive_string(fields, "knownAt", location))
let key = archive_string(fields, "key", location)
let start = parse_instant(archive_string(fields, "validFrom", location))
let end = match archive_optional_string(fields, "validTo", location) {
Some(text) => Some(parse_instant(text))
None => None
}
let valid = match end {
Some(finish) => Span::new(start, finish)
None => Span::open(start)
}
let value = archive_optional_string(fields, "value", location)
(known_at, { key, valid, value, })
}
///|
/// Import a schema-v1 archive by replaying validated atomic batches.
pub fn Ledger::from_json(
value : Json,
maximum~ : Int32 = 1000000,
) -> Ledger raise TemporalError {
let fields = archive_fields(value, "root")
check_archive_fields(fields, ["schemaVersion", "revisions"], "root")
match fields.get("schemaVersion") {
Some(Number(version, ..)) if version == 1.0 => ()
_ => raise InvalidArchive("unsupported schemaVersion")
}
let items = match fields.get("revisions") {
Some(Array(items)) => items
_ => raise InvalidArchive("revisions must be an array")
}
ignore(Ledger::new(maximum~))
if items.length() > maximum {
raise TooManyRevisions(maximum~)
}
let revisions : Array[Revision] = []
let mut pending : Array[Change] = []
let mut pending_time : Int64? = None
for index, item in items {
let (known_at, change) = archive_revision(item, index)
match pending_time {
Some(at) if at != known_at => {
if known_at < at {
raise KnowledgeTimeNotIncreasing(previous=at, proposed=known_at)
}
validate_batch_changes(pending)
pending = []
pending_time = Some(known_at)
}
None => pending_time = Some(known_at)
_ => ()
}
revisions.push({
key: change.key,
valid: change.valid,
known_at,
value: change.value,
sequence: revisions.length(),
})
pending.push(change)
}
match pending_time {
Some(_) => validate_batch_changes(pending)
None => ()
}
{
revisions,
index: build_index(revisions),
latest_time: pending_time,
maximum,
}
}
///|
/// Parse a bounded JSON archive before replaying its revisions.
pub fn Ledger::from_json_string(
text : String,
maximum~ : Int32 = 1000000,
max_chars~ : Int32 = 16 * 1024 * 1024,
) -> Ledger raise TemporalError {
if max_chars <= 0 || text.length() > max_chars {
raise InvalidArchive("archive exceeds character limit")
}
reject_duplicate_json_keys(text)
let value = @json.parse(text) catch {
_ => raise InvalidArchive("invalid JSON syntax")
}
Ledger::from_json(value, maximum~)
}