Skip to main content

json_tools_rs/
builder.rs

1//! Builder API and execution engine for JSONTools.
2//!
3//! Provides the fluent builder interface (`JSONTools::new().flatten().execute()`)
4//! and orchestrates flattening, unflattening, transformations, and parallel
5//! batch processing.
6
7use rayon::prelude::*;
8use smallvec::SmallVec;
9
10use crate::config::{
11    BooleanConversionConfig, CollisionConfig, DateConversionConfig, FilteringConfig,
12    NullConversionConfig, NumberConversionConfig, OperationMode, ProcessingConfig,
13    ReplacementConfig, TypeConversionConfig, DEFAULT_MAX_ARRAY_INDEX,
14    DEFAULT_NESTED_PARALLEL_THRESHOLD, DEFAULT_NUM_THREADS, DEFAULT_PARALLEL_THRESHOLD,
15};
16use crate::error::JsonToolsError;
17use crate::flatten::process_single_json;
18use crate::transform::process_single_json_normal;
19use crate::types::{JsonInput, JsonOutput};
20use crate::unflatten::process_single_json_for_unflatten;
21
22// ================================================================================================
23// ProcessingConfig::from_json_tools() - lives here to avoid circular dependency
24// ================================================================================================
25
26impl ProcessingConfig {
27    /// Create a ProcessingConfig from a JSONTools builder instance
28    pub fn from_json_tools(tools: &JSONTools) -> Self {
29        let type_conversion = TypeConversionConfig {
30            dates: tools.date_conversion.clone(),
31            nulls: tools.null_conversion.clone(),
32            booleans: tools.boolean_conversion.clone(),
33            numbers: tools.number_conversion.clone(),
34        };
35        let type_conversion_mode = type_conversion.classify();
36        Self {
37            separator: tools.separator.clone(),
38            lowercase_keys: tools.lowercase_keys,
39            filtering: FilteringConfig {
40                remove_empty_strings: tools.remove_empty_string_values,
41                remove_nulls: tools.remove_null_values,
42                remove_empty_objects: tools.remove_empty_objects,
43                remove_empty_arrays: tools.remove_empty_arrays,
44            },
45            collision: CollisionConfig {
46                handle_collisions: tools.handle_key_collision,
47            },
48            replacements: ReplacementConfig {
49                key_replacements: tools.key_replacements.clone(),
50                value_replacements: tools.value_replacements.clone(),
51                key_exclusions: tools.key_exclusions.clone(),
52                value_exclusions: tools.value_exclusions.clone(),
53            },
54            type_conversion,
55            type_conversion_mode,
56            parallel_threshold: tools.parallel_threshold,
57            num_threads: tools.num_threads,
58            nested_parallel_threshold: tools.nested_parallel_threshold,
59            max_array_index: tools.max_array_index,
60        }
61    }
62}
63
64// ================================================================================================
65// JSONTools Builder Struct
66// ================================================================================================
67
68/// Unified JSON Tools API with builder pattern for both flattening and unflattening operations
69///
70/// This is the unified interface for all JSON manipulation operations.
71/// It provides a single entry point for all JSON manipulation operations with a consistent builder pattern.
72#[derive(Debug, Clone)]
73pub struct JSONTools {
74    // SmallVec fields (stack-allocated for 0-2 replacements, common case)
75    /// Key replacement patterns (find, replace)
76    /// Uses SmallVec to avoid heap allocation for 0-2 replacements (90% of use cases)
77    key_replacements: SmallVec<[(String, String); 2]>,
78    /// Value replacement patterns (find, replace)
79    /// Uses SmallVec to avoid heap allocation for 0-2 replacements (90% of use cases)
80    value_replacements: SmallVec<[(String, String); 2]>,
81    /// Key exclusion patterns -- keys matching any of these are dropped entirely
82    /// (along with their subtree). Uses SmallVec to avoid heap allocation for 0-2
83    /// patterns (common case)
84    key_exclusions: SmallVec<[String; 2]>,
85    /// Value exclusion patterns -- a key-value pair is dropped if its (scalar leaf)
86    /// value matches any of these. Uses SmallVec to avoid heap allocation for 0-2
87    /// patterns (common case)
88    value_exclusions: SmallVec<[String; 2]>,
89    /// Separator for nested keys (default: ".")
90    separator: String,
91
92    // Medium fields (8 bytes on 64-bit systems)
93    /// Minimum batch size to use parallel processing (default: 100)
94    parallel_threshold: usize,
95    /// Number of threads for parallel processing (None = use system default)
96    num_threads: Option<usize>,
97    /// Minimum object/array size for nested parallel processing within a single JSON document
98    nested_parallel_threshold: usize,
99    /// Maximum array index allowed during unflattening (DoS protection)
100    max_array_index: usize,
101
102    // Type-conversion sub-configs (dates, nulls, booleans, numbers)
103    /// Date/datetime conversion settings
104    date_conversion: DateConversionConfig,
105    /// Null-string conversion settings
106    null_conversion: NullConversionConfig,
107    /// Boolean-string conversion settings
108    boolean_conversion: BooleanConversionConfig,
109    /// Numeric-string conversion settings
110    number_conversion: NumberConversionConfig,
111
112    // Medium fields (2 bytes)
113    /// Current operation mode (flatten or unflatten)
114    mode: Option<OperationMode>,
115
116    // Small fields (1 byte each) - grouped together to minimize padding
117    /// Remove keys with empty string values
118    remove_empty_string_values: bool,
119    /// Remove keys with null values
120    remove_null_values: bool,
121    /// Remove keys with empty object values
122    remove_empty_objects: bool,
123    /// Remove keys with empty array values
124    remove_empty_arrays: bool,
125    /// Convert all keys to lowercase
126    lowercase_keys: bool,
127    /// Handle key collisions by collecting values into arrays
128    handle_key_collision: bool,
129}
130
131impl Default for JSONTools {
132    fn default() -> Self {
133        Self {
134            // SmallVec fields - no heap allocation for 0-2 replacements!
135            key_replacements: SmallVec::new(),
136            value_replacements: SmallVec::new(),
137            key_exclusions: SmallVec::new(),
138            value_exclusions: SmallVec::new(),
139            separator: ".".to_string(),
140            // Medium fields — use shared LazyLock statics from config module
141            parallel_threshold: *DEFAULT_PARALLEL_THRESHOLD,
142            num_threads: *DEFAULT_NUM_THREADS,
143            nested_parallel_threshold: *DEFAULT_NESTED_PARALLEL_THRESHOLD,
144            max_array_index: *DEFAULT_MAX_ARRAY_INDEX,
145            date_conversion: DateConversionConfig::default(),
146            null_conversion: NullConversionConfig::default(),
147            boolean_conversion: BooleanConversionConfig::default(),
148            number_conversion: NumberConversionConfig::default(),
149            mode: None,
150            // Small fields
151            remove_empty_string_values: false,
152            remove_null_values: false,
153            remove_empty_objects: false,
154            remove_empty_arrays: false,
155            lowercase_keys: false,
156            handle_key_collision: false,
157        }
158    }
159}
160
161impl JSONTools {
162    /// Create a new JSONTools instance with default settings
163    pub fn new() -> Self {
164        Self::default()
165    }
166
167    /// Set the operation mode to flatten
168    #[must_use]
169    pub fn flatten(mut self) -> Self {
170        self.mode = Some(OperationMode::Flatten);
171        self
172    }
173
174    /// Set the operation mode to unflatten
175    #[must_use]
176    pub fn unflatten(mut self) -> Self {
177        self.mode = Some(OperationMode::Unflatten);
178        self
179    }
180
181    /// Set the operation mode to normal (apply transformations without flatten/unflatten)
182    ///
183    /// In normal mode, key/value replacements, filtering, and type conversion are applied
184    /// recursively to the JSON structure without flattening or unflattening it.
185    ///
186    /// # Example
187    ///
188    /// ```rust
189    /// use json_tools_rs::{JSONTools, JsonOutput};
190    ///
191    /// let json = r#"{"Name": "John", "Age": "30", "Active": "true"}"#;
192    /// let result = JSONTools::new()
193    ///     .normal()
194    ///     .lowercase_keys(true)
195    ///     .auto_convert_types(true)
196    ///     .execute(json).unwrap();
197    ///
198    /// match result {
199    ///     JsonOutput::Single(output) => {
200    ///         assert!(output.contains(r#""name""#));
201    ///         assert!(output.contains(r#":30"#) || output.contains(r#": 30"#));
202    ///     }
203    ///     _ => unreachable!(),
204    /// }
205    /// ```
206    #[must_use]
207    pub fn normal(mut self) -> Self {
208        self.mode = Some(OperationMode::Normal);
209        self
210    }
211
212    /// Set the separator used for nested keys (default: ".")
213    ///
214    /// Empty separators are rejected at [`execute()`](Self::execute) time with a descriptive error.
215    #[must_use]
216    pub fn separator(mut self, separator: impl Into<String>) -> Self {
217        self.separator = separator.into();
218        self
219    }
220
221    /// Convert all keys to lowercase
222    #[must_use]
223    pub fn lowercase_keys(mut self, value: bool) -> Self {
224        self.lowercase_keys = value;
225        self
226    }
227
228    /// Add a key replacement pattern
229    ///
230    /// Patterns are literal (exact substring match) by default. Wrap a pattern in
231    /// `r'...'` (e.g. `r'^admin_'`) to use standard Rust regex syntax instead. A
232    /// malformed `r'...'` pattern is silently treated as "no match" rather than
233    /// raising an error. Works for both flatten and unflatten operations.
234    ///
235    /// # Examples
236    ///
237    /// ```rust
238    /// use json_tools_rs::{JSONTools, JsonOutput};
239    ///
240    /// // Regex pattern, via the r'...' wrapper
241    /// let json = r#"{"user_name": "John", "admin_name": "Jane"}"#;
242    /// let result = JSONTools::new()
243    ///     .flatten()
244    ///     .key_replacement("r'(user|admin)_'", "person_")
245    ///     .execute(json).unwrap();
246    ///
247    /// // Literal pattern (the default -- no r'...' wrapper)
248    /// let result2 = JSONTools::new()
249    ///     .flatten()
250    ///     .key_replacement("user_", "person_")
251    ///     .execute(json).unwrap();
252    /// ```
253    #[must_use]
254    pub fn key_replacement(mut self, find: impl Into<String>, replace: impl Into<String>) -> Self {
255        self.key_replacements.push((find.into(), replace.into()));
256        self
257    }
258
259    /// Add a value replacement pattern
260    ///
261    /// Patterns are literal (exact substring match) by default. Wrap a pattern in
262    /// `r'...'` (e.g. `r'^admin_'`) to use standard Rust regex syntax instead. A
263    /// malformed `r'...'` pattern is silently treated as "no match" rather than
264    /// raising an error. Works for both flatten and unflatten operations.
265    ///
266    /// # Examples
267    ///
268    /// ```rust
269    /// use json_tools_rs::{JSONTools, JsonOutput};
270    ///
271    /// // Regex pattern, via the r'...' wrapper
272    /// let json = r#"{"role": "super", "level": "admin"}"#;
273    /// let result = JSONTools::new()
274    ///     .flatten()
275    ///     .value_replacement("r'^(super|admin)$'", "administrator")
276    ///     .execute(json).unwrap();
277    ///
278    /// // Literal pattern (the default -- no r'...' wrapper)
279    /// let result2 = JSONTools::new()
280    ///     .flatten()
281    ///     .value_replacement("@example.com", "@company.org")
282    ///     .execute(json).unwrap();
283    /// ```
284    #[must_use]
285    pub fn value_replacement(
286        mut self,
287        find: impl Into<String>,
288        replace: impl Into<String>,
289    ) -> Self {
290        self.value_replacements.push((find.into(), replace.into()));
291        self
292    }
293
294    /// Exclude any key (and its entire value/subtree) whose name contains `pattern`
295    ///
296    /// Patterns are literal (exact substring match) by default. Wrap a pattern in
297    /// `r'...'` (e.g. `r'^crypto_'`) to use standard Rust regex syntax instead,
298    /// matching `key_replacement`'s convention. Additive -- call once per keyword to
299    /// exclude multiple.
300    ///
301    /// Checked against the full dot-path in flatten/unflatten mode, and per key at
302    /// each nesting level in normal mode. Matching a container key drops its entire
303    /// subtree without walking it -- a leaf key is caught by the same check at its
304    /// own level. Array elements are never matched (no key name to check).
305    ///
306    /// # Examples
307    ///
308    /// ```rust
309    /// use json_tools_rs::{JSONTools, JsonOutput};
310    ///
311    /// // Matching a container key ("crypto_wallet") drops its entire subtree --
312    /// // "coin" and "balance" never appear in the output, without being individually
313    /// // matched themselves.
314    /// let json = r#"{"user": {"name": "John", "crypto_wallet": {"coin": "BTC", "balance": 100}}}"#;
315    /// let result = JSONTools::new()
316    ///     .flatten()
317    ///     .exclude_key("crypto")
318    ///     .execute(json).unwrap();
319    /// // Output: {"user.name": "John"}
320    /// ```
321    #[must_use]
322    pub fn exclude_key(mut self, pattern: impl Into<String>) -> Self {
323        self.key_exclusions.push(pattern.into());
324        self
325    }
326
327    /// Drop a key-value pair whose value contains `pattern`
328    ///
329    /// Patterns are literal (exact substring match) by default. Wrap a pattern in
330    /// `r'...'` to use regex, matching `exclude_key`'s convention. Additive -- call
331    /// once per pattern to exclude multiple.
332    ///
333    /// Only ever applies to scalar leaf values (strings/numbers/booleans/null) --
334    /// containers have no single value to check, so a value inside a nested object is
335    /// still individually checked, but the object itself never is. Checked against the
336    /// final value *after* any configured `value_replacement`/`auto_convert_types`
337    /// have run, so a value that only matches after being replaced or converted is
338    /// still caught -- matching `remove_nulls`'s ordering guarantee. A no-op at the
339    /// document root (there's no parent key to drop the value from).
340    ///
341    /// **Unflatten-specific note**: string values are matched against their JSON-
342    /// serialized form (including surrounding quotes), not the unescaped logical
343    /// text. Literal patterns are unaffected by this, but a regex with anchors needs
344    /// to account for the quotes, e.g. `r'^"admin"$'` rather than `r'^admin$'`, to
345    /// match a value that's exactly `"admin"`.
346    ///
347    /// # Examples
348    ///
349    /// ```rust
350    /// use json_tools_rs::{JSONTools, JsonOutput};
351    ///
352    /// let json = r#"{"user": {"name": "John", "status": "banned"}}"#;
353    /// let result = JSONTools::new()
354    ///     .flatten()
355    ///     .exclude_value("banned")
356    ///     .execute(json).unwrap();
357    /// // Output: {"user.name": "John"}
358    /// ```
359    #[must_use]
360    pub fn exclude_value(mut self, pattern: impl Into<String>) -> Self {
361        self.value_exclusions.push(pattern.into());
362        self
363    }
364
365    /// Remove keys with empty string values
366    ///
367    /// Works for both flatten and unflatten operations:
368    /// - In flatten mode: removes flattened keys that have empty string values
369    /// - In unflatten mode: removes keys from the unflattened JSON structure that have empty string values
370    #[must_use]
371    pub fn remove_empty_strings(mut self, value: bool) -> Self {
372        self.remove_empty_string_values = value;
373        self
374    }
375
376    /// Remove keys with null values
377    ///
378    /// Works identically in `.flatten()`, `.unflatten()`, and `.normal()` mode, and
379    /// for both single-document and batch input:
380    /// - In flatten mode: removes flattened keys that have null values
381    /// - In unflatten mode: removes keys from the unflattened JSON structure that have null values
382    /// - In normal mode: removes keys (at any nesting depth) that have null values
383    ///
384    /// This check runs *last* in a value's processing pipeline -- after any
385    /// configured `value_replacement` and `auto_convert_types` have both been
386    /// applied -- so it reliably catches a null produced by either of those, not
387    /// just a null present in the original input. A root-level `null` (the entire
388    /// document, not a nested key) is never removed, since there's no parent key to
389    /// omit it under.
390    #[must_use]
391    pub fn remove_nulls(mut self, value: bool) -> Self {
392        self.remove_null_values = value;
393        self
394    }
395
396    /// Remove keys with empty object values
397    ///
398    /// Works for both flatten and unflatten operations:
399    /// - In flatten mode: removes flattened keys that have empty object values
400    /// - In unflatten mode: removes keys from the unflattened JSON structure that have empty object values
401    #[must_use]
402    pub fn remove_empty_objects(mut self, value: bool) -> Self {
403        self.remove_empty_objects = value;
404        self
405    }
406
407    /// Remove keys with empty array values
408    ///
409    /// Works for both flatten and unflatten operations:
410    /// - In flatten mode: removes flattened keys that have empty array values
411    /// - In unflatten mode: removes keys from the unflattened JSON structure that have empty array values
412    #[must_use]
413    pub fn remove_empty_arrays(mut self, value: bool) -> Self {
414        self.remove_empty_arrays = value;
415        self
416    }
417
418    /// Handle key collisions by collecting values into arrays
419    ///
420    /// When enabled, collect all values that would have the same key into an array.
421    /// Works for all operations (flatten, unflatten, normal).
422    #[must_use]
423    pub fn handle_key_collision(mut self, value: bool) -> Self {
424        self.handle_key_collision = value;
425        self
426    }
427
428    /// Enable automatic type conversion from strings to dates, nulls, booleans, and numbers
429    ///
430    /// When enabled, the library will attempt to convert string values to their native
431    /// JSON types:
432    /// - **Dates**: ISO-8601 date/datetime strings are normalized to UTC
433    /// - **Nulls**: "null"/"NULL"/"nil"/"none"/"N/A"/"NA" -> null
434    /// - **Booleans**: "true"/"TRUE"/"True"/"yes"/"on" -> true, "false"/"no"/"off" -> false
435    /// - **Numbers**: "123" -> 123, "1,234.56" -> 1234.56, "$99.99" -> 99.99, "1e5" -> 100000
436    ///
437    /// If conversion fails, the original string value is kept. No errors are thrown.
438    ///
439    /// Works for all operations (flatten, unflatten, normal).
440    ///
441    /// This is equivalent to calling [`Self::convert_dates`], [`Self::convert_nulls`],
442    /// [`Self::convert_booleans`], and [`Self::convert_numbers`] all with the same
443    /// `enable` value -- it only ever flips each category's on/off switch, preserving
444    /// any per-category customization already configured via the `convert_*_config`
445    /// methods. For independent control over each category, or to customize a
446    /// category's behavior (e.g. recognizing extra null tokens, disabling UTC
447    /// normalization for dates, or turning off individual number sub-formats like
448    /// currency/percent/basis-points/suffixes/fractions/radix), use the `convert_*`
449    /// methods directly instead of this one.
450    ///
451    /// # Example
452    /// ```
453    /// use json_tools_rs::{JSONTools, JsonOutput};
454    ///
455    /// let json = r#"{"id": "123", "price": "1,234.56", "active": "true"}"#;
456    /// let result = JSONTools::new()
457    ///     .flatten()
458    ///     .auto_convert_types(true)
459    ///     .execute(json)
460    ///     .unwrap();
461    ///
462    /// match result {
463    ///     JsonOutput::Single(output) => {
464    ///         // Result: {"id": 123, "price": 1234.56, "active": true}
465    ///         assert!(output.contains(r#""id":123"#));
466    ///         assert!(output.contains(r#""price":1234.56"#));
467    ///         assert!(output.contains(r#""active":true"#));
468    ///     }
469    ///     _ => unreachable!(),
470    /// }
471    /// ```
472    #[must_use]
473    pub fn auto_convert_types(mut self, enable: bool) -> Self {
474        self.date_conversion.enabled = enable;
475        self.null_conversion.enabled = enable;
476        self.boolean_conversion.enabled = enable;
477        self.number_conversion.enabled = enable;
478        self
479    }
480
481    /// Enable or disable date/datetime string conversion independently of the other
482    /// type-conversion categories. See [`Self::auto_convert_types`] for the general
483    /// behavior; use [`Self::convert_dates_config`] to customize UTC-normalization
484    /// behavior.
485    ///
486    /// # Example
487    /// ```
488    /// use json_tools_rs::JSONTools;
489    ///
490    /// let tools = JSONTools::new().flatten().convert_dates(true);
491    /// ```
492    #[must_use]
493    pub fn convert_dates(mut self, enable: bool) -> Self {
494        self.date_conversion.enabled = enable;
495        self
496    }
497
498    /// Configure date/datetime conversion with custom settings (e.g. disabling UTC
499    /// normalization, or disabling the UTC assumption for timezone-less datetimes).
500    /// Sets `enabled` from the passed [`crate::DateConversionConfig`]'s own `enabled`
501    /// field.
502    ///
503    /// # Example
504    /// ```
505    /// use json_tools_rs::{JSONTools, DateConversionConfig};
506    ///
507    /// let tools = JSONTools::new().flatten().convert_dates_config(
508    ///     DateConversionConfig::new().enabled(true).assume_utc_for_naive(false),
509    /// );
510    /// ```
511    #[must_use]
512    pub fn convert_dates_config(mut self, config: DateConversionConfig) -> Self {
513        self.date_conversion = config;
514        self
515    }
516
517    /// Enable or disable null-string conversion independently of the other
518    /// type-conversion categories. See [`Self::auto_convert_types`] for the general
519    /// behavior; use [`Self::convert_nulls_config`] to recognize additional tokens.
520    ///
521    /// # Example
522    /// ```
523    /// use json_tools_rs::JSONTools;
524    ///
525    /// let tools = JSONTools::new().flatten().convert_nulls(true);
526    /// ```
527    #[must_use]
528    pub fn convert_nulls(mut self, enable: bool) -> Self {
529        self.null_conversion.enabled = enable;
530        self
531    }
532
533    /// Configure null-string conversion with additional recognized tokens, beyond the
534    /// built-in list. Sets `enabled` from the passed [`crate::NullConversionConfig`]'s
535    /// own `enabled` field.
536    ///
537    /// # Example
538    /// ```
539    /// use json_tools_rs::{JSONTools, NullConversionConfig};
540    ///
541    /// let tools = JSONTools::new().flatten().convert_nulls_config(
542    ///     NullConversionConfig::new().enabled(true).add_extra_token("missing"),
543    /// );
544    /// ```
545    #[must_use]
546    pub fn convert_nulls_config(mut self, config: NullConversionConfig) -> Self {
547        self.null_conversion = config;
548        self
549    }
550
551    /// Enable or disable boolean-string conversion independently of the other
552    /// type-conversion categories. See [`Self::auto_convert_types`] for the general
553    /// behavior; use [`Self::convert_booleans_config`] to recognize additional tokens.
554    ///
555    /// # Example
556    /// ```
557    /// use json_tools_rs::JSONTools;
558    ///
559    /// let tools = JSONTools::new().flatten().convert_booleans(true);
560    /// ```
561    #[must_use]
562    pub fn convert_booleans(mut self, enable: bool) -> Self {
563        self.boolean_conversion.enabled = enable;
564        self
565    }
566
567    /// Configure boolean-string conversion with additional recognized true/false
568    /// tokens, beyond the built-in lists. Sets `enabled` from the passed
569    /// [`crate::BooleanConversionConfig`]'s own `enabled` field.
570    ///
571    /// # Example
572    /// ```
573    /// use json_tools_rs::{JSONTools, BooleanConversionConfig};
574    ///
575    /// let tools = JSONTools::new().flatten().convert_booleans_config(
576    ///     BooleanConversionConfig::new()
577    ///         .enabled(true)
578    ///         .add_extra_true_token("si")
579    ///         .add_extra_false_token("nope"),
580    /// );
581    /// ```
582    #[must_use]
583    pub fn convert_booleans_config(mut self, config: BooleanConversionConfig) -> Self {
584        self.boolean_conversion = config;
585        self
586    }
587
588    /// Enable or disable numeric-string conversion independently of the other
589    /// type-conversion categories. See [`Self::auto_convert_types`] for the general
590    /// behavior; use [`Self::convert_numbers_config`] to disable individual
591    /// sub-formats (currency, percent, basis points, suffixes, fractions, radix).
592    ///
593    /// # Example
594    /// ```
595    /// use json_tools_rs::JSONTools;
596    ///
597    /// let tools = JSONTools::new().flatten().convert_numbers(true);
598    /// ```
599    #[must_use]
600    pub fn convert_numbers(mut self, enable: bool) -> Self {
601        self.number_conversion.enabled = enable;
602        self
603    }
604
605    /// Configure numeric-string conversion, individually toggling sub-formats.
606    /// Plain integers/decimals, scientific notation, and thousands-separator cleanup
607    /// are always applied when `enabled` is true; currency, percent, basis-points,
608    /// suffixes, fractions, and radix parsing can each be disabled independently.
609    /// Sets `enabled` from the passed [`crate::NumberConversionConfig`]'s own
610    /// `enabled` field.
611    ///
612    /// # Example
613    /// ```
614    /// use json_tools_rs::{JSONTools, NumberConversionConfig};
615    ///
616    /// let tools = JSONTools::new().flatten().convert_numbers_config(
617    ///     NumberConversionConfig::new().enabled(true).currency(false),
618    /// );
619    /// ```
620    #[must_use]
621    pub fn convert_numbers_config(mut self, config: NumberConversionConfig) -> Self {
622        self.number_conversion = config;
623        self
624    }
625
626    /// Read the currently configured date-conversion settings. `pub(crate)` --
627    /// used by the Python bindings' read-modify-write kwargs bridging.
628    #[cfg(feature = "python")]
629    pub(crate) fn date_conversion(&self) -> &DateConversionConfig {
630        &self.date_conversion
631    }
632
633    /// Read the currently configured null-conversion settings. `pub(crate)` --
634    /// used by the Python bindings' read-modify-write kwargs bridging.
635    #[cfg(feature = "python")]
636    pub(crate) fn null_conversion(&self) -> &NullConversionConfig {
637        &self.null_conversion
638    }
639
640    /// Read the currently configured boolean-conversion settings. `pub(crate)` --
641    /// used by the Python bindings' read-modify-write kwargs bridging.
642    #[cfg(feature = "python")]
643    pub(crate) fn boolean_conversion(&self) -> &BooleanConversionConfig {
644        &self.boolean_conversion
645    }
646
647    /// Read the currently configured number-conversion settings. `pub(crate)` --
648    /// used by the Python bindings' read-modify-write kwargs bridging.
649    #[cfg(feature = "python")]
650    pub(crate) fn number_conversion(&self) -> &NumberConversionConfig {
651        &self.number_conversion
652    }
653
654    /// Set the minimum batch size for parallel processing (only available with 'parallel' feature)
655    ///
656    /// When processing multiple JSON documents, this threshold determines when to use
657    /// parallel processing. Batches smaller than this threshold will be processed sequentially
658    /// to avoid the overhead of thread spawning.
659    ///
660    /// Default: 100 items (can be overridden with JSON_TOOLS_PARALLEL_THRESHOLD environment variable)
661    ///
662    /// # Arguments
663    ///
664    /// * `threshold` - Minimum number of items in a batch to trigger parallel processing
665    ///
666    /// # Example
667    ///
668    /// ```
669    /// use json_tools_rs::JSONTools;
670    ///
671    /// let tools = JSONTools::new()
672    ///     .flatten()
673    ///     .parallel_threshold(50); // Only use parallelism for batches of 50+ items
674    /// ```
675    #[must_use]
676    pub fn parallel_threshold(mut self, threshold: usize) -> Self {
677        self.parallel_threshold = threshold;
678        self
679    }
680
681    /// Configure the number of threads for parallel processing
682    ///
683    /// By default, the number of logical CPUs is used. This method allows you to override
684    /// that behavior for specific workloads or resource constraints.
685    ///
686    /// # Arguments
687    ///
688    /// * `num_threads` - Number of threads to use (None = use system default)
689    ///
690    /// # Examples
691    ///
692    /// ```
693    /// use json_tools_rs::JSONTools;
694    ///
695    /// let tools = JSONTools::new()
696    ///     .flatten()
697    ///     .num_threads(Some(4)); // Use exactly 4 threads
698    /// ```
699    #[must_use]
700    pub fn num_threads(mut self, num_threads: Option<usize>) -> Self {
701        self.num_threads = num_threads;
702        self
703    }
704
705    /// Configure the threshold for nested parallel processing within individual JSON documents
706    ///
707    /// When flattening or unflattening a single large JSON document, this threshold determines
708    /// when to parallelize the processing of objects and arrays. Only objects/arrays with more
709    /// than this many keys/items will be processed in parallel.
710    ///
711    /// Default: 100 (can be overridden with JSON_TOOLS_NESTED_PARALLEL_THRESHOLD environment variable)
712    ///
713    /// # Arguments
714    ///
715    /// * `threshold` - Minimum number of keys/items to trigger nested parallelism
716    ///
717    /// # Examples
718    ///
719    /// ```
720    /// use json_tools_rs::JSONTools;
721    ///
722    /// let tools = JSONTools::new()
723    ///     .flatten()
724    ///     .nested_parallel_threshold(200); // Only parallelize objects/arrays with 200+ items
725    /// ```
726    #[must_use]
727    pub fn nested_parallel_threshold(mut self, threshold: usize) -> Self {
728        self.nested_parallel_threshold = threshold;
729        self
730    }
731
732    /// Set the maximum array index allowed during unflattening
733    ///
734    /// This prevents denial-of-service attacks where a malicious flattened key like
735    /// `"items.999999999"` would cause allocation of a massive array. Keys with array
736    /// indices exceeding this limit will produce an error during unflattening.
737    ///
738    /// Default: 100,000 (can be overridden with JSON_TOOLS_MAX_ARRAY_INDEX environment variable)
739    #[must_use]
740    pub fn max_array_index(mut self, max: usize) -> Self {
741        self.max_array_index = max;
742        self
743    }
744
745    /// Execute the configured operation on the provided JSON input
746    ///
747    /// This method performs the selected operation based on the mode set by calling
748    /// `.flatten()`, `.unflatten()`, or `.normal()`. If no mode was set, an error is returned.
749    ///
750    /// # Arguments
751    /// * `json_input` - JSON input that can be a single string, multiple strings, or other supported types
752    ///
753    /// # Returns
754    /// * `Result<JsonOutput, Box<dyn Error>>` - The processed JSON result or an error
755    ///
756    /// # Errors
757    /// * Returns an error if no operation mode has been set
758    /// * Returns an error if the JSON input is invalid
759    /// * Returns an error if processing fails for any other reason
760    pub fn execute<'a, T>(&self, json_input: T) -> Result<JsonOutput, JsonToolsError>
761    where
762        T: Into<JsonInput<'a>>,
763    {
764        let mode = self.mode.as_ref().ok_or_else(|| {
765            JsonToolsError::configuration_error(
766                "Operation mode not set. Call .flatten(), .unflatten(), or .normal() before .execute()"
767            )
768        })?;
769
770        if self.separator.is_empty() {
771            return Err(JsonToolsError::configuration_error(
772                "Separator cannot be empty. Use .separator(\".\") or another non-empty string",
773            ));
774        }
775
776        if let Some(0) = self.num_threads {
777            return Err(JsonToolsError::configuration_error(
778                "num_threads must be at least 1. Use None for system default",
779            ));
780        }
781
782        let input = json_input.into();
783        match mode {
784            OperationMode::Flatten => self.execute_flatten(input),
785            OperationMode::Unflatten => self.execute_unflatten(input),
786            OperationMode::Normal => self.execute_normal(input),
787        }
788    }
789
790    /// Generic batch processing helper that eliminates code duplication
791    /// Processes single or multiple JSON inputs using the provided processor function
792    #[inline]
793    fn execute_with_processor<'a, F>(
794        input: JsonInput<'a>,
795        config: &ProcessingConfig,
796        processor: F,
797    ) -> Result<JsonOutput, JsonToolsError>
798    where
799        F: Fn(&str, &ProcessingConfig) -> Result<String, JsonToolsError> + Sync + Send,
800    {
801        match input {
802            JsonInput::Single(json_cow) => {
803                let result = processor(json_cow.as_ref(), config)?;
804                Ok(JsonOutput::Single(result))
805            }
806            JsonInput::Multiple(json_list) => {
807                let results = Self::process_batch(json_list, config, &processor)?;
808                Ok(JsonOutput::Multiple(results))
809            }
810            JsonInput::MultipleOwned(vecs) => {
811                let results = Self::process_batch(&vecs, config, &processor)?;
812                Ok(JsonOutput::Multiple(results))
813            }
814        }
815    }
816
817    /// Process a batch of items (parallel or sequential) using a shared processor function.
818    /// Items must implement AsRef<str> + Sync, covering both &str slices and Cow<str> vecs.
819    fn process_batch<I, F>(
820        items: &[I],
821        config: &ProcessingConfig,
822        processor: &F,
823    ) -> Result<Vec<String>, JsonToolsError>
824    where
825        I: AsRef<str> + Sync,
826        F: Fn(&str, &ProcessingConfig) -> Result<String, JsonToolsError> + Sync + Send,
827    {
828        if items.len() >= config.parallel_threshold {
829            let map_item = |(index, item): (usize, &I)| {
830                processor(item.as_ref(), config)
831                    .map_err(|e| JsonToolsError::batch_processing_error(index, e))
832            };
833
834            // rayon's global pool is already sized to available_parallelism() and persists
835            // across calls -- reuse it directly unless the caller explicitly overrode the
836            // thread count, in which case a dedicated scoped pool is built for this call only
837            // (that override is rare; the common None case stays on the fast persistent pool).
838            return match config.num_threads {
839                None => items.par_iter().enumerate().map(map_item).collect(),
840                Some(n) => {
841                    let pool = rayon::ThreadPoolBuilder::new()
842                        .num_threads(n)
843                        .build()
844                        .map_err(|e| {
845                            JsonToolsError::configuration_error(format!(
846                                "failed to build thread pool with {n} threads: {e}"
847                            ))
848                        })?;
849                    pool.install(|| items.par_iter().enumerate().map(map_item).collect())
850                }
851            };
852        }
853
854        // Sequential processing (default or below threshold)
855        let mut results = Vec::with_capacity(items.len());
856        for (index, item) in items.iter().enumerate() {
857            match processor(item.as_ref(), config) {
858                Ok(result) => results.push(result),
859                Err(e) => return Err(JsonToolsError::batch_processing_error(index, e)),
860            }
861        }
862        Ok(results)
863    }
864
865    fn execute_flatten<'a>(&self, input: JsonInput<'a>) -> Result<JsonOutput, JsonToolsError> {
866        let config = ProcessingConfig::from_json_tools(self);
867        Self::execute_with_processor(input, &config, process_single_json)
868    }
869
870    fn execute_unflatten<'a>(&self, input: JsonInput<'a>) -> Result<JsonOutput, JsonToolsError> {
871        let config = ProcessingConfig::from_json_tools(self);
872        Self::execute_with_processor(input, &config, process_single_json_for_unflatten)
873    }
874
875    fn execute_normal<'a>(&self, input: JsonInput<'a>) -> Result<JsonOutput, JsonToolsError> {
876        let config = ProcessingConfig::from_json_tools(self);
877        Self::execute_with_processor(input, &config, process_single_json_normal)
878    }
879}