// Ported from the operator/evaluation semantics of json-logic-js 2.0.5.
// Original copyright (c) 2015 Jeremy Wadhams, MIT; see third_party/json-logic-js/LICENSE.
///|
fn read_var(
data : Json,
key : Json,
fallback : Json,
path : String,
) -> Json raise LogicError {
if key == Json::null() || key == "".to_json() {
return data
}
let mut current = data
for part in js_string(key).split(".") {
let part = part.to_owned()
match current {
Object(fields) =>
match fields.get(part) {
Some(value) => current = value
None => return fallback
}
Array(items) => {
if part == "length" {
current = items.length().to_json()
continue
}
let i = @string.parse_int(part) catch { _ => return fallback }
if i < 0 || i >= items.length() || i.to_string() != part {
return fallback
}
current = items[i]
}
String(s) => {
if part == "length" {
current = s.length().to_json()
continue
}
let i = @string.parse_int(part) catch { _ => return fallback }
if i < 0 || i >= s.length() || i.to_string() != part {
return fallback
}
current = string_slice(s, i, i + 1, path)
}
_ => return fallback
}
}
current
}
///|
fn eval(
rule : Json,
data : Json,
ctx : Context,
depth : Int,
path : String,
) -> Json raise LogicError {
ctx.visit(depth, path)
let (op, raw) = match rule {
Array(xs) => {
let result : Array[Json] = []
for i, x in xs {
result.push(eval(x, data, ctx, depth + 1, pointer(path, i.to_string())))
}
return result.to_json()
}
Object(fields) => {
if fields.length() != 1 {
return rule
}
let key = fields.keys().to_array()[0]
(key, fields[key])
}
_ => return rule
}
let values = match raw {
Array(xs) => xs
_ => [raw]
}
let op_path = pointer(path, op)
let child_path = (i : Int) => {
match raw {
Array(_) => pointer(op_path, i.to_string())
_ => op_path
}
}
match op {
"if" | "?:" => {
let mut i = 0
while i + 1 < values.length() {
if truthy(eval(values[i], data, ctx, depth + 1, child_path(i))) {
return eval(values[i + 1], data, ctx, depth + 1, child_path(i + 1))
}
i += 2
}
return if i < values.length() {
eval(values[i], data, ctx, depth + 1, child_path(i))
} else {
Json::null()
}
}
"and" | "or" => {
if values.is_empty() {
raise InvalidRule(
path, "empty and/or would produce JavaScript undefined",
)
}
let mut result = Json::null()
for i, value in values {
result = eval(value, data, ctx, depth + 1, child_path(i))
if (op == "and" && !truthy(result)) || (op == "or" && truthy(result)) {
return result
}
}
return result
}
"map" | "filter" | "all" | "some" | "none" | "reduce" => {
let source = eval(
arg(values, 0, path),
data,
ctx,
depth + 1,
child_path(0),
)
let scoped = arg(values, 1, path)
let items = match source {
Array(xs) => xs
_ => []
}
if op == "reduce" {
let mut acc = if values.length() > 2 {
eval(values[2], data, ctx, depth + 1, child_path(2))
} else {
Json::null()
}
for item in items {
let scope : Json = { "current": item, "accumulator": acc }
acc = eval(scoped, scope, ctx, depth + 1, child_path(1))
}
return acc
}
let output : Array[Json] = []
for item in items {
let result = eval(scoped, item, ctx, depth + 1, child_path(1))
match op {
"map" => output.push(result)
"filter" => if truthy(result) { output.push(item) }
"all" => if !truthy(result) { return false.to_json() }
"some" => if truthy(result) { return true.to_json() }
"none" => if truthy(result) { return false.to_json() }
_ => ()
}
}
return match op {
"map" | "filter" => output.to_json()
"all" => (!items.is_empty()).to_json()
"none" => true.to_json()
_ => false.to_json()
}
}
_ => ()
}
let args : Array[Json] = []
for i, value in values {
args.push(eval(value, data, ctx, depth + 1, child_path(i)))
}
simple_op(op, args, data, ctx, depth, path)
}
///|
fn simple_op(
op : String,
args : Array[Json],
data : Json,
ctx : Context,
depth : Int,
path : String,
) -> Json raise LogicError {
match op {
"var" =>
read_var(
data,
args.get(0).unwrap_or(Json::null()),
args.get(1).unwrap_or(Json::null()),
path,
)
"!" | "!!" =>
(if op == "!" {
!truthy(arg(args, 0, path))
} else {
truthy(arg(args, 0, path))
}).to_json()
"==" | "!=" | "===" | "!==" => {
let a = arg(args, 0, path)
let b = arg(args, 1, path)
let equal = if op == "===" || op == "!==" {
strict_equal(a, b)
} else {
loose_equal(a, b)
}
(if op == "!=" || op == "!==" { !equal } else { equal }).to_json()
}
">" | ">=" | "<" | "<=" => {
let a = arg(args, 0, path)
let b = arg(args, 1, path)
let first = relational(op, a, b)
(first &&
(
args.length() < 3 ||
op == ">" ||
op == ">=" ||
relational(op, b, args[2])
)).to_json()
}
"+" | "*" => {
if op == "*" && args.is_empty() {
raise InvalidRule(path, "multiplication requires an argument")
}
// JS reduce without an initial value preserves a single * operand's type.
if op == "*" && args.length() == 1 {
return args[0]
}
let mut result = if op == "+" { 0.0 } else { 1.0 }
for value in args {
let n = parse_float(value)
result = if op == "+" { result + n } else { result * n }
}
number_result(result, path)
}
"-" | "/" | "%" => {
let a = numeric(arg(args, 0, path))
if op == "-" && args.length() == 1 {
return number_result(-a, path)
}
let b = numeric(arg(args, 1, path))
number_result(
match op {
"-" => a - b
"/" => a / b
_ => a % b
},
path,
)
}
"min" | "max" => {
if args.is_empty() {
raise NonFiniteNumber(path)
}
let mut result = numeric(args[0])
for value in args {
let n = numeric(value)
if n.is_nan() {
raise NonFiniteNumber(path)
}
if (op == "min" && n < result) || (op == "max" && n > result) {
result = n
}
}
number_result(result, path)
}
"cat" =>
args
.map(x => if x == Json::null() { "" } else { js_string(x) })
.join("")
.to_json()
"substr" => {
let source = js_string(arg(args, 0, path))
let len = source.length()
let start_n = numeric(args.get(1).unwrap_or(0))
let start = clamped_index(start_n, len)
let end = if args.length() < 3 {
len
} else {
let n = numeric(args[2])
let available = len - start
let count = if n < 0.0 { available.to_double() + n } else { n }
if count.is_nan() || count <= 0.0 {
start
} else if count >= available.to_double() {
len
} else {
start + count.to_int()
}
}
string_slice(source, start, end, path)
}
"in" => {
let needle = arg(args, 0, path)
match arg(args, 1, path) {
Array(xs) => xs.any(x => strict_equal(x, needle)).to_json()
String(s) => s.contains(js_string(needle)).to_json()
_ => false.to_json()
}
}
"merge" => {
let result : Array[Json] = []
for value in args {
match value {
Array(xs) =>
for x in xs {
ctx.visit(depth + 1, path)
result.push(x)
}
_ => {
ctx.visit(depth + 1, path)
result.push(value)
}
}
}
result.to_json()
}
"missing" | "missing_some" => {
let keys = if op == "missing_some" {
match arg(args, 1, path) {
Array(xs) => xs
_ => raise InvalidRule(path, "missing_some requires an array of keys")
}
} else {
match args.get(0) {
Some(Array(xs)) => xs
_ => args
}
}
let missing : Array[Json] = []
for key in keys {
ctx.visit(depth + 1, path)
let value = read_var(data, key, Json::null(), path)
if value == Json::null() || value == "".to_json() {
missing.push(key)
}
}
if op == "missing_some" &&
(keys.length() - missing.length()).to_double() >= numeric(args[0]) {
([] : Array[Json]).to_json()
} else {
missing.to_json()
}
}
"log" => {
let value = arg(args, 0, path)
ctx.logs.push(value)
value
}
_ => raise InvalidRule(path, "unrecognized operation: " + op)
}
}
///|
fn clamped_index(value : Double, len : Int) -> Int {
if value.is_nan() {
return 0
}
if value < -len.to_double() {
return 0
}
if value > len.to_double() {
return len
}
let n = value.to_int()
if n < 0 {
len + n
} else {
n
}
}