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