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    CollisionConfig, FilteringConfig, OperationMode, ProcessingConfig, ReplacementConfig,
12    DEFAULT_MAX_ARRAY_INDEX, DEFAULT_NESTED_PARALLEL_THRESHOLD, DEFAULT_NUM_THREADS,
13    DEFAULT_PARALLEL_THRESHOLD,
14};
15use crate::error::JsonToolsError;
16use crate::flatten::process_single_json;
17use crate::transform::process_single_json_normal;
18use crate::types::{JsonInput, JsonOutput};
19use crate::unflatten::process_single_json_for_unflatten;
20
21// ================================================================================================
22// ProcessingConfig::from_json_tools() - lives here to avoid circular dependency
23// ================================================================================================
24
25impl ProcessingConfig {
26    /// Create a ProcessingConfig from a JSONTools builder instance
27    pub fn from_json_tools(tools: &JSONTools) -> Self {
28        Self {
29            separator: tools.separator.clone(),
30            lowercase_keys: tools.lowercase_keys,
31            filtering: FilteringConfig {
32                remove_empty_strings: tools.remove_empty_string_values,
33                remove_nulls: tools.remove_null_values,
34                remove_empty_objects: tools.remove_empty_objects,
35                remove_empty_arrays: tools.remove_empty_arrays,
36            },
37            collision: CollisionConfig {
38                handle_collisions: tools.handle_key_collision,
39            },
40            replacements: ReplacementConfig {
41                key_replacements: tools.key_replacements.clone(),
42                value_replacements: tools.value_replacements.clone(),
43            },
44            auto_convert_types: tools.auto_convert_types,
45            parallel_threshold: tools.parallel_threshold,
46            num_threads: tools.num_threads,
47            nested_parallel_threshold: tools.nested_parallel_threshold,
48            max_array_index: tools.max_array_index,
49        }
50    }
51}
52
53// ================================================================================================
54// JSONTools Builder Struct
55// ================================================================================================
56
57/// Unified JSON Tools API with builder pattern for both flattening and unflattening operations
58///
59/// This is the unified interface for all JSON manipulation operations.
60/// It provides a single entry point for all JSON manipulation operations with a consistent builder pattern.
61#[derive(Debug, Clone)]
62pub struct JSONTools {
63    // SmallVec fields (stack-allocated for 0-2 replacements, common case)
64    /// Key replacement patterns (find, replace)
65    /// Uses SmallVec to avoid heap allocation for 0-2 replacements (90% of use cases)
66    key_replacements: SmallVec<[(String, String); 2]>,
67    /// Value replacement patterns (find, replace)
68    /// Uses SmallVec to avoid heap allocation for 0-2 replacements (90% of use cases)
69    value_replacements: SmallVec<[(String, String); 2]>,
70    /// Separator for nested keys (default: ".")
71    separator: String,
72
73    // Medium fields (8 bytes on 64-bit systems)
74    /// Minimum batch size to use parallel processing (default: 100)
75    parallel_threshold: usize,
76    /// Number of threads for parallel processing (None = use system default)
77    num_threads: Option<usize>,
78    /// Minimum object/array size for nested parallel processing within a single JSON document
79    nested_parallel_threshold: usize,
80    /// Maximum array index allowed during unflattening (DoS protection)
81    max_array_index: usize,
82
83    // Medium fields (2 bytes)
84    /// Current operation mode (flatten or unflatten)
85    mode: Option<OperationMode>,
86
87    // Small fields (1 byte each) - grouped together to minimize padding
88    /// Remove keys with empty string values
89    remove_empty_string_values: bool,
90    /// Remove keys with null values
91    remove_null_values: bool,
92    /// Remove keys with empty object values
93    remove_empty_objects: bool,
94    /// Remove keys with empty array values
95    remove_empty_arrays: bool,
96    /// Convert all keys to lowercase
97    lowercase_keys: bool,
98    /// Handle key collisions by collecting values into arrays
99    handle_key_collision: bool,
100    /// Automatically convert string values to numbers and booleans
101    auto_convert_types: bool,
102}
103
104impl Default for JSONTools {
105    fn default() -> Self {
106        Self {
107            // SmallVec fields - no heap allocation for 0-2 replacements!
108            key_replacements: SmallVec::new(),
109            value_replacements: SmallVec::new(),
110            separator: ".".to_string(),
111            // Medium fields — use shared LazyLock statics from config module
112            parallel_threshold: *DEFAULT_PARALLEL_THRESHOLD,
113            num_threads: *DEFAULT_NUM_THREADS,
114            nested_parallel_threshold: *DEFAULT_NESTED_PARALLEL_THRESHOLD,
115            max_array_index: *DEFAULT_MAX_ARRAY_INDEX,
116            mode: None,
117            // Small fields
118            remove_empty_string_values: false,
119            remove_null_values: false,
120            remove_empty_objects: false,
121            remove_empty_arrays: false,
122            lowercase_keys: false,
123            handle_key_collision: false,
124            auto_convert_types: false,
125        }
126    }
127}
128
129impl JSONTools {
130    /// Create a new JSONTools instance with default settings
131    pub fn new() -> Self {
132        Self::default()
133    }
134
135    /// Set the operation mode to flatten
136    #[must_use]
137    pub fn flatten(mut self) -> Self {
138        self.mode = Some(OperationMode::Flatten);
139        self
140    }
141
142    /// Set the operation mode to unflatten
143    #[must_use]
144    pub fn unflatten(mut self) -> Self {
145        self.mode = Some(OperationMode::Unflatten);
146        self
147    }
148
149    /// Set the operation mode to normal (apply transformations without flatten/unflatten)
150    ///
151    /// In normal mode, key/value replacements, filtering, and type conversion are applied
152    /// recursively to the JSON structure without flattening or unflattening it.
153    ///
154    /// # Example
155    ///
156    /// ```rust
157    /// use json_tools_rs::{JSONTools, JsonOutput};
158    ///
159    /// let json = r#"{"Name": "John", "Age": "30", "Active": "true"}"#;
160    /// let result = JSONTools::new()
161    ///     .normal()
162    ///     .lowercase_keys(true)
163    ///     .auto_convert_types(true)
164    ///     .execute(json).unwrap();
165    ///
166    /// match result {
167    ///     JsonOutput::Single(output) => {
168    ///         assert!(output.contains(r#""name""#));
169    ///         assert!(output.contains(r#":30"#) || output.contains(r#": 30"#));
170    ///     }
171    ///     _ => unreachable!(),
172    /// }
173    /// ```
174    #[must_use]
175    pub fn normal(mut self) -> Self {
176        self.mode = Some(OperationMode::Normal);
177        self
178    }
179
180    /// Set the separator used for nested keys (default: ".")
181    ///
182    /// Empty separators are rejected at [`execute()`](Self::execute) time with a descriptive error.
183    #[must_use]
184    pub fn separator(mut self, separator: impl Into<String>) -> Self {
185        self.separator = separator.into();
186        self
187    }
188
189    /// Convert all keys to lowercase
190    #[must_use]
191    pub fn lowercase_keys(mut self, value: bool) -> Self {
192        self.lowercase_keys = value;
193        self
194    }
195
196    /// Add a key replacement pattern
197    ///
198    /// Patterns are literal (exact substring match) by default. Wrap a pattern in
199    /// `r'...'` (e.g. `r'^admin_'`) to use standard Rust regex syntax instead. A
200    /// malformed `r'...'` pattern is silently treated as "no match" rather than
201    /// raising an error. Works for both flatten and unflatten operations.
202    ///
203    /// # Examples
204    ///
205    /// ```rust
206    /// use json_tools_rs::{JSONTools, JsonOutput};
207    ///
208    /// // Regex pattern, via the r'...' wrapper
209    /// let json = r#"{"user_name": "John", "admin_name": "Jane"}"#;
210    /// let result = JSONTools::new()
211    ///     .flatten()
212    ///     .key_replacement("r'(user|admin)_'", "person_")
213    ///     .execute(json).unwrap();
214    ///
215    /// // Literal pattern (the default -- no r'...' wrapper)
216    /// let result2 = JSONTools::new()
217    ///     .flatten()
218    ///     .key_replacement("user_", "person_")
219    ///     .execute(json).unwrap();
220    /// ```
221    #[must_use]
222    pub fn key_replacement(mut self, find: impl Into<String>, replace: impl Into<String>) -> Self {
223        self.key_replacements.push((find.into(), replace.into()));
224        self
225    }
226
227    /// Add a value replacement pattern
228    ///
229    /// Patterns are literal (exact substring match) by default. Wrap a pattern in
230    /// `r'...'` (e.g. `r'^admin_'`) to use standard Rust regex syntax instead. A
231    /// malformed `r'...'` pattern is silently treated as "no match" rather than
232    /// raising an error. Works for both flatten and unflatten operations.
233    ///
234    /// # Examples
235    ///
236    /// ```rust
237    /// use json_tools_rs::{JSONTools, JsonOutput};
238    ///
239    /// // Regex pattern, via the r'...' wrapper
240    /// let json = r#"{"role": "super", "level": "admin"}"#;
241    /// let result = JSONTools::new()
242    ///     .flatten()
243    ///     .value_replacement("r'^(super|admin)$'", "administrator")
244    ///     .execute(json).unwrap();
245    ///
246    /// // Literal pattern (the default -- no r'...' wrapper)
247    /// let result2 = JSONTools::new()
248    ///     .flatten()
249    ///     .value_replacement("@example.com", "@company.org")
250    ///     .execute(json).unwrap();
251    /// ```
252    #[must_use]
253    pub fn value_replacement(
254        mut self,
255        find: impl Into<String>,
256        replace: impl Into<String>,
257    ) -> Self {
258        self.value_replacements.push((find.into(), replace.into()));
259        self
260    }
261
262    /// Remove keys with empty string values
263    ///
264    /// Works for both flatten and unflatten operations:
265    /// - In flatten mode: removes flattened keys that have empty string values
266    /// - In unflatten mode: removes keys from the unflattened JSON structure that have empty string values
267    #[must_use]
268    pub fn remove_empty_strings(mut self, value: bool) -> Self {
269        self.remove_empty_string_values = value;
270        self
271    }
272
273    /// Remove keys with null values
274    ///
275    /// Works for both flatten and unflatten operations:
276    /// - In flatten mode: removes flattened keys that have null values
277    /// - In unflatten mode: removes keys from the unflattened JSON structure that have null values
278    #[must_use]
279    pub fn remove_nulls(mut self, value: bool) -> Self {
280        self.remove_null_values = value;
281        self
282    }
283
284    /// Remove keys with empty object values
285    ///
286    /// Works for both flatten and unflatten operations:
287    /// - In flatten mode: removes flattened keys that have empty object values
288    /// - In unflatten mode: removes keys from the unflattened JSON structure that have empty object values
289    #[must_use]
290    pub fn remove_empty_objects(mut self, value: bool) -> Self {
291        self.remove_empty_objects = value;
292        self
293    }
294
295    /// Remove keys with empty array values
296    ///
297    /// Works for both flatten and unflatten operations:
298    /// - In flatten mode: removes flattened keys that have empty array values
299    /// - In unflatten mode: removes keys from the unflattened JSON structure that have empty array values
300    #[must_use]
301    pub fn remove_empty_arrays(mut self, value: bool) -> Self {
302        self.remove_empty_arrays = value;
303        self
304    }
305
306    /// Handle key collisions by collecting values into arrays
307    ///
308    /// When enabled, collect all values that would have the same key into an array.
309    /// Works for all operations (flatten, unflatten, normal).
310    #[must_use]
311    pub fn handle_key_collision(mut self, value: bool) -> Self {
312        self.handle_key_collision = value;
313        self
314    }
315
316    /// Enable automatic type conversion from strings to numbers and booleans
317    ///
318    /// When enabled, the library will attempt to convert string values to numbers or booleans:
319    /// - **Numbers**: "123" -> 123, "1,234.56" -> 1234.56, "$99.99" -> 99.99, "1e5" -> 100000
320    /// - **Booleans**: "true"/"TRUE"/"True" -> true, "false"/"FALSE"/"False" -> false
321    ///
322    /// If conversion fails, the original string value is kept. No errors are thrown.
323    ///
324    /// Works for all operations (flatten, unflatten, normal).
325    ///
326    /// # Example
327    /// ```
328    /// use json_tools_rs::{JSONTools, JsonOutput};
329    ///
330    /// let json = r#"{"id": "123", "price": "1,234.56", "active": "true"}"#;
331    /// let result = JSONTools::new()
332    ///     .flatten()
333    ///     .auto_convert_types(true)
334    ///     .execute(json)
335    ///     .unwrap();
336    ///
337    /// match result {
338    ///     JsonOutput::Single(output) => {
339    ///         // Result: {"id": 123, "price": 1234.56, "active": true}
340    ///         assert!(output.contains(r#""id":123"#));
341    ///         assert!(output.contains(r#""price":1234.56"#));
342    ///         assert!(output.contains(r#""active":true"#));
343    ///     }
344    ///     _ => unreachable!(),
345    /// }
346    /// ```
347    #[must_use]
348    pub fn auto_convert_types(mut self, enable: bool) -> Self {
349        self.auto_convert_types = enable;
350        self
351    }
352
353    /// Set the minimum batch size for parallel processing (only available with 'parallel' feature)
354    ///
355    /// When processing multiple JSON documents, this threshold determines when to use
356    /// parallel processing. Batches smaller than this threshold will be processed sequentially
357    /// to avoid the overhead of thread spawning.
358    ///
359    /// Default: 100 items (can be overridden with JSON_TOOLS_PARALLEL_THRESHOLD environment variable)
360    ///
361    /// # Arguments
362    ///
363    /// * `threshold` - Minimum number of items in a batch to trigger parallel processing
364    ///
365    /// # Example
366    ///
367    /// ```
368    /// use json_tools_rs::JSONTools;
369    ///
370    /// let tools = JSONTools::new()
371    ///     .flatten()
372    ///     .parallel_threshold(50); // Only use parallelism for batches of 50+ items
373    /// ```
374    #[must_use]
375    pub fn parallel_threshold(mut self, threshold: usize) -> Self {
376        self.parallel_threshold = threshold;
377        self
378    }
379
380    /// Configure the number of threads for parallel processing
381    ///
382    /// By default, the number of logical CPUs is used. This method allows you to override
383    /// that behavior for specific workloads or resource constraints.
384    ///
385    /// # Arguments
386    ///
387    /// * `num_threads` - Number of threads to use (None = use system default)
388    ///
389    /// # Examples
390    ///
391    /// ```
392    /// use json_tools_rs::JSONTools;
393    ///
394    /// let tools = JSONTools::new()
395    ///     .flatten()
396    ///     .num_threads(Some(4)); // Use exactly 4 threads
397    /// ```
398    #[must_use]
399    pub fn num_threads(mut self, num_threads: Option<usize>) -> Self {
400        self.num_threads = num_threads;
401        self
402    }
403
404    /// Configure the threshold for nested parallel processing within individual JSON documents
405    ///
406    /// When flattening or unflattening a single large JSON document, this threshold determines
407    /// when to parallelize the processing of objects and arrays. Only objects/arrays with more
408    /// than this many keys/items will be processed in parallel.
409    ///
410    /// Default: 100 (can be overridden with JSON_TOOLS_NESTED_PARALLEL_THRESHOLD environment variable)
411    ///
412    /// # Arguments
413    ///
414    /// * `threshold` - Minimum number of keys/items to trigger nested parallelism
415    ///
416    /// # Examples
417    ///
418    /// ```
419    /// use json_tools_rs::JSONTools;
420    ///
421    /// let tools = JSONTools::new()
422    ///     .flatten()
423    ///     .nested_parallel_threshold(200); // Only parallelize objects/arrays with 200+ items
424    /// ```
425    #[must_use]
426    pub fn nested_parallel_threshold(mut self, threshold: usize) -> Self {
427        self.nested_parallel_threshold = threshold;
428        self
429    }
430
431    /// Set the maximum array index allowed during unflattening
432    ///
433    /// This prevents denial-of-service attacks where a malicious flattened key like
434    /// `"items.999999999"` would cause allocation of a massive array. Keys with array
435    /// indices exceeding this limit will produce an error during unflattening.
436    ///
437    /// Default: 100,000 (can be overridden with JSON_TOOLS_MAX_ARRAY_INDEX environment variable)
438    #[must_use]
439    pub fn max_array_index(mut self, max: usize) -> Self {
440        self.max_array_index = max;
441        self
442    }
443
444    /// Execute the configured operation on the provided JSON input
445    ///
446    /// This method performs the selected operation based on the mode set by calling
447    /// `.flatten()`, `.unflatten()`, or `.normal()`. If no mode was set, an error is returned.
448    ///
449    /// # Arguments
450    /// * `json_input` - JSON input that can be a single string, multiple strings, or other supported types
451    ///
452    /// # Returns
453    /// * `Result<JsonOutput, Box<dyn Error>>` - The processed JSON result or an error
454    ///
455    /// # Errors
456    /// * Returns an error if no operation mode has been set
457    /// * Returns an error if the JSON input is invalid
458    /// * Returns an error if processing fails for any other reason
459    pub fn execute<'a, T>(&self, json_input: T) -> Result<JsonOutput, JsonToolsError>
460    where
461        T: Into<JsonInput<'a>>,
462    {
463        let mode = self.mode.as_ref().ok_or_else(|| {
464            JsonToolsError::configuration_error(
465                "Operation mode not set. Call .flatten(), .unflatten(), or .normal() before .execute()"
466            )
467        })?;
468
469        if self.separator.is_empty() {
470            return Err(JsonToolsError::configuration_error(
471                "Separator cannot be empty. Use .separator(\".\") or another non-empty string",
472            ));
473        }
474
475        if let Some(0) = self.num_threads {
476            return Err(JsonToolsError::configuration_error(
477                "num_threads must be at least 1. Use None for system default",
478            ));
479        }
480
481        let input = json_input.into();
482        match mode {
483            OperationMode::Flatten => self.execute_flatten(input),
484            OperationMode::Unflatten => self.execute_unflatten(input),
485            OperationMode::Normal => self.execute_normal(input),
486        }
487    }
488
489    /// Generic batch processing helper that eliminates code duplication
490    /// Processes single or multiple JSON inputs using the provided processor function
491    #[inline]
492    fn execute_with_processor<'a, F>(
493        input: JsonInput<'a>,
494        config: &ProcessingConfig,
495        processor: F,
496    ) -> Result<JsonOutput, JsonToolsError>
497    where
498        F: Fn(&str, &ProcessingConfig) -> Result<String, JsonToolsError> + Sync + Send,
499    {
500        match input {
501            JsonInput::Single(json_cow) => {
502                let result = processor(json_cow.as_ref(), config)?;
503                Ok(JsonOutput::Single(result))
504            }
505            JsonInput::Multiple(json_list) => {
506                let results = Self::process_batch(json_list, config, &processor)?;
507                Ok(JsonOutput::Multiple(results))
508            }
509            JsonInput::MultipleOwned(vecs) => {
510                let results = Self::process_batch(&vecs, config, &processor)?;
511                Ok(JsonOutput::Multiple(results))
512            }
513        }
514    }
515
516    /// Process a batch of items (parallel or sequential) using a shared processor function.
517    /// Items must implement AsRef<str> + Sync, covering both &str slices and Cow<str> vecs.
518    fn process_batch<I, F>(
519        items: &[I],
520        config: &ProcessingConfig,
521        processor: &F,
522    ) -> Result<Vec<String>, JsonToolsError>
523    where
524        I: AsRef<str> + Sync,
525        F: Fn(&str, &ProcessingConfig) -> Result<String, JsonToolsError> + Sync + Send,
526    {
527        if items.len() >= config.parallel_threshold {
528            let map_item = |(index, item): (usize, &I)| {
529                processor(item.as_ref(), config)
530                    .map_err(|e| JsonToolsError::batch_processing_error(index, e))
531            };
532
533            // rayon's global pool is already sized to available_parallelism() and persists
534            // across calls -- reuse it directly unless the caller explicitly overrode the
535            // thread count, in which case a dedicated scoped pool is built for this call only
536            // (that override is rare; the common None case stays on the fast persistent pool).
537            return match config.num_threads {
538                None => items.par_iter().enumerate().map(map_item).collect(),
539                Some(n) => {
540                    let pool = rayon::ThreadPoolBuilder::new()
541                        .num_threads(n)
542                        .build()
543                        .map_err(|e| {
544                            JsonToolsError::configuration_error(format!(
545                                "failed to build thread pool with {n} threads: {e}"
546                            ))
547                        })?;
548                    pool.install(|| items.par_iter().enumerate().map(map_item).collect())
549                }
550            };
551        }
552
553        // Sequential processing (default or below threshold)
554        let mut results = Vec::with_capacity(items.len());
555        for (index, item) in items.iter().enumerate() {
556            match processor(item.as_ref(), config) {
557                Ok(result) => results.push(result),
558                Err(e) => return Err(JsonToolsError::batch_processing_error(index, e)),
559            }
560        }
561        Ok(results)
562    }
563
564    fn execute_flatten<'a>(&self, input: JsonInput<'a>) -> Result<JsonOutput, JsonToolsError> {
565        let config = ProcessingConfig::from_json_tools(self);
566        Self::execute_with_processor(input, &config, process_single_json)
567    }
568
569    fn execute_unflatten<'a>(&self, input: JsonInput<'a>) -> Result<JsonOutput, JsonToolsError> {
570        let config = ProcessingConfig::from_json_tools(self);
571        Self::execute_with_processor(input, &config, process_single_json_for_unflatten)
572    }
573
574    fn execute_normal<'a>(&self, input: JsonInput<'a>) -> Result<JsonOutput, JsonToolsError> {
575        let config = ProcessingConfig::from_json_tools(self);
576        Self::execute_with_processor(input, &config, process_single_json_normal)
577    }
578}