///|
fn value_kind(value : Json) -> String {
match value {
Null => "null"
True | False => "boolean"
Number(_) => "number"
String(_) => "string"
Array(_) => "array"
Object(_) => "object"
}
}
///|
fn[T] eval_fail(
code : String,
message : String,
span : Span,
hint? : String,
) -> T raise RuleFailure {
raise RuleFailure(Diagnostic::new(Evaluate, code, message, span, hint?))
}
///|
fn expect_bool(value : Json, span : Span) -> Bool raise RuleFailure {
match value {
True => true
False => false
other =>
eval_fail(
"E001",
"expected boolean, found \{value_kind(other)}",
span,
hint="Boolean operators only accept true or false.",
)
}
}
///|
fn expect_number(value : Json, span : Span) -> Double raise RuleFailure {
match value {
Number(number, ..) => number
other =>
eval_fail("E002", "expected number, found \{value_kind(other)}", span)
}
}
///|
fn expect_string(value : Json, span : Span) -> String raise RuleFailure {
match value {
String(text) => text
other =>
eval_fail("E003", "expected string, found \{value_kind(other)}", span)
}
}
///|
fn compare_values(
left : Json,
right : Json,
span : Span,
) -> Int raise RuleFailure {
match (left, right) {
(Number(a, ..), Number(b, ..)) =>
if a < b {
-1
} else if a > b {
1
} else {
0
}
(String(a), String(b)) => a.lexical_compare(b)
_ =>
eval_fail(
"E004",
"values of type \{value_kind(left)} and \{value_kind(right)} cannot be ordered",
span,
hint="Ordering comparisons require two numbers or two strings.",
)
}
}
///|
fn evaluate_member(
target : Json,
name : String,
span : Span,
) -> Json raise RuleFailure {
match target {
Object(fields) =>
match fields.get(name) {
Some(value) => value
None =>
eval_fail(
"E005",
"field `\{name}` does not exist",
span,
hint="Check the rule path and the input JSON shape.",
)
}
other =>
eval_fail(
"E006",
"cannot read field `\{name}` from \{value_kind(other)}",
span,
)
}
}
///|
fn evaluate_optional_variable(
context : Json,
name : String,
span : Span,
) -> Json raise RuleFailure {
match context {
Object(fields) => fields.get(name).unwrap_or(Json::null())
other =>
eval_fail(
"E006",
"cannot read optional field `\{name}` from \{value_kind(other)}",
span,
)
}
}
///|
fn evaluate_optional_member(
target : Json,
name : String,
span : Span,
) -> Json raise RuleFailure {
match target {
Null => Json::null()
Object(fields) => fields.get(name).unwrap_or(Json::null())
other =>
eval_fail(
"E006",
"cannot read optional field `\{name}` from \{value_kind(other)}",
span,
hint="Optional access suppresses missing and null values, not type errors.",
)
}
}
///|
fn evaluate_index(
target : Json,
index : Json,
span : Span,
) -> Json raise RuleFailure {
match (target, index) {
(Array(values), Number(number, ..)) => {
let position = number.to_int()
if Double::from_int(position) != number {
eval_fail("E007", "array index must be an integer", span)
}
match values.get(position) {
Some(value) => value
None =>
eval_fail("E008", "array index \{position} is out of bounds", span)
}
}
(Object(fields), String(key)) =>
match fields.get(key) {
Some(value) => value
None => eval_fail("E005", "field `\{key}` does not exist", span)
}
(Array(_), other) =>
eval_fail(
"E007",
"array index must be a number, found \{value_kind(other)}",
span,
)
(Object(_), other) =>
eval_fail(
"E009",
"object index must be a string, found \{value_kind(other)}",
span,
)
(other, _) =>
eval_fail("E010", "cannot index a \{value_kind(other)} value", span)
}
}
///|
fn evaluate_optional_index(
target : Json,
index : Json,
span : Span,
) -> Json raise RuleFailure {
match (target, index) {
(Null, _) => Json::null()
(Array(values), Number(number, ..)) => {
let position = number.to_int()
if Double::from_int(position) != number {
eval_fail("E007", "array index must be an integer", span)
}
values.get(position).unwrap_or(Json::null())
}
(Object(fields), String(key)) => fields.get(key).unwrap_or(Json::null())
(Array(_), other) =>
eval_fail(
"E007",
"array index must be a number, found \{value_kind(other)}",
span,
)
(Object(_), other) =>
eval_fail(
"E009",
"object index must be a string, found \{value_kind(other)}",
span,
)
(other, _) =>
eval_fail(
"E010",
"cannot optionally index a \{value_kind(other)} value",
span,
hint="Optional access suppresses missing and null values, not type errors.",
)
}
}
///|
fn evaluate_optional_base(
expression : Expr,
context : Json,
budget : EvaluationBudget,
) -> Json raise RuleFailure {
match expression {
Variable(name, span) => {
budget.consume(span)
evaluate_optional_variable(context, name, span)
}
other => evaluate_expr(other, context, budget)
}
}
///|
fn require_arity(
name : String,
arguments : Array[Json],
expected : Int,
span : Span,
) -> Unit raise RuleFailure {
if arguments.length() != expected {
eval_fail(
"E011",
"function `\{name}` expects \{expected} argument(s), received \{arguments.length()}",
span,
)
}
}
///|
fn expect_object(
value : Json,
span : Span,
) -> Map[String, Json] raise RuleFailure {
match value {
Object(fields) => fields
other =>
eval_fail("E025", "expected object, found \{value_kind(other)}", span)
}
}
///|
fn evaluate_number_array(
name : String,
value : Json,
span : Span,
) -> Array[Json] raise RuleFailure {
match value {
Array(values) => values
other =>
eval_fail(
"E026",
"function `\{name}` expects an array, found \{value_kind(other)}",
span,
)
}
}
///|
fn evaluate_boolean_array(
name : String,
value : Json,
span : Span,
) -> Array[Json] raise RuleFailure {
match value {
Array(values) => values
other =>
eval_fail(
"E027",
"function `\{name}` expects an array, found \{value_kind(other)}",
span,
)
}
}
///|
fn evaluate_call(
name : String,
arguments : Array[Json],
span : Span,
) -> Json raise RuleFailure {
match name {
"len" => {
require_arity(name, arguments, 1, span)
let length = match arguments[0] {
String(value) => value.char_length()
Array(values) => values.length()
Object(fields) => fields.length()
other =>
eval_fail("E012", "len() does not accept \{value_kind(other)}", span)
}
Json::number(Double::from_int(length), repr=length.to_string())
}
"contains" => {
require_arity(name, arguments, 2, span)
let result = match (arguments[0], arguments[1]) {
(String(text), String(part)) => text.contains(part)
(Array(values), needle) => values.contains(needle)
(Object(fields), String(key)) => fields.contains(key)
(container, _) =>
eval_fail(
"E013",
"contains() does not support \{value_kind(container)} with this value",
span,
)
}
Json::boolean(result)
}
"has" => {
require_arity(name, arguments, 2, span)
let fields = expect_object(arguments[0], span)
let key = expect_string(arguments[1], span)
Json::boolean(fields.contains(key))
}
"get" => {
require_arity(name, arguments, 3, span)
let fields = expect_object(arguments[0], span)
let key = expect_string(arguments[1], span)
match fields.get(key) {
Some(value) => value
None => arguments[2]
}
}
"starts_with" => {
require_arity(name, arguments, 2, span)
let text = expect_string(arguments[0], span)
let prefix = expect_string(arguments[1], span)
Json::boolean(text.has_prefix(prefix))
}
"ends_with" => {
require_arity(name, arguments, 2, span)
let text = expect_string(arguments[0], span)
let suffix = expect_string(arguments[1], span)
Json::boolean(text.has_suffix(suffix))
}
"lower" => {
require_arity(name, arguments, 1, span)
Json::string(expect_string(arguments[0], span).to_lower())
}
"upper" => {
require_arity(name, arguments, 1, span)
Json::string(expect_string(arguments[0], span).to_upper())
}
"abs" => {
require_arity(name, arguments, 1, span)
Json::number(expect_number(arguments[0], span).abs())
}
"min" => {
require_arity(name, arguments, 2, span)
let left = expect_number(arguments[0], span)
let right = expect_number(arguments[1], span)
Json::number(if left < right { left } else { right })
}
"max" => {
require_arity(name, arguments, 2, span)
let left = expect_number(arguments[0], span)
let right = expect_number(arguments[1], span)
Json::number(if left > right { left } else { right })
}
"clamp" => {
require_arity(name, arguments, 3, span)
let value = expect_number(arguments[0], span)
let minimum = expect_number(arguments[1], span)
let maximum = expect_number(arguments[2], span)
if minimum > maximum {
eval_fail(
"E028",
"clamp() minimum must not exceed maximum",
span,
hint="Swap the range bounds or correct the configured values.",
)
}
Json::number(
if value < minimum {
minimum
} else if value > maximum {
maximum
} else {
value
},
)
}
"sum" => {
require_arity(name, arguments, 1, span)
let values = evaluate_number_array(name, arguments[0], span)
let mut total = 0.0
for value in values {
total = total + expect_number(value, span)
}
Json::number(total)
}
"matches" => {
require_arity(name, arguments, 2, span)
let text = expect_string(arguments[0], span)
let pattern = expect_string(arguments[1], span)
if pattern.char_length() > 512 {
eval_fail(
"E029",
"regular expression exceeds the 512-character safety limit",
span,
hint="Use a shorter pattern or split the validation into multiple rules.",
)
}
if text.char_length() > 65536 {
eval_fail(
"E030",
"regular-expression input exceeds the 65536-character safety limit",
span,
hint="Validate or truncate large input before applying matches().",
)
}
let regex = @string.Regex::Regex(pattern) catch {
_ =>
eval_fail(
"E023",
"invalid regular expression",
span,
hint="MoonRule uses MoonBit's regular expression syntax.",
)
}
Json::boolean(regex.execute(text) is Some(_))
}
"is_date" => {
require_arity(name, arguments, 1, span)
Json::boolean(is_iso_date_string(expect_string(arguments[0], span)))
}
"is_datetime" => {
require_arity(name, arguments, 1, span)
Json::boolean(
is_rfc3339_datetime_string(expect_string(arguments[0], span)),
)
}
"is_ipv4" => {
require_arity(name, arguments, 1, span)
Json::boolean(is_ipv4_string(expect_string(arguments[0], span)))
}
"is_email" => {
require_arity(name, arguments, 1, span)
Json::boolean(is_email_string(expect_string(arguments[0], span)))
}
"is_uuid" => {
require_arity(name, arguments, 1, span)
Json::boolean(is_uuid_string(expect_string(arguments[0], span)))
}
"is_semver" => {
require_arity(name, arguments, 1, span)
Json::boolean(is_semver_string(expect_string(arguments[0], span)))
}
"exists" => {
require_arity(name, arguments, 1, span)
Json::boolean(arguments[0] != Json::null())
}
"type_of" => {
require_arity(name, arguments, 1, span)
Json::string(value_kind(arguments[0]))
}
"coalesce" => {
if arguments.is_empty() || arguments.length() > 32 {
eval_fail(
"E011",
"function `coalesce` expects 1..32 argument(s), received \{arguments.length()}",
span,
)
}
let mut result = Json::null()
for value in arguments {
if value != Json::null() {
result = value
break
}
}
result
}
"all" => {
require_arity(name, arguments, 1, span)
let values = evaluate_boolean_array(name, arguments[0], span)
let mut result = true
for value in values {
if !expect_bool(value, span) {
result = false
break
}
}
Json::boolean(result)
}
"any" => {
require_arity(name, arguments, 1, span)
let values = evaluate_boolean_array(name, arguments[0], span)
let mut result = false
for value in values {
if expect_bool(value, span) {
result = true
break
}
}
Json::boolean(result)
}
_ =>
eval_fail(
"E014",
"unknown function `\{name}`",
span,
hint="Inspect builtin_functions() for the complete supported function catalog.",
)
}
}
///|
fn evaluate_binary(
left : Json,
operator : BinaryOp,
right : Json,
span : Span,
) -> Json raise RuleFailure {
match operator {
Equal => Json::boolean(left == right)
NotEqual => Json::boolean(left != right)
Less => Json::boolean(compare_values(left, right, span) < 0)
LessEqual => Json::boolean(compare_values(left, right, span) <= 0)
Greater => Json::boolean(compare_values(left, right, span) > 0)
GreaterEqual => Json::boolean(compare_values(left, right, span) >= 0)
In => {
let result = match right {
Array(values) => values.contains(left)
Object(fields) =>
match left {
String(key) => fields.contains(key)
other =>
eval_fail(
"E015",
"object membership requires a string key, found \{value_kind(other)}",
span,
)
}
String(text) =>
match left {
String(part) => text.contains(part)
other =>
eval_fail(
"E016",
"string membership requires a string, found \{value_kind(other)}",
span,
)
}
other =>
eval_fail(
"E017",
"right side of `in` must be an array, object, or string; found \{value_kind(other)}",
span,
)
}
Json::boolean(result)
}
Add =>
match (left, right) {
(Number(a, ..), Number(b, ..)) => Json::number(a + b)
(String(a), String(b)) => Json::string(a + b)
_ => eval_fail("E018", "`+` requires two numbers or two strings", span)
}
Subtract =>
Json::number(expect_number(left, span) - expect_number(right, span))
Multiply =>
Json::number(expect_number(left, span) * expect_number(right, span))
Divide => {
let divisor = expect_number(right, span)
if divisor == 0.0 {
eval_fail("E019", "division by zero", span)
}
Json::number(expect_number(left, span) / divisor)
}
Remainder => {
let a = expect_number(left, span)
let b = expect_number(right, span)
if b == 0.0 {
eval_fail("E020", "remainder by zero", span)
}
Json::number(a - (a / b).trunc() * b)
}
Or | And => eval_fail("E021", "internal boolean evaluation error", span)
}
}
///|
fn evaluate_expr(
expr : Expr,
context : Json,
budget : EvaluationBudget,
) -> Json raise RuleFailure {
budget.consume(expr.span())
match expr {
Literal(value, _) => value
Variable(name, span) => evaluate_member(context, name, span)
ArrayLiteral(expressions, _) =>
Json::array(
expressions.map(value => evaluate_expr(value, context, budget)),
)
Member(target, name, span) =>
evaluate_member(evaluate_expr(target, context, budget), name, span)
Index(target, index, span) =>
evaluate_index(
evaluate_expr(target, context, budget),
evaluate_expr(index, context, budget),
span,
)
OptionalMember(target, name, span) =>
evaluate_optional_member(
evaluate_optional_base(target, context, budget),
name,
span,
)
OptionalIndex(target, index, span) => {
let target_value = evaluate_optional_base(target, context, budget)
if target_value == Json::null() {
Json::null()
} else {
evaluate_optional_index(
target_value,
evaluate_expr(index, context, budget),
span,
)
}
}
Call(name, arguments, span) =>
evaluate_call(
name,
arguments.map(argument => evaluate_expr(argument, context, budget)),
span,
)
Unary(operator, operand, span) => {
let value = evaluate_expr(operand, context, budget)
match operator {
Not => Json::boolean(!expect_bool(value, span))
Negate => Json::number(-expect_number(value, span))
}
}
Binary(left, Or, right, _) => {
let left_value = expect_bool(
evaluate_expr(left, context, budget),
left.span(),
)
if left_value {
Json::boolean(true)
} else {
Json::boolean(
expect_bool(evaluate_expr(right, context, budget), right.span()),
)
}
}
Binary(left, And, right, _) => {
let left_value = expect_bool(
evaluate_expr(left, context, budget),
left.span(),
)
if !left_value {
Json::boolean(false)
} else {
Json::boolean(
expect_bool(evaluate_expr(right, context, budget), right.span()),
)
}
}
Binary(left, operator, right, span) =>
evaluate_binary(
evaluate_expr(left, context, budget),
operator,
evaluate_expr(right, context, budget),
span,
)
}
}