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)]
46pub struct FilteringConfig {
47    /// Remove keys with empty string values
48    pub remove_empty_strings: bool,
49    /// Remove keys with null values
50    pub remove_nulls: bool,
51    /// Remove keys with empty object values
52    pub remove_empty_objects: bool,
53    /// Remove keys with empty array values
54    pub remove_empty_arrays: bool,
55}
56
57impl FilteringConfig {
58    /// Create a new FilteringConfig with all filters disabled
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Enable removal of empty strings
64    #[must_use]
65    pub fn remove_empty_strings(mut self, enabled: bool) -> Self {
66        self.remove_empty_strings = enabled;
67        self
68    }
69
70    /// Enable removal of null values
71    #[must_use]
72    pub fn remove_nulls(mut self, enabled: bool) -> Self {
73        self.remove_nulls = enabled;
74        self
75    }
76
77    /// Enable removal of empty objects
78    #[must_use]
79    pub fn remove_empty_objects(mut self, enabled: bool) -> Self {
80        self.remove_empty_objects = enabled;
81        self
82    }
83
84    /// Enable removal of empty arrays
85    #[must_use]
86    pub fn remove_empty_arrays(mut self, enabled: bool) -> Self {
87        self.remove_empty_arrays = enabled;
88        self
89    }
90
91    /// Check if any filtering is enabled
92    pub fn has_any_filter(&self) -> bool {
93        self.remove_empty_strings
94            || self.remove_nulls
95            || self.remove_empty_objects
96            || self.remove_empty_arrays
97    }
98}
99
100/// Configuration for collision handling strategies
101#[derive(Debug, Clone, Default)]
102pub struct CollisionConfig {
103    /// Handle key collisions by collecting values into arrays
104    pub handle_collisions: bool,
105}
106
107impl CollisionConfig {
108    /// Create a new CollisionConfig with collision handling disabled
109    pub fn new() -> Self {
110        Self::default()
111    }
112
113    /// Enable collision handling by collecting values into arrays
114    #[must_use]
115    pub fn handle_collisions(mut self, enabled: bool) -> Self {
116        self.handle_collisions = enabled;
117        self
118    }
119
120    /// Check if any collision handling is enabled
121    pub fn has_collision_handling(&self) -> bool {
122        self.handle_collisions
123    }
124}
125
126/// Configuration for replacement operations
127#[derive(Debug, Clone, Default)]
128pub struct ReplacementConfig {
129    /// Key replacement patterns (find, replace)
130    /// Uses SmallVec to avoid heap allocation for 0-2 replacements (common case)
131    pub key_replacements: SmallVec<[(String, String); 2]>,
132    /// Value replacement patterns (find, replace)
133    /// Uses SmallVec to avoid heap allocation for 0-2 replacements (common case)
134    pub value_replacements: SmallVec<[(String, String); 2]>,
135}
136
137impl ReplacementConfig {
138    /// Create a new ReplacementConfig with no replacements
139    pub fn new() -> Self {
140        Self {
141            key_replacements: SmallVec::new(),
142            value_replacements: SmallVec::new(),
143        }
144    }
145
146    /// Add a key replacement pattern
147    #[must_use]
148    pub fn add_key_replacement(
149        mut self,
150        find: impl Into<String>,
151        replace: impl Into<String>,
152    ) -> Self {
153        self.key_replacements.push((find.into(), replace.into()));
154        self
155    }
156
157    /// Add a value replacement pattern
158    #[must_use]
159    pub fn add_value_replacement(
160        mut self,
161        find: impl Into<String>,
162        replace: impl Into<String>,
163    ) -> Self {
164        self.value_replacements.push((find.into(), replace.into()));
165        self
166    }
167
168    /// Check if any key replacements are configured
169    pub fn has_key_replacements(&self) -> bool {
170        !self.key_replacements.is_empty()
171    }
172
173    /// Check if any value replacements are configured
174    pub fn has_value_replacements(&self) -> bool {
175        !self.value_replacements.is_empty()
176    }
177}
178
179/// Comprehensive configuration for JSON processing operations
180#[derive(Debug, Clone)]
181pub struct ProcessingConfig {
182    /// Separator for nested keys (default: ".")
183    pub separator: String,
184    /// Convert all keys to lowercase
185    pub lowercase_keys: bool,
186    /// Filtering configuration
187    pub filtering: FilteringConfig,
188    /// Collision handling configuration
189    pub collision: CollisionConfig,
190    /// Replacement configuration
191    pub replacements: ReplacementConfig,
192    /// Automatically convert string values to numbers and booleans
193    pub auto_convert_types: bool,
194    /// Minimum batch size for parallel processing
195    pub parallel_threshold: usize,
196    /// Number of threads for parallel processing (None = use system default)
197    pub num_threads: Option<usize>,
198    /// Minimum object/array size for nested parallel processing within a single JSON document
199    /// Only objects/arrays with more than this many keys/items will be processed in parallel
200    /// Default: 100 (can be overridden with JSON_TOOLS_NESTED_PARALLEL_THRESHOLD environment variable)
201    pub nested_parallel_threshold: usize,
202    /// Maximum array index allowed during unflattening to prevent DoS via malicious keys
203    /// Default: 100,000 (can be overridden with JSON_TOOLS_MAX_ARRAY_INDEX environment variable)
204    pub max_array_index: usize,
205}
206
207impl Default for ProcessingConfig {
208    fn default() -> Self {
209        Self {
210            separator: ".".to_string(),
211            lowercase_keys: false,
212            filtering: FilteringConfig::default(),
213            collision: CollisionConfig::default(),
214            replacements: ReplacementConfig::default(),
215            auto_convert_types: false,
216            parallel_threshold: *DEFAULT_PARALLEL_THRESHOLD,
217            num_threads: None, // Use system default (number of logical CPUs)
218            nested_parallel_threshold: *DEFAULT_NESTED_PARALLEL_THRESHOLD,
219            max_array_index: *DEFAULT_MAX_ARRAY_INDEX,
220        }
221    }
222}
223
224impl ProcessingConfig {
225    /// Create a new ProcessingConfig with default settings
226    pub fn new() -> Self {
227        Self::default()
228    }
229
230    /// Resolve the thread count to use for a parallel workload of `item_count` items,
231    /// honoring an explicit `num_threads` override and never exceeding `item_count`.
232    pub(crate) fn effective_thread_count(&self, item_count: usize) -> usize {
233        let base = self.num_threads.unwrap_or_else(|| {
234            std::thread::available_parallelism()
235                .map(|p| p.get())
236                .unwrap_or(4)
237        });
238        base.max(1).min(item_count)
239    }
240
241    /// Set the separator for nested keys
242    #[must_use]
243    pub fn separator(mut self, separator: impl Into<String>) -> Self {
244        self.separator = separator.into();
245        self
246    }
247
248    /// Enable lowercase key conversion
249    #[must_use]
250    pub fn lowercase_keys(mut self, enabled: bool) -> Self {
251        self.lowercase_keys = enabled;
252        self
253    }
254
255    /// Configure filtering options
256    #[must_use]
257    pub fn filtering(mut self, filtering: FilteringConfig) -> Self {
258        self.filtering = filtering;
259        self
260    }
261
262    /// Configure collision handling options
263    #[must_use]
264    pub fn collision(mut self, collision: CollisionConfig) -> Self {
265        self.collision = collision;
266        self
267    }
268
269    /// Configure replacement options
270    #[must_use]
271    pub fn replacements(mut self, replacements: ReplacementConfig) -> Self {
272        self.replacements = replacements;
273        self
274    }
275}