datalogic_rs/builder.rs
1//! Builder for [`Engine`].
2//!
3//! The single entry point for non-default engine construction
4//! (config, custom operators, templating). Replaces the four ad-hoc
5//! 4.x constructors (`new`, `with_preserve_structure`, `with_config`,
6//! `with_config_and_structure`).
7
8use std::collections::HashMap;
9
10use crate::CustomOperator;
11use crate::config::EvaluationConfig;
12use crate::engine::Engine;
13
14/// Builder for [`Engine`]. Construct via [`Engine::builder`].
15///
16/// ```
17/// use datalogic_rs::Engine;
18///
19/// let engine = Engine::builder().build();
20/// # let _ = engine;
21/// ```
22///
23/// # Defaults
24///
25/// `Engine::builder().build()` produces the same engine as
26/// [`Engine::new`] / [`Engine::default`]:
27///
28/// - **`config`** — [`EvaluationConfig::default`]: JavaScript-flavoured
29/// truthiness, NaN errors on bad arithmetic input, `±f64::MAX` on
30/// division by zero, `loose_equality_errors = true`,
31/// `max_recursion_depth = 256`, and the implicit `null`/`bool`/
32/// `""` → 0 numeric coercions enabled. Override with [`Self::with_config`];
33/// [`EvaluationConfig::safe_arithmetic`] / [`EvaluationConfig::strict`]
34/// are alternative starting points.
35/// - **`templating`** — `false` (templating mode off). Set with
36/// [`Self::with_templating`]; only effective when the crate is
37/// built with `feature = "templating"`.
38/// - **`operators`** — empty. Add custom operators with
39/// [`Self::add_operator`] before [`Self::build`] freezes the set.
40/// - **`constant_folding`** — `true`. The compile pipeline pre-computes
41/// constant sub-expressions during [`Engine::compile`]. Disable with
42/// [`Self::with_constant_folding`] when you need every operator to
43/// survive in the compiled tree (e.g. for tooling that walks the
44/// structure or applies its own rewrites).
45#[must_use = "the builder is consumed by `.build()`"]
46pub struct EngineBuilder {
47 config: EvaluationConfig,
48 templating: bool,
49 constant_folding: bool,
50 operators: HashMap<String, Box<dyn CustomOperator>>,
51}
52
53impl Default for EngineBuilder {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl EngineBuilder {
60 /// Fresh builder with default config and no custom operators.
61 #[inline]
62 pub fn new() -> Self {
63 Self {
64 config: EvaluationConfig::default(),
65 templating: false,
66 constant_folding: true,
67 operators: HashMap::new(),
68 }
69 }
70
71 /// Set the evaluation config.
72 #[inline]
73 #[must_use = "builder methods return a new builder; chain into `.build()`"]
74 pub fn with_config(mut self, config: EvaluationConfig) -> Self {
75 self.config = config;
76 self
77 }
78
79 /// Toggle templating mode (multi-key objects compile to output-shaping
80 /// templates; unknown operator keys pass through verbatim). Only
81 /// effective when the crate is built with `feature = "templating"`.
82 #[inline]
83 #[must_use = "builder methods return a new builder; chain into `.build()`"]
84 pub fn with_templating(mut self, on: bool) -> Self {
85 self.templating = on;
86 self
87 }
88
89 /// Toggle the compile-time constant-folding pass. Default: `true`
90 /// (folding enabled). Pass `false` when every operator must survive
91 /// in the compiled tree — debuggers, alternate evaluators, or any
92 /// caller that walks the compiled structure and would be surprised
93 /// to see a `{"+": [1, 2]}` collapsed to a `3` literal.
94 // The trace-feature addendum links to `Engine::trace`, which only
95 // exists with `feature = "trace"`. Gate the whole paragraph behind
96 // the same feature so `cargo doc` without `--all-features` doesn't
97 // break on the intra-doc link.
98 #[cfg_attr(feature = "trace", doc = "")]
99 #[cfg_attr(
100 feature = "trace",
101 doc = "The trace surface ([`crate::Engine::trace`]) always disables folding"
102 )]
103 #[cfg_attr(
104 feature = "trace",
105 doc = "internally regardless of this setting, since traces would otherwise"
106 )]
107 #[cfg_attr(feature = "trace", doc = "lose the folded operators as steps.")]
108 #[inline]
109 #[must_use = "builder methods return a new builder; chain into `.build()`"]
110 pub fn with_constant_folding(mut self, on: bool) -> Self {
111 self.constant_folding = on;
112 self
113 }
114
115 /// Register a [`CustomOperator`] under `name`. Multiple calls with the
116 /// same name overwrite the prior registration.
117 ///
118 /// Accepts both typed operators (`T: CustomOperator + 'static`) and
119 /// pre-boxed trait objects (`Box<dyn CustomOperator>`) — the bare
120 /// `Box<dyn CustomOperator>` itself implements `CustomOperator`
121 /// (delegating to the inner), so a single entry point covers both
122 /// shapes:
123 ///
124 /// ```ignore
125 /// builder
126 /// .add_operator("typed", MyOp) // typed
127 /// .add_operator("dyn", boxed_op_from_registry as Box<_>) // pre-boxed
128 /// ```
129 ///
130 /// Operator registration is builder-only; once [`Self::build`] hands
131 /// you an [`Engine`], its operator set is frozen.
132 ///
133 /// **Built-ins always win.** If `name` collides with a built-in
134 /// JSONLogic operator (`+`, `if`, `var`, `map`, …), the built-in is
135 /// dispatched and the registered custom op is never reached. To
136 /// extend the operator set, choose a name that doesn't parse as a
137 /// built-in.
138 #[inline]
139 #[must_use = "builder methods return a new builder; chain into `.build()`"]
140 pub fn add_operator<T>(mut self, name: impl Into<String>, operator: T) -> Self
141 where
142 T: CustomOperator + 'static,
143 {
144 self.operators.insert(name.into(), Box::new(operator));
145 self
146 }
147
148 /// Finalise the builder into an immutable [`Engine`] engine.
149 pub fn build(self) -> Engine {
150 Engine::from_builder_parts(
151 self.config,
152 self.templating,
153 self.constant_folding,
154 self.operators,
155 )
156 }
157}