json_tools_rs/config.rs
1//! Configuration types for JSONTools operations.
2//!
3//! Defines operation modes, filtering flags, collision handling, replacement
4//! patterns, and processing thresholds. Supports environment variable overrides
5//! for parallelism settings.
6
7use smallvec::SmallVec;
8use std::sync::LazyLock;
9
10/// Parse an environment variable once at process startup.
11pub(crate) fn parse_env_usize(name: &str, default: usize) -> usize {
12 std::env::var(name)
13 .ok()
14 .and_then(|v| v.parse().ok())
15 .unwrap_or(default)
16}
17
18/// Parse an optional environment variable (returns None if unset).
19pub(crate) fn parse_env_usize_opt(name: &str) -> Option<usize> {
20 std::env::var(name).ok().and_then(|v| v.parse().ok())
21}
22
23pub(crate) static DEFAULT_PARALLEL_THRESHOLD: LazyLock<usize> =
24 LazyLock::new(|| parse_env_usize("JSON_TOOLS_PARALLEL_THRESHOLD", 100));
25pub(crate) static DEFAULT_NESTED_PARALLEL_THRESHOLD: LazyLock<usize> =
26 LazyLock::new(|| parse_env_usize("JSON_TOOLS_NESTED_PARALLEL_THRESHOLD", 100));
27pub(crate) static DEFAULT_MAX_ARRAY_INDEX: LazyLock<usize> =
28 LazyLock::new(|| parse_env_usize("JSON_TOOLS_MAX_ARRAY_INDEX", 100_000));
29pub(crate) static DEFAULT_NUM_THREADS: LazyLock<Option<usize>> =
30 LazyLock::new(|| parse_env_usize_opt("JSON_TOOLS_NUM_THREADS"));
31
32/// Operation mode for the unified JSONTools API
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[repr(u8)] // Smaller discriminant for better cache locality
35pub(crate) enum OperationMode {
36 /// Flatten JSON structures
37 Flatten,
38 /// Unflatten JSON structures
39 Unflatten,
40 /// Normal processing (no flatten/unflatten) applying transformations recursively
41 Normal,
42}
43
44/// Configuration for filtering operations
45#[derive(Debug, Clone, Default)]
46#[non_exhaustive]
47pub struct FilteringConfig {
48 /// Remove keys with empty string values
49 pub remove_empty_strings: bool,
50 /// Remove keys with null values
51 pub remove_nulls: bool,
52 /// Remove keys with empty object values
53 pub remove_empty_objects: bool,
54 /// Remove keys with empty array values
55 pub remove_empty_arrays: bool,
56}
57
58impl FilteringConfig {
59 /// Create a new FilteringConfig with all filters disabled
60 pub fn new() -> Self {
61 Self::default()
62 }
63
64 /// Enable removal of empty strings
65 #[must_use]
66 pub fn remove_empty_strings(mut self, enabled: bool) -> Self {
67 self.remove_empty_strings = enabled;
68 self
69 }
70
71 /// Enable removal of null values
72 #[must_use]
73 pub fn remove_nulls(mut self, enabled: bool) -> Self {
74 self.remove_nulls = enabled;
75 self
76 }
77
78 /// Enable removal of empty objects
79 #[must_use]
80 pub fn remove_empty_objects(mut self, enabled: bool) -> Self {
81 self.remove_empty_objects = enabled;
82 self
83 }
84
85 /// Enable removal of empty arrays
86 #[must_use]
87 pub fn remove_empty_arrays(mut self, enabled: bool) -> Self {
88 self.remove_empty_arrays = enabled;
89 self
90 }
91
92 /// Check if any filtering is enabled
93 pub fn has_any_filter(&self) -> bool {
94 self.remove_empty_strings
95 || self.remove_nulls
96 || self.remove_empty_objects
97 || self.remove_empty_arrays
98 }
99}
100
101/// Configuration for collision handling strategies
102#[derive(Debug, Clone, Default)]
103#[non_exhaustive]
104pub struct CollisionConfig {
105 /// Handle key collisions by collecting values into arrays
106 pub handle_collisions: bool,
107}
108
109impl CollisionConfig {
110 /// Create a new CollisionConfig with collision handling disabled
111 pub fn new() -> Self {
112 Self::default()
113 }
114
115 /// Enable collision handling by collecting values into arrays
116 #[must_use]
117 pub fn handle_collisions(mut self, enabled: bool) -> Self {
118 self.handle_collisions = enabled;
119 self
120 }
121
122 /// Check if any collision handling is enabled
123 pub fn has_collision_handling(&self) -> bool {
124 self.handle_collisions
125 }
126}
127
128/// Configuration for replacement operations
129#[derive(Debug, Clone, Default)]
130#[non_exhaustive]
131pub struct ReplacementConfig {
132 /// Key replacement patterns (find, replace)
133 /// Uses SmallVec to avoid heap allocation for 0-2 replacements (common case)
134 pub key_replacements: SmallVec<[(String, String); 2]>,
135 /// Value replacement patterns (find, replace)
136 /// Uses SmallVec to avoid heap allocation for 0-2 replacements (common case)
137 pub value_replacements: SmallVec<[(String, String); 2]>,
138}
139
140impl ReplacementConfig {
141 /// Create a new ReplacementConfig with no replacements
142 pub fn new() -> Self {
143 Self {
144 key_replacements: SmallVec::new(),
145 value_replacements: SmallVec::new(),
146 }
147 }
148
149 /// Add a key replacement pattern
150 #[must_use]
151 pub fn add_key_replacement(
152 mut self,
153 find: impl Into<String>,
154 replace: impl Into<String>,
155 ) -> Self {
156 self.key_replacements.push((find.into(), replace.into()));
157 self
158 }
159
160 /// Add a value replacement pattern
161 #[must_use]
162 pub fn add_value_replacement(
163 mut self,
164 find: impl Into<String>,
165 replace: impl Into<String>,
166 ) -> Self {
167 self.value_replacements.push((find.into(), replace.into()));
168 self
169 }
170
171 /// Check if any key replacements are configured
172 pub fn has_key_replacements(&self) -> bool {
173 !self.key_replacements.is_empty()
174 }
175
176 /// Check if any value replacements are configured
177 pub fn has_value_replacements(&self) -> bool {
178 !self.value_replacements.is_empty()
179 }
180}
181
182/// Configuration for date/datetime string detection and UTC normalization, used by
183/// [`TypeConversionConfig`].
184#[derive(Debug, Clone, PartialEq)]
185#[non_exhaustive]
186pub struct DateConversionConfig {
187 /// Enable date/datetime detection and conversion
188 pub enabled: bool,
189 /// Normalize recognized dates/datetimes to UTC (default: true). When false, a
190 /// value recognized as a date/datetime is left byte-for-byte unchanged, but is
191 /// still protected from being misinterpreted as a number by the numbers category.
192 pub normalize_to_utc: bool,
193 /// For naive datetimes with no timezone info (e.g. `"2024-01-15T10:30:00"`),
194 /// assume UTC and append `Z` (default: true). When false, naive datetimes are
195 /// left as-is (still protected from number parsing, but not rewritten) since
196 /// their true offset is unknown.
197 pub assume_utc_for_naive: bool,
198}
199
200impl Default for DateConversionConfig {
201 fn default() -> Self {
202 Self {
203 enabled: false,
204 normalize_to_utc: true,
205 assume_utc_for_naive: true,
206 }
207 }
208}
209
210impl DateConversionConfig {
211 /// Create a new DateConversionConfig with default settings (disabled)
212 pub fn new() -> Self {
213 Self::default()
214 }
215
216 /// Enable or disable date/datetime conversion
217 #[must_use]
218 pub fn enabled(mut self, enabled: bool) -> Self {
219 self.enabled = enabled;
220 self
221 }
222
223 /// Configure UTC normalization of recognized dates/datetimes
224 #[must_use]
225 pub fn normalize_to_utc(mut self, enabled: bool) -> Self {
226 self.normalize_to_utc = enabled;
227 self
228 }
229
230 /// Configure whether naive (timezone-less) datetimes are assumed to be UTC
231 #[must_use]
232 pub fn assume_utc_for_naive(mut self, enabled: bool) -> Self {
233 self.assume_utc_for_naive = enabled;
234 self
235 }
236}
237
238/// Configuration for null-string detection, used by [`TypeConversionConfig`].
239#[derive(Debug, Clone, PartialEq, Default)]
240#[non_exhaustive]
241pub struct NullConversionConfig {
242 /// Enable null-string detection and conversion
243 pub enabled: bool,
244 /// Additional strings recognized as null, beyond the built-in list (`"null"`,
245 /// `"NULL"`, `"Null"`, `"nil"`, `"NIL"`, `"Nil"`, `"none"`, `"NONE"`, `"None"`,
246 /// `"N/A"`, `"n/a"`, `"NA"`, `"na"`). Additive only -- cannot narrow the built-in
247 /// list. Matched exactly (case-sensitive).
248 pub extra_tokens: SmallVec<[String; 2]>,
249}
250
251impl NullConversionConfig {
252 /// Create a new NullConversionConfig with default settings (disabled)
253 pub fn new() -> Self {
254 Self::default()
255 }
256
257 /// Enable or disable null-string conversion
258 #[must_use]
259 pub fn enabled(mut self, enabled: bool) -> Self {
260 self.enabled = enabled;
261 self
262 }
263
264 /// Add an additional string to recognize as null, beyond the built-in list
265 #[must_use]
266 pub fn add_extra_token(mut self, token: impl Into<String>) -> Self {
267 self.extra_tokens.push(token.into());
268 self
269 }
270
271 /// Check if any extra null tokens are configured
272 pub fn has_extra_tokens(&self) -> bool {
273 !self.extra_tokens.is_empty()
274 }
275}
276
277/// Configuration for boolean-string detection, used by [`TypeConversionConfig`].
278#[derive(Debug, Clone, PartialEq, Default)]
279#[non_exhaustive]
280pub struct BooleanConversionConfig {
281 /// Enable boolean-string detection and conversion
282 pub enabled: bool,
283 /// Additional strings recognized as `true`, beyond the built-in list (`"true"`,
284 /// `"TRUE"`, `"True"`, `"yes"`, `"YES"`, `"Yes"`, `"y"`, `"Y"`, `"on"`, `"ON"`,
285 /// `"On"`). Additive only. Matched exactly (case-sensitive).
286 pub extra_true_tokens: SmallVec<[String; 2]>,
287 /// Additional strings recognized as `false`, beyond the built-in list (`"false"`,
288 /// `"FALSE"`, `"False"`, `"no"`, `"NO"`, `"No"`, `"n"`, `"N"`, `"off"`, `"OFF"`,
289 /// `"Off"`). Additive only. Matched exactly (case-sensitive).
290 pub extra_false_tokens: SmallVec<[String; 2]>,
291}
292
293impl BooleanConversionConfig {
294 /// Create a new BooleanConversionConfig with default settings (disabled)
295 pub fn new() -> Self {
296 Self::default()
297 }
298
299 /// Enable or disable boolean-string conversion
300 #[must_use]
301 pub fn enabled(mut self, enabled: bool) -> Self {
302 self.enabled = enabled;
303 self
304 }
305
306 /// Add an additional string to recognize as `true`, beyond the built-in list
307 #[must_use]
308 pub fn add_extra_true_token(mut self, token: impl Into<String>) -> Self {
309 self.extra_true_tokens.push(token.into());
310 self
311 }
312
313 /// Add an additional string to recognize as `false`, beyond the built-in list
314 #[must_use]
315 pub fn add_extra_false_token(mut self, token: impl Into<String>) -> Self {
316 self.extra_false_tokens.push(token.into());
317 self
318 }
319
320 /// Check if any extra boolean tokens are configured
321 pub fn has_extra_tokens(&self) -> bool {
322 !self.extra_true_tokens.is_empty() || !self.extra_false_tokens.is_empty()
323 }
324}
325
326/// Configuration for numeric-string detection, used by [`TypeConversionConfig`].
327///
328/// Plain integers/decimals, scientific notation, and thousands-separator cleanup
329/// (`"1,234.56"`, `"1.234,56"`, `"1 234.56"`) are always-on "core" behavior whenever
330/// `enabled` is true -- no one asked to disable unambiguous number parsing. The
331/// remaining sub-formats are individually toggleable because each is "opinionated"
332/// (can reinterpret a string that wasn't meant to be a number) in a way plain numeric
333/// parsing isn't.
334#[derive(Debug, Clone, PartialEq)]
335#[non_exhaustive]
336pub struct NumberConversionConfig {
337 /// Enable numeric-string detection and conversion
338 pub enabled: bool,
339 /// Strip currency symbols (`$`, etc.), 3-letter currency codes (`USD`, `EUR`,
340 /// ... -- only when followed by a space), and credit/debit suffixes (`CR`/`DR`).
341 /// Default: true.
342 pub currency: bool,
343 /// Parse `%`, `\u{2030}` (permille), and `\u{2031}` (per-ten-thousand) suffixes.
344 /// Default: true.
345 pub percent: bool,
346 /// Parse text basis-point suffixes: `"25bp"`/`"25bps"`/`"25 bp"`/`"25 bps"`.
347 /// Default: true.
348 pub basis_points: bool,
349 /// Parse K/M/B/T magnitude suffixes: `"1K"`, `"2.5M"`, `"3B"`, `"1T"`.
350 /// Default: true.
351 pub suffixes: bool,
352 /// Parse fractions: `"1/2"`, `"2 1/2"`. Default: true.
353 pub fractions: bool,
354 /// Parse hex/binary/octal literals: `"0x1A2B"`, `"0b1010"`, `"0o777"`.
355 /// Default: true.
356 pub radix: bool,
357}
358
359impl Default for NumberConversionConfig {
360 fn default() -> Self {
361 Self {
362 enabled: false,
363 currency: true,
364 percent: true,
365 basis_points: true,
366 suffixes: true,
367 fractions: true,
368 radix: true,
369 }
370 }
371}
372
373impl NumberConversionConfig {
374 /// Create a new NumberConversionConfig with default settings (disabled)
375 pub fn new() -> Self {
376 Self::default()
377 }
378
379 /// Enable or disable numeric-string conversion
380 #[must_use]
381 pub fn enabled(mut self, enabled: bool) -> Self {
382 self.enabled = enabled;
383 self
384 }
385
386 /// Configure currency symbol/code/credit-debit-suffix stripping
387 #[must_use]
388 pub fn currency(mut self, enabled: bool) -> Self {
389 self.currency = enabled;
390 self
391 }
392
393 /// Configure percent/permille/per-ten-thousand suffix parsing
394 #[must_use]
395 pub fn percent(mut self, enabled: bool) -> Self {
396 self.percent = enabled;
397 self
398 }
399
400 /// Configure text basis-point suffix parsing
401 #[must_use]
402 pub fn basis_points(mut self, enabled: bool) -> Self {
403 self.basis_points = enabled;
404 self
405 }
406
407 /// Configure K/M/B/T magnitude suffix parsing
408 #[must_use]
409 pub fn suffixes(mut self, enabled: bool) -> Self {
410 self.suffixes = enabled;
411 self
412 }
413
414 /// Configure fraction parsing
415 #[must_use]
416 pub fn fractions(mut self, enabled: bool) -> Self {
417 self.fractions = enabled;
418 self
419 }
420
421 /// Configure hex/binary/octal literal parsing
422 #[must_use]
423 pub fn radix(mut self, enabled: bool) -> Self {
424 self.radix = enabled;
425 self
426 }
427}
428
429/// Bundles all four type-conversion categories (dates, nulls, booleans, numbers).
430/// Assembled by [`crate::JSONTools`]'s `convert_*`/`convert_*_config`/
431/// `auto_convert_types` builder methods and consumed by the processing engine.
432#[derive(Debug, Clone, Default)]
433#[non_exhaustive]
434pub struct TypeConversionConfig {
435 /// Date/datetime conversion settings
436 pub dates: DateConversionConfig,
437 /// Null-string conversion settings
438 pub nulls: NullConversionConfig,
439 /// Boolean-string conversion settings
440 pub booleans: BooleanConversionConfig,
441 /// Numeric-string conversion settings
442 pub numbers: NumberConversionConfig,
443}
444
445impl TypeConversionConfig {
446 /// Create a new TypeConversionConfig with all categories disabled
447 pub fn new() -> Self {
448 Self::default()
449 }
450
451 /// Configure date/datetime conversion
452 #[must_use]
453 pub fn dates(mut self, config: DateConversionConfig) -> Self {
454 self.dates = config;
455 self
456 }
457
458 /// Configure null-string conversion
459 #[must_use]
460 pub fn nulls(mut self, config: NullConversionConfig) -> Self {
461 self.nulls = config;
462 self
463 }
464
465 /// Configure boolean-string conversion
466 #[must_use]
467 pub fn booleans(mut self, config: BooleanConversionConfig) -> Self {
468 self.booleans = config;
469 self
470 }
471
472 /// Configure numeric-string conversion
473 #[must_use]
474 pub fn numbers(mut self, config: NumberConversionConfig) -> Self {
475 self.numbers = config;
476 self
477 }
478
479 /// Check if any type-conversion category is enabled
480 pub fn has_any_enabled(&self) -> bool {
481 self.dates.enabled || self.nulls.enabled || self.booleans.enabled || self.numbers.enabled
482 }
483
484 /// Classify into the fast-path bucket used by the hot per-string call sites in
485 /// `flatten.rs`/`unflatten.rs`/`transform.rs`. Cheap (a handful of field/
486 /// `is_empty()` comparisons, no allocation) -- computed once per `execute()` call
487 /// in `ProcessingConfig::from_json_tools()`, not per string. See
488 /// `TypeConversionMode`'s own doc comment for why this split exists.
489 pub(crate) fn classify(&self) -> TypeConversionMode {
490 if !self.has_any_enabled() {
491 return TypeConversionMode::Disabled;
492 }
493 let all_default = self.dates
494 == DateConversionConfig {
495 enabled: true,
496 ..DateConversionConfig::default()
497 }
498 && self.nulls
499 == NullConversionConfig {
500 enabled: true,
501 ..NullConversionConfig::default()
502 }
503 && self.booleans
504 == BooleanConversionConfig {
505 enabled: true,
506 ..BooleanConversionConfig::default()
507 }
508 && self.numbers
509 == NumberConversionConfig {
510 enabled: true,
511 ..NumberConversionConfig::default()
512 };
513 if all_default {
514 TypeConversionMode::AllDefault
515 } else {
516 TypeConversionMode::Custom
517 }
518 }
519}
520
521/// Precomputed fast-path classification of a [`TypeConversionConfig`], cached on
522/// [`ProcessingConfig`]. Not part of the public API -- purely an internal dispatch
523/// optimization that lets the hot per-string call sites in `flatten.rs`/
524/// `unflatten.rs`/`transform.rs` avoid re-deriving "is this the untouched-default
525/// case" on every single string value. `AllDefault` routes to the original,
526/// unmodified `try_convert_string_to_json_bytes` (zero behavior/performance change
527/// from before this type existed); `Custom` routes to the new
528/// `try_convert_string_to_json_bytes_configured`.
529#[derive(Debug, Clone, Copy, PartialEq, Eq)]
530#[repr(u8)]
531pub(crate) enum TypeConversionMode {
532 /// No type-conversion category is enabled
533 Disabled,
534 /// All four categories are enabled with untouched default sub-settings
535 AllDefault,
536 /// At least one category is enabled with non-default sub-settings, or only a
537 /// subset of categories is enabled
538 Custom,
539}
540
541/// Comprehensive configuration for JSON processing operations
542#[derive(Debug, Clone)]
543#[non_exhaustive]
544pub struct ProcessingConfig {
545 /// Separator for nested keys (default: ".")
546 pub separator: String,
547 /// Convert all keys to lowercase
548 pub lowercase_keys: bool,
549 /// Filtering configuration
550 pub filtering: FilteringConfig,
551 /// Collision handling configuration
552 pub collision: CollisionConfig,
553 /// Replacement configuration
554 pub replacements: ReplacementConfig,
555 /// Type-conversion configuration (dates, nulls, booleans, numbers)
556 pub type_conversion: TypeConversionConfig,
557 /// Precomputed fast-path classification of `type_conversion`, cached here so the
558 /// hot per-string call sites never re-derive it. See `TypeConversionMode`'s doc
559 /// comment.
560 pub(crate) type_conversion_mode: TypeConversionMode,
561 /// Minimum batch size for parallel processing
562 pub parallel_threshold: usize,
563 /// Number of threads for parallel processing (None = use system default)
564 pub num_threads: Option<usize>,
565 /// Minimum object/array size for nested parallel processing within a single JSON document
566 /// Only objects/arrays with more than this many keys/items will be processed in parallel
567 /// Default: 100 (can be overridden with JSON_TOOLS_NESTED_PARALLEL_THRESHOLD environment variable)
568 pub nested_parallel_threshold: usize,
569 /// Maximum array index allowed during unflattening to prevent DoS via malicious keys
570 /// Default: 100,000 (can be overridden with JSON_TOOLS_MAX_ARRAY_INDEX environment variable)
571 pub max_array_index: usize,
572}
573
574impl Default for ProcessingConfig {
575 fn default() -> Self {
576 Self {
577 separator: ".".to_string(),
578 lowercase_keys: false,
579 filtering: FilteringConfig::default(),
580 collision: CollisionConfig::default(),
581 replacements: ReplacementConfig::default(),
582 type_conversion: TypeConversionConfig::default(),
583 type_conversion_mode: TypeConversionMode::Disabled,
584 parallel_threshold: *DEFAULT_PARALLEL_THRESHOLD,
585 num_threads: None, // Use system default (number of logical CPUs)
586 nested_parallel_threshold: *DEFAULT_NESTED_PARALLEL_THRESHOLD,
587 max_array_index: *DEFAULT_MAX_ARRAY_INDEX,
588 }
589 }
590}
591
592impl ProcessingConfig {
593 /// Create a new ProcessingConfig with default settings
594 pub fn new() -> Self {
595 Self::default()
596 }
597
598 /// Resolve the thread count to use for a parallel workload of `item_count` items,
599 /// honoring an explicit `num_threads` override and never exceeding `item_count`.
600 pub(crate) fn effective_thread_count(&self, item_count: usize) -> usize {
601 let base = self.num_threads.unwrap_or_else(|| {
602 std::thread::available_parallelism()
603 .map(|p| p.get())
604 .unwrap_or(4)
605 });
606 base.max(1).min(item_count)
607 }
608
609 /// Set the separator for nested keys
610 #[must_use]
611 pub fn separator(mut self, separator: impl Into<String>) -> Self {
612 self.separator = separator.into();
613 self
614 }
615
616 /// Enable lowercase key conversion
617 #[must_use]
618 pub fn lowercase_keys(mut self, enabled: bool) -> Self {
619 self.lowercase_keys = enabled;
620 self
621 }
622
623 /// Configure filtering options
624 #[must_use]
625 pub fn filtering(mut self, filtering: FilteringConfig) -> Self {
626 self.filtering = filtering;
627 self
628 }
629
630 /// Configure collision handling options
631 #[must_use]
632 pub fn collision(mut self, collision: CollisionConfig) -> Self {
633 self.collision = collision;
634 self
635 }
636
637 /// Configure replacement options
638 #[must_use]
639 pub fn replacements(mut self, replacements: ReplacementConfig) -> Self {
640 self.replacements = replacements;
641 self
642 }
643
644 /// Configure type-conversion options (dates, nulls, booleans, numbers)
645 #[must_use]
646 pub fn type_conversion(mut self, type_conversion: TypeConversionConfig) -> Self {
647 self.type_conversion_mode = type_conversion.classify();
648 self.type_conversion = type_conversion;
649 self
650 }
651}