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}