use indexmap::IndexMap;
use crate::ast::{PathSegment, Value, Variable};
use crate::validate::Config;
pub const MAX_RESOLVE_DEPTH: usize = 64;
#[must_use]
pub fn resolve(value: &Value, config: &Config<'_>) -> Option<Value> {
resolve_at(value, config, 0)
}
fn resolve_at(value: &Value, config: &Config<'_>, depth: usize) -> Option<Value> {
if depth > MAX_RESOLVE_DEPTH {
return None;
}
match value {
Value::Null | Value::Boolean(_) | Value::Number(_) | Value::String(_) => {
Some(value.clone())
}
Value::Array(items) => Some(Value::Array(
items
.iter()
.map(|item| resolve_at(item, config, depth + 1).unwrap_or(Value::Null))
.collect(),
)),
Value::Hash(entries) => {
let mut out = IndexMap::with_capacity(entries.len());
for (key, value) in entries {
if let Some(resolved) = resolve_at(value, config, depth + 1) {
out.insert(key.clone(), resolved);
}
}
Some(Value::Hash(out))
}
Value::Variable(variable) => resolve_variable(variable, config),
Value::Function(function) => {
let declared = config.functions.get(function.name.as_str())?;
let transform = declared.transform.as_ref()?;
let mut parameters = IndexMap::with_capacity(function.parameters.len());
for (key, value) in &function.parameters {
parameters.insert(key.clone(), resolve_at(value, config, depth + 1));
}
transform(¶meters, config)
}
}
}
#[must_use]
pub fn resolve_variable(variable: &Variable, config: &Config<'_>) -> Option<Value> {
let variables = config.variables.as_ref()?;
let mut current: Option<&Value> = None;
for (position, segment) in variable.path.iter().enumerate() {
current = match (position, current) {
(0, _) => match segment {
PathSegment::Key(key) => variables.get(key.as_str()),
PathSegment::Index(index) => variables.get(&js_key(*index)),
},
(_, Some(value)) => step(value, segment),
(_, None) => return None,
};
}
current.and_then(|value| resolve_at(value, config, 1))
}
fn step<'v>(value: &'v Value, segment: &PathSegment) -> Option<&'v Value> {
match (value, segment) {
(Value::Hash(entries), PathSegment::Key(key)) => entries.get(key.as_str()),
(Value::Hash(entries), PathSegment::Index(index)) => entries.get(&js_key(*index)),
(Value::Array(items), PathSegment::Index(index)) => {
index_of(*index).and_then(|index| items.get(index))
}
(Value::Array(items), PathSegment::Key(key)) => {
key.parse::<usize>().ok().and_then(|index| items.get(index))
}
_ => None,
}
}
fn index_of(number: f64) -> Option<usize> {
if number < 0.0 || number.fract() != 0.0 || !number.is_finite() {
return None;
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "guarded above: finite, non-negative, integral"
)]
Some(number as usize)
}
fn js_key(number: f64) -> String {
if number.fract() == 0.0 && number.is_finite() {
#[allow(
clippy::cast_possible_truncation,
reason = "integral and finite, checked immediately above"
)]
return (number as i64).to_string();
}
number.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Function;
use crate::builtins;
fn config() -> Config<'static> {
let mut config = builtins::config();
config.variables = Some(IndexMap::from([
("foo".to_string(), Value::String("bar".to_string())),
(
"nested".to_string(),
Value::Hash(IndexMap::from([(
"this is a test".to_string(),
Value::String("bar".to_string()),
)])),
),
(
"list".to_string(),
Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]),
),
]));
config
}
fn variable(path: &[&str]) -> Value {
Value::Variable(Variable::new(
path.iter()
.map(|step| PathSegment::Key((*step).to_string()))
.collect(),
))
}
#[test]
fn a_variable_resolves_to_its_value() {
assert_eq!(
resolve(&variable(&["foo"]), &config()),
Some(Value::String("bar".to_string()))
);
}
#[test]
fn a_missing_variable_is_undefined_rather_than_null() {
assert_eq!(resolve(&variable(&["nope"]), &config()), None);
}
#[test]
fn no_variables_at_all_resolves_to_undefined() {
assert_eq!(resolve(&variable(&["foo"]), &Config::new()), None);
}
#[test]
fn a_string_key_indexes_a_hash() {
assert_eq!(
resolve(&variable(&["nested", "this is a test"]), &config()),
Some(Value::String("bar".to_string()))
);
}
#[test]
fn a_numeric_step_indexes_an_array() {
let path = Value::Variable(Variable::new(vec![
PathSegment::Key("list".to_string()),
PathSegment::Index(1.0),
]));
assert_eq!(resolve(&path, &config()), Some(Value::Number(2.0)));
let past_end = Value::Variable(Variable::new(vec![
PathSegment::Key("list".to_string()),
PathSegment::Index(9.0),
]));
assert_eq!(resolve(&past_end, &config()), None);
}
#[test]
fn a_path_through_a_string_is_undefined_rather_than_a_crash() {
assert_eq!(resolve(&variable(&["foo", "length"]), &config()), None);
}
#[test]
fn equals_matches_a_variable_against_a_literal() {
let call = |value: &str| {
let mut config = builtins::config();
config.variables = Some(IndexMap::from([(
"foo".to_string(),
Value::String(value.to_string()),
)]));
let mut parameters = IndexMap::new();
parameters.insert(
Function::positional_key(0),
Value::Variable(Variable::new(vec![PathSegment::Key("foo".to_string())])),
);
parameters.insert(
Function::positional_key(1),
Value::String("bar".to_string()),
);
resolve(
&Value::Function(Function::new("equals".to_string(), parameters)),
&config,
)
};
assert_eq!(call("bar"), Some(Value::Boolean(true)));
assert_eq!(call("baz"), Some(Value::Boolean(false)));
}
#[test]
fn an_unregistered_function_is_undefined() {
let call = Value::Function(Function::new("nope".to_string(), IndexMap::new()));
assert_eq!(resolve(&call, &builtins::config()), None);
}
#[test]
fn deep_nesting_terminates_rather_than_overflowing() {
let mut value = Value::Number(1.0);
for _ in 0..500 {
value = Value::Array(vec![value]);
}
assert!(resolve(&value, &Config::new()).is_some());
}
}