///|
/// Errors include a JSON Pointer into the rule whenever evaluation is involved.
pub(all) suberror LogicError {
InvalidRule(String, String)
LimitExceeded(String)
NonFiniteNumber(String)
InvalidInput(String)
}
///|
/// The value and captured log operations from one evaluation.
pub(all) struct Evaluation {
value : Json
logs : Array[Json]
steps_used : Int
}
///|
priv struct Context {
mut remaining : Int
max_depth : Int
logs : Array[Json]
}
///|
fn Context::visit(
self : Context,
depth : Int,
path : String,
) -> Unit raise LogicError {
if depth > self.max_depth || self.remaining <= 0 {
raise LimitExceeded(path)
}
self.remaining -= 1
}
///|
fn validate_input(
value : Json,
ctx : Context,
depth : Int,
) -> Unit raise LogicError {
ctx.visit(depth, "")
match value {
Number(n, ..) =>
if n.is_nan() || n.is_inf() {
raise InvalidInput("input contains a non-finite number")
}
Array(xs) =>
for x in xs {
validate_input(x, ctx, depth + 1)
}
Object(xs) =>
for key, x in xs {
if has_unpaired_surrogate(key) {
raise InvalidInput("object key contains an unpaired UTF-16 surrogate")
}
validate_input(x, ctx, depth + 1)
}
String(s) =>
if has_unpaired_surrogate(s) {
raise InvalidInput("input contains an unpaired UTF-16 surrogate")
}
_ => ()
}
}
///|
/// Evaluate a rule with bounded input traversal and execution. Logs are captured, not printed.
pub fn evaluate(
rule : Json,
data : Json,
max_steps? : Int = 100000,
max_depth? : Int = 128,
) -> Evaluation raise LogicError {
if max_steps <= 0 || max_depth < 1 || max_depth > 256 {
raise InvalidInput(
"max_steps must be positive and max_depth must be between 1 and 256",
)
}
let ctx = { remaining: max_steps, max_depth, logs: [], }
validate_input(rule, ctx, 0)
validate_input(data, ctx, 0)
let value = eval(rule, data, ctx, 0, "")
{ value, logs: ctx.logs, steps_used: max_steps - ctx.remaining, }
}
///|
/// Evaluate a JSONLogic rule. See evaluate for log capture and execution budgets.
pub fn apply(rule : Json, data : Json) -> Json raise LogicError {
evaluate(rule, data).value
}
///|
/// Parse two JSON documents and evaluate. Parse failures are distinct from rule errors.
pub fn apply_json(rule : String, data : String) -> Json raise LogicError {
if rule.length() > 2097152 || data.length() > 2097152 {
raise InvalidInput("JSON document exceeds size limit")
}
let rule = @json.parse(rule) catch {
_ => raise InvalidInput("invalid rule JSON")
}
let data = @json.parse(data) catch {
_ => raise InvalidInput("invalid data JSON")
}
apply(rule, data)
}
///|
/// JSONLogic considers the empty array false and the string "0" true.
pub fn truthy(value : Json) -> Bool {
match value {
Null | False => false
Number(n, ..) => n != 0.0 && !n.is_nan()
String(s) => !s.is_empty()
Array(a) => !a.is_empty()
_ => true
}
}
///|
fn pointer(path : String, key : String) -> String {
path + "/" + key.replace_all(old="~", new="~0").replace_all(old="/", new="~1")
}
///|
fn arg(values : Array[Json], i : Int, path : String) -> Json raise LogicError {
if i >= values.length() {
raise InvalidRule(path, "missing argument " + i.to_string())
}
values[i]
}
///|
fn number_result(n : Double, path : String) -> Json raise LogicError {
if n.is_nan() || n.is_inf() {
raise NonFiniteNumber(path)
}
n.to_json()
}
///|
/// Render an evaluation failure with its rule location.
pub fn LogicError::to_string(self : LogicError) -> String {
match self {
InvalidRule(path, message) => "invalid rule at " + path + ": " + message
LimitExceeded(path) => "evaluation limit exceeded at " + path
NonFiniteNumber(path) => "non-finite result at " + path
InvalidInput(message) => "invalid input: " + message
}
}
///|
fn has_unpaired_surrogate(s : String) -> Bool {
let mut i = 0
while i < s.length() {
let c = s[i]
if c >= 0xD800 && c <= 0xDBFF {
if i + 1 >= s.length() || s[i + 1] < 0xDC00 || s[i + 1] > 0xDFFF {
return true
}
i += 2
} else {
if c >= 0xDC00 && c <= 0xDFFF {
return true
}
i += 1
}
}
false
}
///|
fn string_slice(
s : String,
start : Int,
end : Int,
path : String,
) -> Json raise LogicError {
// Check code units before StringView::to_owned can normalize an incomplete pair.
if start < end &&
(
(s[start] >= 0xDC00 && s[start] <= 0xDFFF) ||
(s[end - 1] >= 0xD800 && s[end - 1] <= 0xDBFF)
) {
raise InvalidRule(path, "operation produces an unpaired UTF-16 surrogate")
}
s[start:end].to_owned().to_json()
}