use std::collections::BTreeMap;
use maplibre_expr::{
evaluate, evaluate_with, parse_with, EvaluationContext, Feature, Options, Value,
};
use serde_json::json;
fn feature_with(key: &str, value: Value) -> EvaluationContext {
let mut props = BTreeMap::new();
props.insert(key.to_string(), value);
EvaluationContext::new().with_feature(Feature {
properties: props,
..Feature::default()
})
}
#[test]
fn macro_expands_at_parse_time() {
let mut opts = Options::new();
opts.macro_def("double", vec!["x".into()], json!(["*", ["var", "x"], 2]));
let expr = parse_with(&json!(["double", ["get", "n"]]), &opts).unwrap();
let ctx = feature_with("n", Value::Number(21.0));
assert_eq!(evaluate(&expr, &ctx).unwrap(), Value::Number(42.0));
}
#[test]
fn macros_can_nest() {
let mut opts = Options::new();
opts.macro_def("double", vec!["x".into()], json!(["*", ["var", "x"], 2]));
opts.macro_def(
"quad",
vec!["x".into()],
json!(["double", ["double", ["var", "x"]]]),
);
let expr = parse_with(&json!(["quad", 3]), &opts).unwrap();
assert_eq!(
evaluate(&expr, &EvaluationContext::new()).unwrap(),
Value::Number(12.0)
);
}
#[test]
fn recursive_macro_is_rejected() {
let mut opts = Options::new();
opts.macro_def("loop", vec![], json!(["loop"]));
assert!(parse_with(&json!(["loop"]), &opts).is_err());
}
#[test]
fn macro_arity_is_checked() {
let mut opts = Options::new();
opts.macro_def("double", vec!["x".into()], json!(["*", ["var", "x"], 2]));
assert!(parse_with(&json!(["double", 1, 2]), &opts).is_err());
}
#[test]
fn recursive_function_runs() {
let mut opts = Options::new();
opts.function(
"countdown",
vec!["n".into()],
json!([
"case",
["<=", ["var", "n"], 0],
0,
["countdown", ["-", ["var", "n"], 1]]
]),
);
let expr = parse_with(&json!(["countdown", 5]), &opts).unwrap();
assert_eq!(
evaluate_with(&expr, &EvaluationContext::new(), &opts).unwrap(),
Value::Number(0.0)
);
}
#[test]
fn function_can_recurse_and_compute() {
let mut opts = Options::new();
opts.function(
"sum",
vec!["n".into()],
json!([
"case",
["<=", ["var", "n"], 0],
0,
["+", ["var", "n"], ["sum", ["-", ["var", "n"], 1]]]
]),
);
let expr = parse_with(&json!(["sum", 5]), &opts).unwrap();
assert_eq!(
evaluate_with(&expr, &EvaluationContext::new(), &opts).unwrap(),
Value::Number(15.0)
);
}
#[test]
fn unbounded_recursion_errors_rather_than_hangs() {
let mut opts = Options::new();
opts.function(
"forever",
vec!["n".into()],
json!(["forever", ["+", ["var", "n"], 1]]),
);
let expr = parse_with(&json!(["forever", 0]), &opts).unwrap();
assert!(evaluate_with(&expr, &EvaluationContext::new(), &opts).is_err());
}
#[test]
fn function_arity_is_checked_at_parse() {
let mut opts = Options::new();
opts.function("id", vec!["x".into()], json!(["var", "x"]));
assert!(parse_with(&json!(["id", 1, 2]), &opts).is_err());
}
#[test]
fn native_function_changes_result_by_argument() {
let mut opts = Options::new();
opts.native("label", 1, |args, _ctx| {
let code = args[0].as_str().unwrap_or("");
Ok(Value::String(
match code {
"hi" => "Hospital",
"sc" => "School",
_ => "Unknown",
}
.to_string(),
))
});
let expr = parse_with(&json!(["label", ["get", "code"]]), &opts).unwrap();
let ctx = feature_with("code", Value::String("sc".into()));
assert_eq!(
evaluate_with(&expr, &ctx, &opts).unwrap(),
Value::String("School".into())
);
}
#[test]
fn native_function_can_read_context() {
let mut opts = Options::new();
opts.native("plus_zoom", 1, |args, ctx| {
let n = args[0].as_number().unwrap_or(0.0);
Ok(Value::Number(n + ctx.zoom.unwrap_or(0.0)))
});
let expr = parse_with(&json!(["plus_zoom", 10]), &opts).unwrap();
let ctx = EvaluationContext::new().with_zoom(5.0);
assert_eq!(
evaluate_with(&expr, &ctx, &opts).unwrap(),
Value::Number(15.0)
);
}
#[test]
fn options_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Options>();
}