Skip to main content

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}