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}