datalogic_rs/config.rs
1//! Engine evaluation knobs: NaN/divbyzero handling, truthiness rules,
2//! numeric coercion, and the recursion-depth cap.
3//!
4//! The default configuration matches the JSONLogic reference behaviour
5//! (JavaScript-flavoured truthiness, NaN errors on bad arithmetic input,
6//! `±f64::MAX` on division by zero). Use [`EvaluationConfig::default`]
7//! and tweak from there, or pick [`EvaluationConfig::safe_arithmetic`] /
8//! [`EvaluationConfig::strict`] as alternative starting points. Apply
9//! via [`Engine::builder().with_config(...)`](crate::EngineBuilder::with_config).
10
11use datavalue::OwnedDataValue;
12use std::sync::Arc;
13
14/// Knobs that change how an [`Engine`](crate::Engine) treats edge cases
15/// during evaluation — non-numeric arguments to arithmetic, division by
16/// zero, loose equality across types, truthiness rules, numeric coercion,
17/// and a recursion-depth cap.
18///
19/// Construct via [`Self::default`] for JavaScript-flavoured semantics
20/// (matches the JSONLogic reference behaviour), or use
21/// [`Self::safe_arithmetic`] / [`Self::strict`] as starting points and
22/// tweak from there. Pass to the engine via
23/// [`Engine::builder().with_config(...)`](crate::EngineBuilder::with_config).
24///
25/// # Example
26///
27/// ```rust
28/// use datalogic_rs::{Engine, EvaluationConfig, NanHandling};
29///
30/// // Chainable setters — only one import needed beyond the enum value.
31/// let config = EvaluationConfig::default()
32/// .with_arithmetic_nan_handling(NanHandling::IgnoreValue);
33/// let engine = Engine::builder().with_config(config).build();
34///
35/// // "skipped" can't coerce to a number; with `IgnoreValue` the
36/// // arithmetic continues with the remaining operands.
37/// let result = engine.eval_str(r#"{"+": [1, "skipped", 2]}"#, "null").unwrap();
38/// assert_eq!(result, "3");
39/// ```
40///
41/// The struct is `#[non_exhaustive]` — fields can be added in 5.x
42/// without breaking downstream. Use [`Self::default`] (or the presets)
43/// followed by the `with_*` setters; struct-literal construction from
44/// outside the crate is intentionally not supported.
45#[derive(Clone, Debug)]
46#[non_exhaustive]
47pub struct EvaluationConfig {
48 /// What `+` / `-` / `*` / `/` / `%` (and the variadic `min` / `max`)
49 /// do when an argument can't be coerced to a number. Default:
50 /// [`NanHandling::ThrowError`] — return an `ErrorKind::Thrown`
51 /// carrying `{"type": "NaN"}`. The other variants let arithmetic
52 /// continue (skip the bad value, treat it as 0, or short-circuit
53 /// to `null`).
54 pub arithmetic_nan_handling: NanHandling,
55
56 /// What `/` and `%` do when the divisor is zero, on the **float** path.
57 /// Default: [`DivisionByZeroHandling::ReturnSaturated`] (the
58 /// JavaScript-style `±f64::MAX` / `±f64::MIN` per dividend sign). Switch
59 /// to [`DivisionByZeroHandling::ThrowError`] if you'd rather see a
60 /// surface error than a sentinel value. Applies uniformly to the 2-arg,
61 /// array-fold, and variadic forms.
62 ///
63 /// Carve-out: an **integer** dividend divided by an integer zero always
64 /// errors with `{"type": "NaN"}`, regardless of this setting, since
65 /// there is no in-range integer sentinel to return. Only genuinely
66 /// fractional operands take the configurable float path.
67 pub division_by_zero: DivisionByZeroHandling,
68
69 /// Whether `==` / `!=` (loose equality) raise an error on values
70 /// that can't be sensibly compared (e.g. an object compared to a
71 /// number). Default: `true` (raise). Set to `false` for the
72 /// JavaScript-classic behaviour where any cross-type compare
73 /// returns `false` silently.
74 pub loose_equality_errors: bool,
75
76 /// How values are coerced to booleans by `if`, `and`, `or`, `!`,
77 /// `!!`, and the predicate slot of array operators. Default:
78 /// [`TruthyEvaluator::JavaScript`] (the reference JSONLogic rule:
79 /// false for `null` / `false` / `0` / `NaN` / `""` / empty
80 /// array / empty object, true for everything else). Pick
81 /// [`TruthyEvaluator::Python`], [`TruthyEvaluator::StrictBoolean`],
82 /// or supply a [`TruthyEvaluator::Custom`] closure for full control.
83 pub truthy_evaluator: TruthyEvaluator,
84
85 /// Knobs for the implicit string→number / null→number / bool→number
86 /// coercions used by arithmetic and comparison. Default:
87 /// [`NumericCoercionConfig::default`] (matches the JSONLogic
88 /// reference behaviour). When more than one flag could fire on the
89 /// same value, the precedence is:
90 ///
91 /// 1. `reject_non_numeric` — if `true`, no coercion at all happens;
92 /// the value either parses as a number or the engine reports a
93 /// type error. Overrides every other flag.
94 /// 2. `null_to_zero` — only consulted on `null` values.
95 /// 3. `bool_to_number` — only consulted on `true` / `false`.
96 /// 4. `empty_string_to_zero` — consulted on empty strings.
97 ///
98 /// Each path is independent in practice (the type filters above
99 /// don't overlap), so the precedence only matters when reasoning
100 /// about `reject_non_numeric` vs the rest.
101 pub numeric_coercion: NumericCoercionConfig,
102
103 /// Maximum number of nested [`Engine::evaluate`](crate::Engine::evaluate)
104 /// boundary calls before the engine bails with
105 /// [`ErrorKind::ConfigurationError`](crate::ErrorKind::ConfigurationError).
106 /// Tracked per-thread, so it
107 /// catches `CustomOperator` impls that hold `Arc<Engine>` and
108 /// re-enter via `engine.evaluate(...)` from inside their
109 /// `evaluate(...)`.
110 ///
111 /// Default: `256` — generous for legitimate nested rules, tight
112 /// enough to bail well before a stack overflow on typical
113 /// platforms. The check is skipped entirely when the engine has no
114 /// custom operators registered (built-ins can't recurse via
115 /// boundary re-entry), so pure-built-in workloads pay nothing.
116 pub max_recursion_depth: u32,
117
118 /// Ceiling on the operations one evaluation may charge, or `None`
119 /// (the default) for unbounded. Available with the `budget` feature.
120 ///
121 /// Where [`Self::max_recursion_depth`] bounds *boundary re-entry*
122 /// only, this bounds the work itself: a `map` over a large input
123 /// nested inside another `map` is unbounded under the depth cap and
124 /// bounded under this one. The count is deterministic for a pinned
125 /// crate version, which a wall-clock timeout is not, and the
126 /// evaluation is refused before the work rather than reported after
127 /// it.
128 ///
129 /// One operation is charged per dispatched node, one per item an
130 /// iterator examines, and whatever an operator charges for its own
131 /// data movement — see
132 /// [`Engine::evaluate_metered`](crate::Engine::evaluate_metered) for
133 /// exactly what the number counts. Crossing the ceiling raises
134 /// [`ErrorKind::BudgetExceeded`](crate::ErrorKind::BudgetExceeded),
135 /// which `try` observes but cannot recover from.
136 #[cfg(feature = "budget")]
137 pub ops_budget: Option<u64>,
138}
139
140/// Defines how to handle NaN (Not a Number) scenarios in arithmetic operations
141#[derive(Clone, Debug, PartialEq)]
142pub enum NanHandling {
143 /// Throw an error when encountering non-numeric values (default)
144 ThrowError,
145 /// Ignore non-numeric values and continue with remaining values
146 IgnoreValue,
147 /// Treat non-numeric values as zero
148 CoerceToZero,
149 /// Return null when encountering non-numeric values
150 ReturnNull,
151}
152
153/// Defines how to handle division by zero
154#[derive(Clone, Debug, PartialEq)]
155pub enum DivisionByZeroHandling {
156 /// Saturating division: clamp the result to the f64 extreme rather
157 /// than throw or null. Returns `f64::MAX` for a positive dividend,
158 /// `f64::MIN` for a negative dividend, and `0.0` for `0 / 0` (the
159 /// indeterminate form saturates to neutral). Default.
160 ReturnSaturated,
161 /// Throw an error
162 ThrowError,
163 /// Return null
164 ReturnNull,
165 /// Return infinity (positive or negative based on dividend sign)
166 ReturnInfinity,
167}
168
169/// Defines how to evaluate truthiness of values
170#[derive(Clone)]
171pub enum TruthyEvaluator {
172 /// JavaScript-style truthiness (default)
173 /// - false: null, false, 0, NaN, "", empty array, empty object
174 /// - true: everything else
175 JavaScript,
176
177 /// Python-style truthiness
178 /// - false: None/null, False, 0, 0.0, "", empty collections
179 /// - true: everything else
180 ///
181 /// Differs from [`Self::JavaScript`] on exactly one value: `NaN`.
182 /// Python's `float('nan')` is truthy, JavaScript's `NaN` is falsy.
183 /// Every other rule coincides. `NaN` is not expressible as a JSON
184 /// literal, so this only shows up on values arithmetic produced —
185 /// e.g. `inf * 0` under
186 /// [`DivisionByZeroHandling::ReturnInfinity`].
187 Python,
188
189 /// Strict boolean truthiness
190 /// - false: null, false
191 /// - true: everything else
192 StrictBoolean,
193
194 /// Custom truthiness evaluator. Receives the value as an
195 /// [`OwnedDataValue`] — the canonical v5 owned value type — so the
196 /// callback works without enabling `serde_json` interop.
197 ///
198 /// Note: this variant cannot participate in `PartialEq` or in a
199 /// derived [`Debug`] (the closure is opaque). The hand-rolled `Debug`
200 /// impl prints `Custom(<fn>)` so the surrounding [`EvaluationConfig`]
201 /// stays debug-printable.
202 Custom(Arc<dyn Fn(&OwnedDataValue) -> bool + Send + Sync>),
203}
204
205impl std::fmt::Debug for TruthyEvaluator {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 match self {
208 Self::JavaScript => f.write_str("JavaScript"),
209 Self::Python => f.write_str("Python"),
210 Self::StrictBoolean => f.write_str("StrictBoolean"),
211 Self::Custom(_) => f.write_str("Custom(<fn>)"),
212 }
213 }
214}
215
216impl TruthyEvaluator {
217 /// Wrap a closure as a custom truthiness evaluator without typing
218 /// `Arc::new(...)` at the call site.
219 ///
220 /// The `Arc` wrapping on [`TruthyEvaluator::Custom`] is structurally
221 /// required (it keeps [`EvaluationConfig`] `Clone`), so this helper
222 /// is purely ergonomic — the `Custom` variant remains public for
223 /// callers that already hold an `Arc`.
224 ///
225 /// # Example
226 ///
227 /// ```rust
228 /// use datalogic_rs::{Engine, EvaluationConfig, TruthyEvaluator};
229 /// use datalogic_rs::datavalue::OwnedDataValue;
230 ///
231 /// // Even integers are truthy.
232 /// let config = EvaluationConfig::default().with_truthy_evaluator(
233 /// TruthyEvaluator::custom(|v: &OwnedDataValue| {
234 /// v.as_i64().map(|n| n % 2 == 0).unwrap_or(false)
235 /// }),
236 /// );
237 /// let engine = Engine::builder().with_config(config).build();
238 /// let result = engine.eval_str(r#"{"if": [2, "even", "odd"]}"#, "null").unwrap();
239 /// assert_eq!(result, "\"even\"");
240 /// ```
241 pub fn custom<F>(f: F) -> Self
242 where
243 F: Fn(&OwnedDataValue) -> bool + Send + Sync + 'static,
244 {
245 Self::Custom(Arc::new(f))
246 }
247}
248
249/// Knobs for the implicit value→number coercions arithmetic and
250/// comparison perform on non-numeric arguments.
251///
252/// See [`EvaluationConfig::numeric_coercion`] for how these flags
253/// interact when more than one would fire on the same value (short
254/// answer: `reject_non_numeric` overrides everything else; the rest are
255/// type-disjoint so they don't conflict in practice).
256#[derive(Clone, Debug)]
257#[non_exhaustive]
258pub struct NumericCoercionConfig {
259 /// `""` → `0` in numeric context. Default: `true`. Disable to make
260 /// `{"+": ["", 1]}` fail with a NaN error instead of returning `1`.
261 pub empty_string_to_zero: bool,
262
263 /// `null` → `0` in numeric context. Default: `true`.
264 pub null_to_zero: bool,
265
266 /// `true` → `1`, `false` → `0` in numeric context. Default: `true`.
267 pub bool_to_number: bool,
268
269 /// Reject non-numeric values: a non-numeric value is a type error.
270 /// Default: `false`. When `true`, this flag overrides every other
271 /// flag in this struct — empty strings, nulls, and booleans all
272 /// raise rather than coerce. Acts as a kill switch for the rest of
273 /// the coercion knobs in this struct.
274 ///
275 /// Note: earlier versions also declared a reserved `undefined_to_zero`
276 /// flag here. It never had an effect (JSONLogic does not distinguish a
277 /// missing key from an explicit `null` — the reference `missing`
278 /// operator treats `{"a": null}` exactly like `{}`) and it has been
279 /// removed. A missing var already coerces to `0` under the default
280 /// [`Self::null_to_zero`]` = true`.
281 pub reject_non_numeric: bool,
282}
283
284impl Default for EvaluationConfig {
285 fn default() -> Self {
286 Self {
287 arithmetic_nan_handling: NanHandling::ThrowError,
288 division_by_zero: DivisionByZeroHandling::ReturnSaturated,
289 loose_equality_errors: true,
290 truthy_evaluator: TruthyEvaluator::JavaScript,
291 numeric_coercion: NumericCoercionConfig::default(),
292 max_recursion_depth: 256,
293 #[cfg(feature = "budget")]
294 ops_budget: None,
295 }
296 }
297}
298
299impl Default for NumericCoercionConfig {
300 fn default() -> Self {
301 Self {
302 empty_string_to_zero: true,
303 null_to_zero: true,
304 bool_to_number: true,
305 reject_non_numeric: false,
306 }
307 }
308}
309
310impl NumericCoercionConfig {
311 /// Set [`Self::empty_string_to_zero`].
312 #[must_use]
313 pub fn with_empty_string_to_zero(mut self, value: bool) -> Self {
314 self.empty_string_to_zero = value;
315 self
316 }
317
318 /// Set [`Self::null_to_zero`].
319 #[must_use]
320 pub fn with_null_to_zero(mut self, value: bool) -> Self {
321 self.null_to_zero = value;
322 self
323 }
324
325 /// Set [`Self::bool_to_number`].
326 #[must_use]
327 pub fn with_bool_to_number(mut self, value: bool) -> Self {
328 self.bool_to_number = value;
329 self
330 }
331
332 /// Set [`Self::reject_non_numeric`]. When `true`, this flag
333 /// overrides every other flag — empty strings, nulls, and booleans
334 /// all raise rather than coerce.
335 #[must_use]
336 pub fn with_reject_non_numeric(mut self, value: bool) -> Self {
337 self.reject_non_numeric = value;
338 self
339 }
340}
341
342impl EvaluationConfig {
343 /// Set [`Self::arithmetic_nan_handling`].
344 #[must_use]
345 pub fn with_arithmetic_nan_handling(mut self, value: NanHandling) -> Self {
346 self.arithmetic_nan_handling = value;
347 self
348 }
349
350 /// Set [`Self::division_by_zero`].
351 #[must_use]
352 pub fn with_division_by_zero(mut self, value: DivisionByZeroHandling) -> Self {
353 self.division_by_zero = value;
354 self
355 }
356
357 /// Set [`Self::loose_equality_errors`].
358 #[must_use]
359 pub fn with_loose_equality_errors(mut self, value: bool) -> Self {
360 self.loose_equality_errors = value;
361 self
362 }
363
364 /// Set [`Self::truthy_evaluator`].
365 #[must_use]
366 pub fn with_truthy_evaluator(mut self, value: TruthyEvaluator) -> Self {
367 self.truthy_evaluator = value;
368 self
369 }
370
371 /// Set [`Self::numeric_coercion`].
372 #[must_use]
373 pub fn with_numeric_coercion(mut self, value: NumericCoercionConfig) -> Self {
374 self.numeric_coercion = value;
375 self
376 }
377
378 /// Set [`Self::max_recursion_depth`].
379 #[must_use]
380 pub fn with_max_recursion_depth(mut self, value: u32) -> Self {
381 self.max_recursion_depth = value;
382 self
383 }
384
385 /// Set [`Self::ops_budget`]. Pass `None` for unbounded.
386 ///
387 /// ```rust
388 /// # #[cfg(feature = "budget")] {
389 /// use datalogic_rs::{Engine, EvaluationConfig};
390 ///
391 /// let engine = Engine::builder()
392 /// .with_config(EvaluationConfig::default().with_ops_budget(Some(16)))
393 /// .build();
394 /// let rule = r#"{"map": [{"var": "xs"}, {"*": [{"var": ""}, 2]}]}"#;
395 /// // Two nodes and three items: well inside 16.
396 /// assert_eq!(engine.eval_str(rule, r#"{"xs": [1, 2, 3]}"#).unwrap(), "[2,4,6]");
397 /// // The same rule over 100 items is refused.
398 /// let xs: Vec<String> = (0..100).map(|n| n.to_string()).collect();
399 /// let data = format!(r#"{{"xs": [{}]}}"#, xs.join(","));
400 /// assert_eq!(engine.eval_str(rule, &data).unwrap_err().tag(), "BudgetExceeded");
401 /// # }
402 /// ```
403 #[cfg(feature = "budget")]
404 #[must_use]
405 pub fn with_ops_budget(mut self, value: Option<u64>) -> Self {
406 self.ops_budget = value;
407 self
408 }
409
410 /// Create a configuration with safe arithmetic (ignores non-numeric values)
411 pub fn safe_arithmetic() -> Self {
412 Self {
413 arithmetic_nan_handling: NanHandling::IgnoreValue,
414 division_by_zero: DivisionByZeroHandling::ReturnNull,
415 loose_equality_errors: false,
416 ..Default::default()
417 }
418 }
419
420 /// Create a configuration with strict behavior (more errors)
421 pub fn strict() -> Self {
422 Self {
423 arithmetic_nan_handling: NanHandling::ThrowError,
424 division_by_zero: DivisionByZeroHandling::ThrowError,
425 loose_equality_errors: true,
426 numeric_coercion: NumericCoercionConfig {
427 empty_string_to_zero: false,
428 null_to_zero: false,
429 bool_to_number: false,
430 reject_non_numeric: true,
431 },
432 ..Default::default()
433 }
434 }
435}
436
437#[cfg(feature = "serde_json")]
438impl EvaluationConfig {
439 /// Build a configuration from a JSON object (string form).
440 ///
441 /// This is the wire format the language bindings use to pass engine
442 /// configuration across FFI boundaries through one shared parser;
443 /// Rust callers normally use the typed `with_*` setters instead.
444 ///
445 /// All keys are optional. An optional `"preset"` key (`"default"`,
446 /// `"safe_arithmetic"`, or `"strict"`) selects the starting point;
447 /// the remaining keys override individual fields on top of it.
448 /// Unknown keys, unknown enum strings, and type mismatches are
449 /// rejected with
450 /// [`ErrorKind::ConfigurationError`](crate::ErrorKind::ConfigurationError)
451 /// so typos fail loudly instead of being silently ignored.
452 /// [`TruthyEvaluator::Custom`] cannot be expressed in JSON — custom
453 /// truthiness is only available through the Rust API.
454 ///
455 /// Accepted keys and values (all enum strings are snake_case):
456 ///
457 /// | Key | Value |
458 /// |-----|-------|
459 /// | `preset` | `"default"` \| `"safe_arithmetic"` \| `"strict"` |
460 /// | `arithmetic_nan_handling` | `"throw_error"` \| `"ignore_value"` \| `"coerce_to_zero"` \| `"return_null"` |
461 /// | `division_by_zero` | `"return_saturated"` \| `"throw_error"` \| `"return_null"` \| `"return_infinity"` |
462 /// | `loose_equality_errors` | bool |
463 /// | `truthy_evaluator` | `"javascript"` \| `"python"` \| `"strict_boolean"` |
464 /// | `numeric_coercion` | object with bool keys `empty_string_to_zero`, `null_to_zero`, `bool_to_number`, `reject_non_numeric` |
465 /// | `max_recursion_depth` | integer ≥ 1 |
466 /// | `ops_budget` | integer ≥ 1, or `null` for unbounded (`budget` feature) |
467 ///
468 /// # Example
469 ///
470 /// ```rust
471 /// use datalogic_rs::{Engine, EvaluationConfig};
472 ///
473 /// let config = EvaluationConfig::from_json_str(r#"{
474 /// "preset": "strict",
475 /// "division_by_zero": "return_null",
476 /// "numeric_coercion": {"null_to_zero": true},
477 /// "max_recursion_depth": 64
478 /// }"#).unwrap();
479 /// let engine = Engine::builder().with_config(config).build();
480 /// // 1.5 keeps this on the configurable float path (an integer
481 /// // dividend over an integer zero always errors).
482 /// let result = engine.eval_str(r#"{"/": [1.5, 0]}"#, "null").unwrap();
483 /// assert_eq!(result, "null");
484 /// ```
485 ///
486 /// # Errors
487 ///
488 /// [`ErrorKind::ConfigurationError`](crate::ErrorKind::ConfigurationError)
489 /// if the string is not a JSON object or any key or value is
490 /// unrecognized.
491 pub fn from_json_str(json: &str) -> crate::Result<Self> {
492 use serde_json::Value;
493
494 fn cfg_err(msg: String) -> crate::Error {
495 crate::Error::configuration_error(msg)
496 }
497 fn expect_str<'v>(key: &str, value: &'v Value) -> crate::Result<&'v str> {
498 value
499 .as_str()
500 .ok_or_else(|| cfg_err(format!("config key {key:?} must be a string")))
501 }
502 fn expect_bool(key: &str, value: &Value) -> crate::Result<bool> {
503 value
504 .as_bool()
505 .ok_or_else(|| cfg_err(format!("config key {key:?} must be a boolean")))
506 }
507
508 let root: Value = serde_json::from_str(json)
509 .map_err(|e| cfg_err(format!("config is not valid JSON: {e}")))?;
510 let Value::Object(map) = root else {
511 return Err(cfg_err("config must be a JSON object".to_string()));
512 };
513
514 let mut config = match map.get("preset") {
515 None => Self::default(),
516 Some(preset) => match expect_str("preset", preset)? {
517 "default" => Self::default(),
518 "safe_arithmetic" => Self::safe_arithmetic(),
519 "strict" => Self::strict(),
520 other => {
521 return Err(cfg_err(format!(
522 "unknown preset {other:?} (expected \"default\", \"safe_arithmetic\", or \"strict\")"
523 )));
524 }
525 },
526 };
527
528 for (key, value) in &map {
529 match key.as_str() {
530 "preset" => {} // applied above, before the overrides
531 "arithmetic_nan_handling" => {
532 config.arithmetic_nan_handling = match expect_str(key, value)? {
533 "throw_error" => NanHandling::ThrowError,
534 "ignore_value" => NanHandling::IgnoreValue,
535 "coerce_to_zero" => NanHandling::CoerceToZero,
536 "return_null" => NanHandling::ReturnNull,
537 other => {
538 return Err(cfg_err(format!(
539 "unknown arithmetic_nan_handling {other:?} (expected \"throw_error\", \"ignore_value\", \"coerce_to_zero\", or \"return_null\")"
540 )));
541 }
542 };
543 }
544 "division_by_zero" => {
545 config.division_by_zero = match expect_str(key, value)? {
546 "return_saturated" => DivisionByZeroHandling::ReturnSaturated,
547 "throw_error" => DivisionByZeroHandling::ThrowError,
548 "return_null" => DivisionByZeroHandling::ReturnNull,
549 "return_infinity" => DivisionByZeroHandling::ReturnInfinity,
550 other => {
551 return Err(cfg_err(format!(
552 "unknown division_by_zero {other:?} (expected \"return_saturated\", \"throw_error\", \"return_null\", or \"return_infinity\")"
553 )));
554 }
555 };
556 }
557 "loose_equality_errors" => {
558 config.loose_equality_errors = expect_bool(key, value)?;
559 }
560 "truthy_evaluator" => {
561 config.truthy_evaluator = match expect_str(key, value)? {
562 "javascript" => TruthyEvaluator::JavaScript,
563 "python" => TruthyEvaluator::Python,
564 "strict_boolean" => TruthyEvaluator::StrictBoolean,
565 other => {
566 return Err(cfg_err(format!(
567 "unknown truthy_evaluator {other:?} (expected \"javascript\", \"python\", or \"strict_boolean\"; custom evaluators are Rust-only)"
568 )));
569 }
570 };
571 }
572 "numeric_coercion" => {
573 let Value::Object(coercion) = value else {
574 return Err(cfg_err(
575 "config key \"numeric_coercion\" must be an object".to_string(),
576 ));
577 };
578 for (ck, cv) in coercion {
579 match ck.as_str() {
580 "empty_string_to_zero" => {
581 config.numeric_coercion.empty_string_to_zero = expect_bool(ck, cv)?;
582 }
583 "null_to_zero" => {
584 config.numeric_coercion.null_to_zero = expect_bool(ck, cv)?;
585 }
586 "bool_to_number" => {
587 config.numeric_coercion.bool_to_number = expect_bool(ck, cv)?;
588 }
589 "reject_non_numeric" => {
590 config.numeric_coercion.reject_non_numeric = expect_bool(ck, cv)?;
591 }
592 other => {
593 return Err(cfg_err(format!(
594 "unknown numeric_coercion key {other:?}"
595 )));
596 }
597 }
598 }
599 }
600 "max_recursion_depth" => {
601 let depth = value
602 .as_u64()
603 .filter(|n| (1..=u64::from(u32::MAX)).contains(n))
604 .ok_or_else(|| {
605 cfg_err(format!(
606 "config key \"max_recursion_depth\" must be an integer between 1 and {}",
607 u32::MAX
608 ))
609 })?;
610 config.max_recursion_depth = depth as u32;
611 }
612 #[cfg(feature = "budget")]
613 "ops_budget" => {
614 config.ops_budget = if value.is_null() {
615 None
616 } else {
617 Some(value.as_u64().filter(|n| *n >= 1).ok_or_else(|| {
618 cfg_err(
619 "config key \"ops_budget\" must be a positive integer or null"
620 .to_string(),
621 )
622 })?)
623 };
624 }
625 other => {
626 return Err(cfg_err(format!("unknown config key {other:?}")));
627 }
628 }
629 }
630
631 Ok(config)
632 }
633}