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