use datavalue::OwnedDataValue;
use std::sync::Arc;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct EvaluationConfig {
pub arithmetic_nan_handling: NanHandling,
pub division_by_zero: DivisionByZeroHandling,
pub loose_equality_errors: bool,
pub truthy_evaluator: TruthyEvaluator,
pub numeric_coercion: NumericCoercionConfig,
pub max_recursion_depth: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub enum NanHandling {
ThrowError,
IgnoreValue,
CoerceToZero,
ReturnNull,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DivisionByZeroHandling {
ReturnSaturated,
ThrowError,
ReturnNull,
ReturnInfinity,
}
#[derive(Clone)]
pub enum TruthyEvaluator {
JavaScript,
Python,
StrictBoolean,
Custom(Arc<dyn Fn(&OwnedDataValue) -> bool + Send + Sync>),
}
impl std::fmt::Debug for TruthyEvaluator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::JavaScript => f.write_str("JavaScript"),
Self::Python => f.write_str("Python"),
Self::StrictBoolean => f.write_str("StrictBoolean"),
Self::Custom(_) => f.write_str("Custom(<fn>)"),
}
}
}
impl TruthyEvaluator {
pub fn custom<F>(f: F) -> Self
where
F: Fn(&OwnedDataValue) -> bool + Send + Sync + 'static,
{
Self::Custom(Arc::new(f))
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct NumericCoercionConfig {
pub empty_string_to_zero: bool,
pub null_to_zero: bool,
pub bool_to_number: bool,
pub reject_non_numeric: bool,
}
impl Default for EvaluationConfig {
fn default() -> Self {
Self {
arithmetic_nan_handling: NanHandling::ThrowError,
division_by_zero: DivisionByZeroHandling::ReturnSaturated,
loose_equality_errors: true,
truthy_evaluator: TruthyEvaluator::JavaScript,
numeric_coercion: NumericCoercionConfig::default(),
max_recursion_depth: 256,
}
}
}
impl Default for NumericCoercionConfig {
fn default() -> Self {
Self {
empty_string_to_zero: true,
null_to_zero: true,
bool_to_number: true,
reject_non_numeric: false,
}
}
}
impl NumericCoercionConfig {
#[must_use]
pub fn with_empty_string_to_zero(mut self, value: bool) -> Self {
self.empty_string_to_zero = value;
self
}
#[must_use]
pub fn with_null_to_zero(mut self, value: bool) -> Self {
self.null_to_zero = value;
self
}
#[must_use]
pub fn with_bool_to_number(mut self, value: bool) -> Self {
self.bool_to_number = value;
self
}
#[must_use]
pub fn with_reject_non_numeric(mut self, value: bool) -> Self {
self.reject_non_numeric = value;
self
}
}
impl EvaluationConfig {
#[must_use]
pub fn with_arithmetic_nan_handling(mut self, value: NanHandling) -> Self {
self.arithmetic_nan_handling = value;
self
}
#[must_use]
pub fn with_division_by_zero(mut self, value: DivisionByZeroHandling) -> Self {
self.division_by_zero = value;
self
}
#[must_use]
pub fn with_loose_equality_errors(mut self, value: bool) -> Self {
self.loose_equality_errors = value;
self
}
#[must_use]
pub fn with_truthy_evaluator(mut self, value: TruthyEvaluator) -> Self {
self.truthy_evaluator = value;
self
}
#[must_use]
pub fn with_numeric_coercion(mut self, value: NumericCoercionConfig) -> Self {
self.numeric_coercion = value;
self
}
#[must_use]
pub fn with_max_recursion_depth(mut self, value: u32) -> Self {
self.max_recursion_depth = value;
self
}
pub fn safe_arithmetic() -> Self {
Self {
arithmetic_nan_handling: NanHandling::IgnoreValue,
division_by_zero: DivisionByZeroHandling::ReturnNull,
loose_equality_errors: false,
..Default::default()
}
}
pub fn strict() -> Self {
Self {
arithmetic_nan_handling: NanHandling::ThrowError,
division_by_zero: DivisionByZeroHandling::ThrowError,
loose_equality_errors: true,
numeric_coercion: NumericCoercionConfig {
empty_string_to_zero: false,
null_to_zero: false,
bool_to_number: false,
reject_non_numeric: true,
},
..Default::default()
}
}
}
#[cfg(feature = "serde_json")]
impl EvaluationConfig {
pub fn from_json_str(json: &str) -> crate::Result<Self> {
use serde_json::Value;
fn cfg_err(msg: String) -> crate::Error {
crate::Error::configuration_error(msg)
}
fn expect_str<'v>(key: &str, value: &'v Value) -> crate::Result<&'v str> {
value
.as_str()
.ok_or_else(|| cfg_err(format!("config key {key:?} must be a string")))
}
fn expect_bool(key: &str, value: &Value) -> crate::Result<bool> {
value
.as_bool()
.ok_or_else(|| cfg_err(format!("config key {key:?} must be a boolean")))
}
let root: Value = serde_json::from_str(json)
.map_err(|e| cfg_err(format!("config is not valid JSON: {e}")))?;
let Value::Object(map) = root else {
return Err(cfg_err("config must be a JSON object".to_string()));
};
let mut config = match map.get("preset") {
None => Self::default(),
Some(preset) => match expect_str("preset", preset)? {
"default" => Self::default(),
"safe_arithmetic" => Self::safe_arithmetic(),
"strict" => Self::strict(),
other => {
return Err(cfg_err(format!(
"unknown preset {other:?} (expected \"default\", \"safe_arithmetic\", or \"strict\")"
)));
}
},
};
for (key, value) in &map {
match key.as_str() {
"preset" => {} "arithmetic_nan_handling" => {
config.arithmetic_nan_handling = match expect_str(key, value)? {
"throw_error" => NanHandling::ThrowError,
"ignore_value" => NanHandling::IgnoreValue,
"coerce_to_zero" => NanHandling::CoerceToZero,
"return_null" => NanHandling::ReturnNull,
other => {
return Err(cfg_err(format!(
"unknown arithmetic_nan_handling {other:?} (expected \"throw_error\", \"ignore_value\", \"coerce_to_zero\", or \"return_null\")"
)));
}
};
}
"division_by_zero" => {
config.division_by_zero = match expect_str(key, value)? {
"return_saturated" => DivisionByZeroHandling::ReturnSaturated,
"throw_error" => DivisionByZeroHandling::ThrowError,
"return_null" => DivisionByZeroHandling::ReturnNull,
"return_infinity" => DivisionByZeroHandling::ReturnInfinity,
other => {
return Err(cfg_err(format!(
"unknown division_by_zero {other:?} (expected \"return_saturated\", \"throw_error\", \"return_null\", or \"return_infinity\")"
)));
}
};
}
"loose_equality_errors" => {
config.loose_equality_errors = expect_bool(key, value)?;
}
"truthy_evaluator" => {
config.truthy_evaluator = match expect_str(key, value)? {
"javascript" => TruthyEvaluator::JavaScript,
"python" => TruthyEvaluator::Python,
"strict_boolean" => TruthyEvaluator::StrictBoolean,
other => {
return Err(cfg_err(format!(
"unknown truthy_evaluator {other:?} (expected \"javascript\", \"python\", or \"strict_boolean\"; custom evaluators are Rust-only)"
)));
}
};
}
"numeric_coercion" => {
let Value::Object(coercion) = value else {
return Err(cfg_err(
"config key \"numeric_coercion\" must be an object".to_string(),
));
};
for (ck, cv) in coercion {
match ck.as_str() {
"empty_string_to_zero" => {
config.numeric_coercion.empty_string_to_zero = expect_bool(ck, cv)?;
}
"null_to_zero" => {
config.numeric_coercion.null_to_zero = expect_bool(ck, cv)?;
}
"bool_to_number" => {
config.numeric_coercion.bool_to_number = expect_bool(ck, cv)?;
}
"reject_non_numeric" => {
config.numeric_coercion.reject_non_numeric = expect_bool(ck, cv)?;
}
other => {
return Err(cfg_err(format!(
"unknown numeric_coercion key {other:?}"
)));
}
}
}
}
"max_recursion_depth" => {
let depth = value
.as_u64()
.filter(|n| (1..=u64::from(u32::MAX)).contains(n))
.ok_or_else(|| {
cfg_err(format!(
"config key \"max_recursion_depth\" must be an integer between 1 and {}",
u32::MAX
))
})?;
config.max_recursion_depth = depth as u32;
}
other => {
return Err(cfg_err(format!("unknown config key {other:?}")));
}
}
}
Ok(config)
}
}