///|
fn require_arity(
name : String,
actual : Int,
expected : Int,
) -> Unit raise JmesPathError {
if actual != expected {
fail(
"invalid_arity",
0,
"\{name} expects \{expected} argument(s), got \{actual}",
)
}
}
///|
fn require_min_arity(
name : String,
actual : Int,
minimum : Int,
) -> Unit raise JmesPathError {
if actual < minimum {
fail(
"invalid_arity",
0,
"\{name} expects at least \{minimum} argument(s), got \{actual}",
)
}
}
///|
fn[T] invalid_type(name : String) -> T raise JmesPathError {
fail("invalid_type", 0, "invalid argument type for " + name)
}
///|
fn evaluate_args(
args : Array[Expr],
data : Json,
state : EvalState,
) -> Array[Json] raise JmesPathError {
args.map(arg => evaluate(arg, data, state))
}
///|
fn homogeneous_order(
values : Array[Json],
name : String,
) -> Int raise JmesPathError {
if values.is_empty() {
return 0
}
match values[0] {
Number(_, ..) => {
for value in values {
if !(value is Number(_, ..)) {
invalid_type(name) |> ignore
}
}
1
}
String(_) => {
for value in values {
if !(value is String(_)) {
invalid_type(name) |> ignore
}
}
2
}
_ => invalid_type(name)
}
}
///|
fn compare_ordered(left : Json, right : Json, kind : Int) -> Int {
if kind == 1 {
guard left is Number(a, ..) && right is Number(b, ..) else { return 0 }
if a < b {
-1
} else if a > b {
1
} else {
0
}
} else {
guard left is String(a) && right is String(b) else { return 0 }
a.lexical_compare(b)
}
}
///|
fn function_length(value : Json) -> Json raise JmesPathError {
match value {
String(text) => Json::number(text.to_array().length().to_double())
Array(items) => Json::number(items.length().to_double())
Object(entries) => Json::number(entries.length().to_double())
_ => invalid_type("length")
}
}
///|
fn function_contains(subject : Json, search : Json) -> Json raise JmesPathError {
match subject {
String(text) =>
match search {
String(needle) => Json::boolean(text.contains(needle))
_ => invalid_type("contains")
}
Array(items) => Json::boolean(items.any(fn(item) { item == search }))
_ => invalid_type("contains")
}
}
///|
fn function_numeric_array(
name : String,
value : Json,
average : Bool,
) -> Json raise JmesPathError {
guard value is Array(items) else { return invalid_type(name) }
let mut total = 0.0
for item in items {
guard item is Number(number, ..) else { return invalid_type(name) }
total += number
}
if average {
if items.is_empty() {
Json::null()
} else {
Json::number(total / items.length().to_double())
}
} else {
Json::number(total)
}
}
///|
fn function_extreme(
name : String,
value : Json,
maximum : Bool,
) -> Json raise JmesPathError {
guard value is Array(items) else { return invalid_type(name) }
if items.is_empty() {
return Json::null()
}
let kind = homogeneous_order(items, name)
let mut best = items[0]
for i = 1; i < items.length(); i = i + 1 {
let order = compare_ordered(items[i], best, kind)
if (maximum && order > 0) || (!maximum && order < 0) {
best = items[i]
}
}
best
}
///|
fn function_sort(value : Json) -> Json raise JmesPathError {
guard value is Array(items) else { return invalid_type("sort") }
let result = items.copy()
let kind = homogeneous_order(result, "sort")
if kind != 0 {
result.sort_by(fn(a, b) { compare_ordered(a, b, kind) })
}
Json::array(result)
}
///|
fn json_type(value : Json) -> String {
match value {
Null => "null"
True | False => "boolean"
Number(_, ..) => "number"
String(_) => "string"
Array(_) => "array"
Object(_) => "object"
}
}
///|
fn evaluate_expref_function(
name : String,
args : Array[Expr],
data : Json,
state : EvalState,
) -> Json raise JmesPathError {
require_arity(name, args.length(), 2)
let (array_expr, reference) = if name == "map" {
guard args[0] is ExpressionRef(reference) else { return invalid_type(name) }
(args[1], reference)
} else {
guard args[1] is ExpressionRef(reference) else { return invalid_type(name) }
(args[0], reference)
}
guard evaluate(array_expr, data, state) is Array(items) else {
return invalid_type(name)
}
if name == "map" {
let mapped = items.map(item => evaluate(reference, item, state))
state.add_results(mapped.length())
return Json::array(mapped)
}
if items.is_empty() {
return if name == "sort_by" { Json::array([]) } else { Json::null() }
}
let pairs : Array[(Json, Json)] = []
for item in items {
pairs.push((item, evaluate(reference, item, state)))
}
let keys = pairs.map(pair => pair.1)
let kind = homogeneous_order(keys, name)
if name == "sort_by" {
pairs.sort_by(fn(a, b) { compare_ordered(a.1, b.1, kind) })
return Json::array(pairs.map(pair => pair.0))
}
let maximum = name == "max_by"
let mut best = pairs[0]
for i = 1; i < pairs.length(); i = i + 1 {
let order = compare_ordered(pairs[i].1, best.1, kind)
if (maximum && order > 0) || (!maximum && order < 0) {
best = pairs[i]
}
}
best.0
}
///|
fn evaluate_function(
name : String,
args : Array[Expr],
data : Json,
state : EvalState,
) -> Json raise JmesPathError {
if name == "map" || name == "sort_by" || name == "max_by" || name == "min_by" {
return evaluate_expref_function(name, args, data, state)
}
let values = evaluate_args(args, data, state)
match name {
"abs" => {
require_arity(name, values.length(), 1)
guard values[0] is Number(value, ..) else { return invalid_type(name) }
Json::number(value.abs())
}
"avg" => {
require_arity(name, values.length(), 1)
function_numeric_array(name, values[0], true)
}
"ceil" => {
require_arity(name, values.length(), 1)
guard values[0] is Number(value, ..) else { return invalid_type(name) }
Json::number(value.ceil())
}
"contains" => {
require_arity(name, values.length(), 2)
function_contains(values[0], values[1])
}
"ends_with" => {
require_arity(name, values.length(), 2)
guard values[0] is String(subject) && values[1] is String(suffix) else {
return invalid_type(name)
}
Json::boolean(subject.has_suffix(suffix))
}
"floor" => {
require_arity(name, values.length(), 1)
guard values[0] is Number(value, ..) else { return invalid_type(name) }
Json::number(value.floor())
}
"join" => {
require_arity(name, values.length(), 2)
guard values[0] is String(separator) && values[1] is Array(items) else {
return invalid_type(name)
}
let strings : Array[String] = []
for item in items {
guard item is String(text) else { return invalid_type(name) }
strings.push(text)
}
Json::string(strings.join(separator))
}
"keys" => {
require_arity(name, values.length(), 1)
guard values[0] is Object(entries) else { return invalid_type(name) }
let keys : Array[Json] = []
for entry in entries {
keys.push(Json::string(entry.0))
}
Json::array(keys)
}
"length" => {
require_arity(name, values.length(), 1)
function_length(values[0])
}
"max" => {
require_arity(name, values.length(), 1)
function_extreme(name, values[0], true)
}
"merge" => {
require_min_arity(name, values.length(), 1)
let result : Map[String, Json] = Map([])
for value in values {
guard value is Object(entries) else { return invalid_type(name) }
for entry in entries {
result[entry.0] = entry.1
}
}
Json::object(result)
}
"min" => {
require_arity(name, values.length(), 1)
function_extreme(name, values[0], false)
}
"not_null" => {
require_min_arity(name, values.length(), 1)
for value in values {
if !(value is Null) {
return value
}
}
Json::null()
}
"reverse" => {
require_arity(name, values.length(), 1)
match values[0] {
Array(items) => Json::array(items.rev())
String(text) => Json::string(String::from_array(text.to_array().rev()))
_ => invalid_type(name)
}
}
"sort" => {
require_arity(name, values.length(), 1)
function_sort(values[0])
}
"starts_with" => {
require_arity(name, values.length(), 2)
guard values[0] is String(subject) && values[1] is String(prefix) else {
return invalid_type(name)
}
Json::boolean(subject.has_prefix(prefix))
}
"sum" => {
require_arity(name, values.length(), 1)
function_numeric_array(name, values[0], false)
}
"to_array" => {
require_arity(name, values.length(), 1)
match values[0] {
Array(_) => values[0]
value => Json::array([value])
}
}
"to_number" => {
require_arity(name, values.length(), 1)
match values[0] {
Number(_, ..) => values[0]
String(text) => {
let parsed = @json.parse(text) catch { _ => return Json::null() }
if parsed is Number(_, ..) {
parsed
} else {
Json::null()
}
}
_ => Json::null()
}
}
"to_string" => {
require_arity(name, values.length(), 1)
match values[0] {
String(_) => values[0]
value => Json::string(value.stringify())
}
}
"type" => {
require_arity(name, values.length(), 1)
Json::string(json_type(values[0]))
}
"values" => {
require_arity(name, values.length(), 1)
guard values[0] is Object(entries) else { return invalid_type(name) }
let result : Array[Json] = []
for entry in entries {
result.push(entry.1)
}
Json::array(result)
}
_ => fail("unknown_function", 0, "unknown function: " + name)
}
}