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}