///|
fn object_at(value : Json, path : String) -> Map[String, Json] raise InputError {
match value {
Object(fields) => fields
_ => invalid(path, "expected object")
}
}
///|
fn known_fields(
fields : Map[String, Json],
allowed : Array[String],
path : String,
) -> Unit raise InputError {
for key, _ in fields {
if !allowed.contains(key) {
invalid(path, "unknown field (check schema)")
}
}
}
///|
fn required(
fields : Map[String, Json],
key : String,
path : String,
) -> Json raise InputError {
match fields.get(key) {
Some(value) => value
None => invalid("\{path}.\{key}", "required field")
}
}
///|
fn text_at(value : Json, path : String) -> String raise InputError {
match value {
String(s) => s
_ => invalid(path, "expected string")
}
}
///|
fn text_field(
fields : Map[String, Json],
key : String,
path : String,
fallback? : String,
) -> String raise InputError {
match fields.get(key) {
Some(value) => text_at(value, "\{path}.\{key}")
None =>
match fallback {
Some(value) => value
None => text_at(required(fields, key, path), "\{path}.\{key}")
}
}
}
///|
fn int_at(value : Json, path : String) -> Int32 raise InputError {
match value {
Number(n, ..) => {
if n < 0.0 || n > 2147483647.0 || n != n.to_int().to_double() {
invalid(path, "expected integer in [0, 2147483647]")
}
n.to_int()
}
_ => invalid(path, "expected nonnegative integer")
}
}
///|
fn optional_int(
fields : Map[String, Json],
key : String,
path : String,
) -> Int32? raise InputError {
match fields.get(key) {
Some(Null) | None => None
Some(value) => Some(int_at(value, "\{path}.\{key}"))
}
}
///|
fn optional_text(
fields : Map[String, Json],
key : String,
path : String,
) -> String? raise InputError {
match fields.get(key) {
Some(Null) | None => None
Some(value) => Some(text_at(value, "\{path}.\{key}"))
}
}
///|
fn string_list(
fields : Map[String, Json],
key : String,
path : String,
) -> Array[String] raise InputError {
match fields.get(key) {
None => []
Some(Array(items)) => {
if items.length() > 64 {
invalid("\{path}.\{key}", "at most 64 items")
}
let result = []
for i, item in items {
result.push(text_at(item, "\{path}.\{key}[\{i}]"))
}
result
}
_ => invalid("\{path}.\{key}", "expected string array")
}
}
///|
fn parse_json(source : String, path : String) -> Json raise InputError {
if source.length() > 8388608 {
invalid(path, "input exceeds 8 Mi UTF-16 units")
}
guard_depth(source, path)
let value = @json.parse(source) catch {
_ => invalid(path, "invalid JSON or nesting exceeds 32")
}
let cursor : Cursor = { text: source, offset: 0, }
cursor.value(path)
value
}
///|
fn event_from_json(value : Json, path : String) -> Event raise InputError {
let f = object_at(value, path)
known_fields(
f,
[
"id", "session", "seq", "at_ms", "kind", "tool", "resource", "inputs", "output",
"labels", "cost", "approval", "expires_ms", "outcome",
],
path,
)
{
id: text_field(f, "id", path),
session: text_field(f, "session", path),
seq: int_at(required(f, "seq", path), "\{path}.seq"),
at_ms: int_at(required(f, "at_ms", path), "\{path}.at_ms"),
kind: text_field(f, "kind", path, fallback="call"),
tool: text_field(f, "tool", path, fallback=""),
resource: text_field(f, "resource", path, fallback=""),
inputs: string_list(f, "inputs", path),
output: optional_text(f, "output", path),
labels: string_list(f, "labels", path),
cost: optional_int(f, "cost", path).unwrap_or(0),
approval: optional_text(f, "approval", path),
expires_ms: optional_int(f, "expires_ms", path),
outcome: text_field(f, "outcome", path, fallback="ok"),
}
}
///|
/// Accept a JSON array or JSONL. Blank JSONL lines are ignored; comments are errors.
pub fn parse_trace(source : String) -> Array[Event] raise InputError {
if source.length() > 8388608 {
invalid("trace", "input exceeds 8 Mi UTF-16 units")
}
let trimmed = source.trim().to_owned()
let events : Array[Event] = []
if trimmed.has_prefix("[") {
match parse_json(trimmed, "trace") {
Array(items) => {
if items.length() > 10000 {
invalid("trace", "at most 10000 events")
}
for i, item in items {
events.push(event_from_json(item, "trace[\{i}]"))
}
}
_ => invalid("trace", "expected array")
}
} else {
for i, line in trimmed.split("\n").to_array() {
let line = line.trim().to_owned()
if !line.is_empty() {
if events.length() >= 10000 {
invalid("trace", "at most 10000 events")
}
events.push(
event_from_json(parse_json(line, "line \{i + 1}"), "line \{i + 1}"),
)
}
}
}
validate_trace(events)
events
}
///|
fn rule_from_json(value : Json, path : String) -> Rule raise InputError {
let f = object_at(value, path)
known_fields(
f,
[
"id", "kind", "tool", "resource", "labels", "limit", "window_ms", "prerequisite",
],
path,
)
let kind = text_field(f, "kind", path)
// Reject misspelled or inapplicable options instead of silently ignoring them.
for key, _ in f {
if key == "labels" && kind != "block_labels" {
invalid(path, "labels is only valid for block_labels")
}
if key == "limit" && kind != "budget" && kind != "rate" {
invalid(path, "limit is only valid for budget or rate")
}
if key == "window_ms" &&
kind != "budget" &&
kind != "rate" &&
kind != "require_before" {
invalid(path, "window_ms is not applicable to this rule")
}
if key == "prerequisite" && kind != "require_before" {
invalid(path, "prerequisite is only valid for require_before")
}
}
{
id: text_field(f, "id", path),
kind,
tool: text_field(f, "tool", path, fallback="*"),
resource: text_field(f, "resource", path, fallback="*"),
labels: string_list(f, "labels", path),
limit: optional_int(f, "limit", path),
window_ms: optional_int(f, "window_ms", path),
prerequisite: optional_text(f, "prerequisite", path),
}
}
///|
pub fn parse_policy(source : String) -> Policy raise InputError {
let f = object_at(parse_json(source, "policy"), "policy")
known_fields(
f,
["schema_version", "name", "default_decision", "rules"],
"policy",
)
if int_at(required(f, "schema_version", "policy"), "policy.schema_version") !=
1 {
invalid("policy.schema_version", "only version 1 is supported")
}
let rules : Array[Rule] = []
match required(f, "rules", "policy") {
Array(items) => {
if items.length() > 128 {
invalid("policy.rules", "at most 128 rules")
}
for i, item in items {
rules.push(rule_from_json(item, "policy.rules[\{i}]"))
}
}
_ => invalid("policy.rules", "expected array")
}
let policy : Policy = {
name: text_field(f, "name", "policy"),
default_decision: text_field(f, "default_decision", "policy"),
rules,
}
validate_policy(policy)
policy
}