use serde_json::Value;
pub const EXPRESSION_RUNTIME_VERSION: &str = "1";
#[derive(Debug, Clone)]
pub struct ExpressionError(pub String);
impl std::fmt::Display for ExpressionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for ExpressionError {}
fn err<T>(msg: impl Into<String>) -> Result<T, ExpressionError> {
Err(ExpressionError(msg.into()))
}
pub type Resolve<'a, C> =
&'a dyn Fn(&Value, &mut C, &mut dyn FnMut(&Value, &mut C) -> Result<f64, ExpressionError>)
-> Result<f64, ExpressionError>;
pub type ResolveRef<'a, C> = &'a dyn Fn(&Value, &mut C) -> Result<String, ExpressionError>;
pub type ClosureTable =
std::collections::HashMap<String, std::collections::HashMap<String, Vec<String>>>;
pub struct EvalOptions<'a, C> {
pub closures: Option<&'a ClosureTable>,
pub resolve_ref: Option<ResolveRef<'a, C>>,
}
#[derive(Debug, Clone)]
pub struct TraceNode {
pub typ: String,
pub value: f64,
pub children: Vec<TraceNode>,
pub left_ref: Option<String>,
pub right_ref: Option<String>,
}
enum Arity {
Unary { operand: &'static str },
Binary { left: &'static str, right: &'static str },
Nary { operands: &'static str },
Ternary { a: &'static str, b: &'static str, c: &'static str },
}
const ARITH: Arity = Arity::Binary { left: "arithLeft", right: "arithRight" };
const COMPARE: Arity = Arity::Binary { left: "compareLeft", right: "compareRight" };
const VALUE: Arity = Arity::Unary { operand: "value" };
fn operator_arity(typ: &str) -> Option<Arity> {
match typ {
"kanonak.org/transformations/Add"
| "kanonak.org/transformations/Subtract"
| "kanonak.org/transformations/Multiply"
| "kanonak.org/transformations/Divide"
| "kanonak.org/math/Power"
| "kanonak.org/math/Modulo"
| "kanonak.org/math/Minimum"
| "kanonak.org/math/Maximum" => Some(ARITH),
"kanonak.org/transformations/Abs"
| "kanonak.org/transformations/Negate"
| "kanonak.org/math/Exp"
| "kanonak.org/math/Ln"
| "kanonak.org/math/Log10"
| "kanonak.org/math/Sqrt"
| "kanonak.org/math/Floor"
| "kanonak.org/math/Ceil"
| "kanonak.org/math/Round"
| "kanonak.org/math/Sign" => Some(VALUE),
"kanonak.org/transformations/Equals"
| "kanonak.org/transformations/GreaterThan"
| "kanonak.org/transformations/LessThan"
| "kanonak.org/transformations/GreaterThanOrEqual"
| "kanonak.org/transformations/LessThanOrEqual" => Some(COMPARE),
"kanonak.org/transformations/And" | "kanonak.org/transformations/Or" => {
Some(Arity::Nary { operands: "operands" })
}
"kanonak.org/math/Clip" => {
Some(Arity::Ternary { a: "clipValue", b: "clipLower", c: "clipUpper" })
}
_ => None,
}
}
fn floored_mod(a: f64, b: f64) -> Result<f64, ExpressionError> {
if b == 0.0 {
return err("Modulo by zero");
}
Ok(a - b * (a / b).floor())
}
fn round_half_away(a: f64) -> f64 {
if a < 0.0 {
-(((-a) + 0.5).floor())
} else {
(a + 0.5).floor()
}
}
fn sign(x: f64) -> f64 {
if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
0.0
}
}
fn truthy(n: f64) -> bool {
n != 0.0
}
fn boolnum(b: bool) -> f64 {
if b {
1.0
} else {
0.0
}
}
fn unary(typ: &str, x: f64) -> Result<f64, ExpressionError> {
match typ {
"kanonak.org/transformations/Abs" => Ok(x.abs()),
"kanonak.org/transformations/Negate" => Ok(-x),
"kanonak.org/math/Exp" => Ok(x.exp()),
"kanonak.org/math/Ln" => {
if x > 0.0 {
Ok(x.ln())
} else {
err("Ln of a non-positive number")
}
}
"kanonak.org/math/Log10" => {
if x > 0.0 {
Ok(x.log10())
} else {
err("Log10 of a non-positive number")
}
}
"kanonak.org/math/Sqrt" => {
if x >= 0.0 {
Ok(x.sqrt())
} else {
err("Sqrt of a negative number")
}
}
"kanonak.org/math/Floor" => Ok(x.floor()),
"kanonak.org/math/Ceil" => Ok(x.ceil()),
"kanonak.org/math/Round" => Ok(round_half_away(x)),
"kanonak.org/math/Sign" => Ok(sign(x)),
_ => err(format!("{typ} has no unary primitive")),
}
}
fn binary(typ: &str, a: f64, b: f64) -> Result<f64, ExpressionError> {
match typ {
"kanonak.org/transformations/Add" => Ok(a + b),
"kanonak.org/transformations/Subtract" => Ok(a - b),
"kanonak.org/transformations/Multiply" => Ok(a * b),
"kanonak.org/transformations/Divide" => {
if b == 0.0 {
err("Divide by zero")
} else {
Ok(a / b)
}
}
"kanonak.org/math/Power" => Ok(a.powf(b)),
"kanonak.org/math/Modulo" => floored_mod(a, b),
"kanonak.org/math/Minimum" => Ok(a.min(b)),
"kanonak.org/math/Maximum" => Ok(a.max(b)),
"kanonak.org/transformations/Equals" => Ok(boolnum(a == b)),
"kanonak.org/transformations/GreaterThan" => Ok(boolnum(a > b)),
"kanonak.org/transformations/LessThan" => Ok(boolnum(a < b)),
"kanonak.org/transformations/GreaterThanOrEqual" => Ok(boolnum(a >= b)),
"kanonak.org/transformations/LessThanOrEqual" => Ok(boolnum(a <= b)),
_ => err(format!("{typ} has no binary primitive")),
}
}
fn literal_value(node: &Value, typ: &str) -> Option<f64> {
match typ {
"kanonak.org/transformations/IntegerLiteral" => node.get("integerLiteral").and_then(as_number),
"kanonak.org/transformations/DecimalLiteral" => node.get("decimalLiteral").and_then(as_number),
"kanonak.org/transformations/BooleanLiteral" => {
let v = node.get("booleanLiteral");
let truthy = matches!(v, Some(Value::Bool(true)))
|| matches!(v, Some(Value::String(s)) if s == "true");
Some(boolnum(truthy))
}
_ => None,
}
}
fn as_number(v: &Value) -> Option<f64> {
match v {
Value::Number(n) => n.as_f64(),
Value::String(s) => s.parse::<f64>().ok(),
Value::Bool(b) => Some(boolnum(*b)),
_ => None,
}
}
fn node_type(node: &Value) -> Result<&str, ExpressionError> {
match node.get("type").and_then(|t| t.as_str()) {
Some(t) => Ok(t),
None => err("node is missing a 'type'"),
}
}
fn operand<'a>(node: &'a Value, typ: &str, key: &str) -> Result<&'a Value, ExpressionError> {
match node.get(key) {
Some(v) if v.is_object() => Ok(v),
_ => err(format!("{typ} is missing operand '{key}'")),
}
}
fn identity_of<C>(
node: &Value,
ctx: &mut C,
options: Option<&EvalOptions<C>>,
) -> Result<String, ExpressionError> {
let typ = node_type(node)?;
if typ == "kanonak.org/transformations/UriLiteral" {
return match node.get("refTo").and_then(|v| v.as_str()) {
Some(s) if !s.is_empty() => Ok(s.to_string()),
_ => err("UriLiteral is missing refTo"),
};
}
match options.and_then(|o| o.resolve_ref) {
Some(resolve_ref) => resolve_ref(node, ctx),
None => err(format!("No resolveRef supplied for identity leaf '{typ}'")),
}
}
fn fold_ordered<C>(
node: &Value,
typ: &str,
ctx: &mut C,
options: Option<&EvalOptions<C>>,
) -> Result<(f64, String, String), ExpressionError> {
let via = match node.get("viaProperty").and_then(|v| v.as_str()) {
Some(s) if !s.is_empty() => s,
_ => return err(format!("{typ} is missing viaProperty")),
};
let left = identity_of(operand(node, typ, "compareLeft")?, ctx, options)?;
let right = identity_of(operand(node, typ, "compareRight")?, ctx, options)?;
let closure = match options.and_then(|o| o.closures).and_then(|c| c.get(via)) {
Some(c) => c,
None => return err(format!("No closure supplied for ordering property '{via}'")),
};
let value = if left == right {
boolnum(typ == "kanonak.org/transformations/IsAtLeast")
} else {
boolnum(closure.get(&left).map_or(false, |set| set.iter().any(|m| m == &right)))
};
Ok((value, left, right))
}
pub fn evaluate<C>(
node: &Value,
ctx: &mut C,
resolve: Resolve<C>,
) -> Result<f64, ExpressionError> {
evaluate_with_options(node, ctx, resolve, None)
}
pub fn evaluate_with_options<C>(
node: &Value,
ctx: &mut C,
resolve: Resolve<C>,
options: Option<&EvalOptions<C>>,
) -> Result<f64, ExpressionError> {
fn go<C>(
node: &Value,
ctx: &mut C,
resolve: Resolve<C>,
options: Option<&EvalOptions<C>>,
) -> Result<f64, ExpressionError> {
let typ = node_type(node)?;
if let Some(arity) = operator_arity(typ) {
return match arity {
Arity::Unary { operand: key } => {
let x = go(operand(node, typ, key)?, ctx, resolve, options)?;
unary(typ, x)
}
Arity::Binary { left, right } => {
let a = go(operand(node, typ, left)?, ctx, resolve, options)?;
let b = go(operand(node, typ, right)?, ctx, resolve, options)?;
binary(typ, a, b)
}
Arity::Nary { operands } => {
let items = match node.get(operands).and_then(|v| v.as_array()) {
Some(arr) => arr,
None => return err(format!("{typ} expects an '{operands}' list")),
};
let is_and = typ == "kanonak.org/transformations/And";
for item in items {
let v = truthy(go(item, ctx, resolve, options)?);
if is_and && !v {
return Ok(0.0);
}
if !is_and && v {
return Ok(1.0);
}
}
Ok(boolnum(is_and))
}
Arity::Ternary { a, b, c } => {
let v = go(operand(node, typ, a)?, ctx, resolve, options)?;
let lo = go(operand(node, typ, b)?, ctx, resolve, options)?;
let hi = go(operand(node, typ, c)?, ctx, resolve, options)?;
Ok(v.max(lo).min(hi))
}
};
}
if typ == "kanonak.org/transformations/Not" {
let inner = go(operand(node, typ, "operand")?, ctx, resolve, options)?;
return Ok(boolnum(!truthy(inner)));
}
if typ == "kanonak.org/transformations/IsAtLeast"
|| typ == "kanonak.org/transformations/Dominates"
{
return fold_ordered(node, typ, ctx, options).map(|(v, _, _)| v);
}
if let Some(lit) = literal_value(node, typ) {
return Ok(lit);
}
let mut recurse =
|n: &Value, c: &mut C| -> Result<f64, ExpressionError> { go(n, c, resolve, options) };
resolve(node, ctx, &mut recurse)
}
go(node, ctx, resolve, options)
}
pub fn explain<C>(
node: &Value,
ctx: &mut C,
resolve: Resolve<C>,
options: Option<&EvalOptions<C>>,
) -> Result<TraceNode, ExpressionError> {
fn leaf(typ: &str, value: f64) -> TraceNode {
TraceNode { typ: typ.to_string(), value, children: Vec::new(), left_ref: None, right_ref: None }
}
fn parent(typ: &str, value: f64, children: Vec<TraceNode>) -> TraceNode {
TraceNode { typ: typ.to_string(), value, children, left_ref: None, right_ref: None }
}
fn go<C>(
node: &Value,
ctx: &mut C,
resolve: Resolve<C>,
options: Option<&EvalOptions<C>>,
) -> Result<TraceNode, ExpressionError> {
let typ = node_type(node)?;
if let Some(arity) = operator_arity(typ) {
return match arity {
Arity::Unary { operand: key } => {
let x = go(operand(node, typ, key)?, ctx, resolve, options)?;
let value = unary(typ, x.value)?;
Ok(parent(typ, value, vec![x]))
}
Arity::Binary { left, right } => {
let a = go(operand(node, typ, left)?, ctx, resolve, options)?;
let b = go(operand(node, typ, right)?, ctx, resolve, options)?;
let value = binary(typ, a.value, b.value)?;
Ok(parent(typ, value, vec![a, b]))
}
Arity::Nary { operands } => {
let items = match node.get(operands).and_then(|v| v.as_array()) {
Some(arr) => arr,
None => return err(format!("{typ} expects an '{operands}' list")),
};
let is_and = typ == "kanonak.org/transformations/And";
let mut children = Vec::new();
for item in items {
let child = go(item, ctx, resolve, options)?;
let v = truthy(child.value);
children.push(child);
if is_and && !v {
return Ok(parent(typ, 0.0, children));
}
if !is_and && v {
return Ok(parent(typ, 1.0, children));
}
}
Ok(parent(typ, boolnum(is_and), children))
}
Arity::Ternary { a, b, c } => {
let v = go(operand(node, typ, a)?, ctx, resolve, options)?;
let lo = go(operand(node, typ, b)?, ctx, resolve, options)?;
let hi = go(operand(node, typ, c)?, ctx, resolve, options)?;
let value = v.value.max(lo.value).min(hi.value);
Ok(parent(typ, value, vec![v, lo, hi]))
}
};
}
if typ == "kanonak.org/transformations/Not" {
let x = go(operand(node, typ, "operand")?, ctx, resolve, options)?;
let value = boolnum(!truthy(x.value));
return Ok(parent(typ, value, vec![x]));
}
if typ == "kanonak.org/transformations/IsAtLeast"
|| typ == "kanonak.org/transformations/Dominates"
{
let (value, left, right) = fold_ordered(node, typ, ctx, options)?;
return Ok(TraceNode {
typ: typ.to_string(),
value,
children: Vec::new(),
left_ref: Some(left),
right_ref: Some(right),
});
}
if let Some(lit) = literal_value(node, typ) {
return Ok(leaf(typ, lit));
}
let mut recurse = |n: &Value, c: &mut C| -> Result<f64, ExpressionError> {
evaluate_with_options(n, c, resolve, options)
};
let value = resolve(node, ctx, &mut recurse)?;
Ok(leaf(typ, value))
}
go(node, ctx, resolve, options)
}