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}