Skip to main content

pjson_rs/
config.rs

1//! Global configuration for PJS Core library
2//!
3//! This module provides centralized configuration for all components,
4//! replacing hardcoded constants with configurable values.
5
6pub mod security;
7
8use crate::compression::CompressionConfig;
9pub use security::SecurityConfig;
10
11/// Errors produced by [`PjsConfig::validate`] and its sub-config validators.
12#[derive(Debug, thiserror::Error)]
13pub enum ConfigError {
14    /// A numeric config field must be strictly greater than zero.
15    #[error("config field `{section}.{field}` must be > 0")]
16    MustBePositive {
17        /// Config section name (e.g. `"streaming"`)
18        section: &'static str,
19        /// Field name within that section (e.g. `"max_frame_size"`)
20        field: &'static str,
21    },
22
23    /// Two related config fields violate an ordering or consistency constraint.
24    #[error("config constraint violated in `{section}`: {message}")]
25    InconsistentBounds {
26        /// Config section name (e.g. `"security.sessions"`)
27        section: &'static str,
28        /// Human-readable description of the violated constraint
29        message: &'static str,
30    },
31}
32
33/// Global configuration for PJS library components
34#[derive(Debug, Clone, Default)]
35pub struct PjsConfig {
36    /// Security configuration and limits
37    pub security: SecurityConfig,
38    /// Configuration for compression algorithms
39    pub compression: CompressionConfig,
40    /// Configuration for parsers
41    pub parser: ParserConfig,
42    /// Configuration for streaming
43    pub streaming: StreamingConfig,
44    /// Configuration for SIMD operations
45    pub simd: SimdConfig,
46}
47
48/// Configuration for JSON parsers
49#[derive(Debug, Clone)]
50pub struct ParserConfig {
51    /// Maximum input size in MB
52    pub max_input_size_mb: usize,
53    /// Buffer initial capacity in bytes
54    pub buffer_initial_capacity: usize,
55    /// SIMD minimum size threshold
56    pub simd_min_size: usize,
57    /// Enable semantic type detection
58    pub enable_semantics: bool,
59}
60
61/// Configuration for streaming operations
62#[derive(Debug, Clone)]
63pub struct StreamingConfig {
64    /// Maximum frame size in bytes
65    pub max_frame_size: usize,
66    /// Default chunk size for processing
67    pub default_chunk_size: usize,
68    /// Timeout for operations in milliseconds
69    pub operation_timeout_ms: u64,
70    /// Maximum bandwidth in bytes per second
71    pub max_bandwidth_bps: u64,
72}
73
74/// Configuration for SIMD acceleration
75#[derive(Debug, Clone)]
76pub struct SimdConfig {
77    /// Batch size for SIMD operations
78    pub batch_size: usize,
79    /// Initial capacity for SIMD buffers
80    pub initial_capacity: usize,
81    /// AVX-512 alignment size in bytes
82    pub avx512_alignment: usize,
83    /// Chunk size for vectorized operations
84    pub vectorized_chunk_size: usize,
85    /// Enable statistics collection
86    pub enable_stats: bool,
87}
88
89impl Default for ParserConfig {
90    fn default() -> Self {
91        Self {
92            max_input_size_mb: 100,
93            buffer_initial_capacity: 8192, // 8KB
94            simd_min_size: 4096,           // 4KB
95            enable_semantics: true,
96        }
97    }
98}
99
100impl Default for StreamingConfig {
101    fn default() -> Self {
102        Self {
103            max_frame_size: 64 * 1024, // 64KB
104            default_chunk_size: 1024,
105            operation_timeout_ms: 5000,   // 5 seconds
106            max_bandwidth_bps: 1_000_000, // 1MB/s
107        }
108    }
109}
110
111impl Default for SimdConfig {
112    fn default() -> Self {
113        Self {
114            batch_size: 100,
115            initial_capacity: 8192, // 8KB
116            avx512_alignment: 64,
117            vectorized_chunk_size: 32,
118            enable_stats: false,
119        }
120    }
121}
122
123impl StreamingConfig {
124    /// Validate streaming configuration invariants.
125    ///
126    /// # Errors
127    ///
128    /// Returns [`ConfigError::MustBePositive`] when `max_frame_size` or
129    /// `operation_timeout_ms` is zero.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use pjson_rs::config::StreamingConfig;
135    ///
136    /// StreamingConfig::default().validate().expect("defaults are valid");
137    /// ```
138    pub fn validate(&self) -> Result<(), ConfigError> {
139        if self.max_frame_size == 0 {
140            return Err(ConfigError::MustBePositive {
141                section: "streaming",
142                field: "max_frame_size",
143            });
144        }
145        if self.operation_timeout_ms == 0 {
146            return Err(ConfigError::MustBePositive {
147                section: "streaming",
148                field: "operation_timeout_ms",
149            });
150        }
151        Ok(())
152    }
153}
154
155impl ParserConfig {
156    /// Validate parser configuration invariants.
157    ///
158    /// # Errors
159    ///
160    /// Returns [`ConfigError::MustBePositive`] when `max_input_size_mb` or
161    /// `buffer_initial_capacity` is zero.
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use pjson_rs::config::ParserConfig;
167    ///
168    /// ParserConfig::default().validate().expect("defaults are valid");
169    /// ```
170    pub fn validate(&self) -> Result<(), ConfigError> {
171        if self.max_input_size_mb == 0 {
172            return Err(ConfigError::MustBePositive {
173                section: "parser",
174                field: "max_input_size_mb",
175            });
176        }
177        if self.buffer_initial_capacity == 0 {
178            return Err(ConfigError::MustBePositive {
179                section: "parser",
180                field: "buffer_initial_capacity",
181            });
182        }
183        Ok(())
184    }
185}
186
187impl SimdConfig {
188    /// Validate SIMD configuration invariants.
189    ///
190    /// The `avx512_alignment` field must be a power of two greater than zero,
191    /// because memory alignment requirements are always powers of two in x86.
192    ///
193    /// # Errors
194    ///
195    /// Returns [`ConfigError::MustBePositive`] when `avx512_alignment` is zero.
196    /// Returns [`ConfigError::InconsistentBounds`] when `avx512_alignment` is
197    /// not a power of two.
198    ///
199    /// # Examples
200    ///
201    /// ```
202    /// use pjson_rs::config::SimdConfig;
203    ///
204    /// SimdConfig::default().validate().expect("defaults are valid");
205    /// ```
206    pub fn validate(&self) -> Result<(), ConfigError> {
207        if self.avx512_alignment == 0 {
208            return Err(ConfigError::MustBePositive {
209                section: "simd",
210                field: "avx512_alignment",
211            });
212        }
213        if !self.avx512_alignment.is_power_of_two() {
214            return Err(ConfigError::InconsistentBounds {
215                section: "simd",
216                message: "avx512_alignment must be a power of two",
217            });
218        }
219        Ok(())
220    }
221}
222
223/// Configuration profiles for different use cases
224impl PjsConfig {
225    /// Validate the entire configuration, including all sub-configs.
226    ///
227    /// Validation is fail-fast: the first error encountered is returned.
228    /// The chain order is: `streaming`, `parser`, `simd`, `security`.
229    ///
230    /// # Errors
231    ///
232    /// Returns the first [`ConfigError`] found in any sub-config.
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use pjson_rs::config::PjsConfig;
238    ///
239    /// PjsConfig::default().validate().expect("defaults are valid");
240    /// ```
241    pub fn validate(&self) -> Result<(), ConfigError> {
242        self.streaming.validate()?;
243        self.parser.validate()?;
244        self.simd.validate()?;
245        self.security.validate()?;
246        Ok(())
247    }
248
249    /// Configuration optimized for low latency
250    pub fn low_latency() -> Self {
251        Self {
252            security: SecurityConfig::development(),
253            compression: CompressionConfig::default(),
254            parser: ParserConfig {
255                max_input_size_mb: 10,
256                buffer_initial_capacity: 4096, // 4KB
257                simd_min_size: 2048,           // 2KB
258                enable_semantics: false,       // Disable for speed
259            },
260            streaming: StreamingConfig {
261                max_frame_size: 16 * 1024, // 16KB
262                default_chunk_size: 512,
263                operation_timeout_ms: 1000,    // 1 second
264                max_bandwidth_bps: 10_000_000, // 10MB/s
265            },
266            simd: SimdConfig {
267                batch_size: 50,
268                initial_capacity: 4096, // 4KB
269                avx512_alignment: 64,
270                vectorized_chunk_size: 16,
271                enable_stats: false,
272            },
273        }
274    }
275
276    /// Configuration optimized for high throughput
277    pub fn high_throughput() -> Self {
278        Self {
279            security: SecurityConfig::high_throughput(),
280            compression: CompressionConfig::default(),
281            parser: ParserConfig {
282                max_input_size_mb: 1000,        // 1GB
283                buffer_initial_capacity: 32768, // 32KB
284                simd_min_size: 8192,            // 8KB
285                enable_semantics: true,
286            },
287            streaming: StreamingConfig {
288                max_frame_size: 256 * 1024, // 256KB
289                default_chunk_size: 4096,
290                operation_timeout_ms: 30000,    // 30 seconds
291                max_bandwidth_bps: 100_000_000, // 100MB/s
292            },
293            simd: SimdConfig {
294                batch_size: 500,
295                initial_capacity: 32768, // 32KB
296                avx512_alignment: 64,
297                vectorized_chunk_size: 64,
298                enable_stats: true,
299            },
300        }
301    }
302
303    /// Configuration optimized for mobile/constrained devices
304    pub fn mobile() -> Self {
305        Self {
306            security: SecurityConfig::low_memory(),
307            compression: CompressionConfig {
308                min_array_length: 1,
309                min_string_length: 2,
310                min_frequency_count: 1,
311                uuid_compression_potential: 0.5,
312                string_dict_threshold: 25.0, // Lower threshold
313                delta_threshold: 15.0,       // Lower threshold
314                min_delta_potential: 0.2,
315                run_length_threshold: 10.0, // Lower threshold
316                min_compression_potential: 0.3,
317                min_numeric_sequence_size: 2,
318            },
319            parser: ParserConfig {
320                max_input_size_mb: 10,
321                buffer_initial_capacity: 2048, // 2KB
322                simd_min_size: 1024,           // 1KB
323                enable_semantics: false,
324            },
325            streaming: StreamingConfig {
326                max_frame_size: 8 * 1024, // 8KB
327                default_chunk_size: 256,
328                operation_timeout_ms: 10000, // 10 seconds
329                max_bandwidth_bps: 100_000,  // 100KB/s
330            },
331            simd: SimdConfig {
332                batch_size: 25,
333                initial_capacity: 2048, // 2KB
334                avx512_alignment: 32,   // Smaller alignment
335                vectorized_chunk_size: 8,
336                enable_stats: false,
337            },
338        }
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_default_config() {
348        let config = PjsConfig::default();
349        assert_eq!(config.parser.max_input_size_mb, 100);
350        assert_eq!(config.streaming.max_frame_size, 64 * 1024);
351        assert_eq!(config.simd.batch_size, 100);
352    }
353
354    #[test]
355    fn test_pjs_config_default_validates() {
356        PjsConfig::default()
357            .validate()
358            .expect("PjsConfig::default() must be valid");
359    }
360
361    #[test]
362    fn test_streaming_config_default_validates() {
363        StreamingConfig::default()
364            .validate()
365            .expect("StreamingConfig::default() must be valid");
366    }
367
368    #[test]
369    fn test_parser_config_default_validates() {
370        ParserConfig::default()
371            .validate()
372            .expect("ParserConfig::default() must be valid");
373    }
374
375    #[test]
376    fn test_simd_config_default_validates() {
377        SimdConfig::default()
378            .validate()
379            .expect("SimdConfig::default() must be valid");
380    }
381
382    #[test]
383    fn test_streaming_rejects_zero_max_frame_size() {
384        let cfg = StreamingConfig {
385            max_frame_size: 0,
386            ..StreamingConfig::default()
387        };
388        let err = cfg.validate().unwrap_err();
389        assert!(matches!(
390            err,
391            ConfigError::MustBePositive {
392                section: "streaming",
393                field: "max_frame_size"
394            }
395        ));
396    }
397
398    #[test]
399    fn test_streaming_rejects_zero_operation_timeout_ms() {
400        let cfg = StreamingConfig {
401            operation_timeout_ms: 0,
402            ..StreamingConfig::default()
403        };
404        let err = cfg.validate().unwrap_err();
405        assert!(matches!(
406            err,
407            ConfigError::MustBePositive {
408                section: "streaming",
409                field: "operation_timeout_ms"
410            }
411        ));
412    }
413
414    #[test]
415    fn test_simd_rejects_non_power_of_two_alignment() {
416        let cfg = SimdConfig {
417            avx512_alignment: 3,
418            ..SimdConfig::default()
419        };
420        let err = cfg.validate().unwrap_err();
421        assert!(matches!(
422            err,
423            ConfigError::InconsistentBounds {
424                section: "simd",
425                ..
426            }
427        ));
428    }
429
430    #[test]
431    fn test_low_latency_profile() {
432        let config = PjsConfig::low_latency();
433        assert_eq!(config.streaming.max_frame_size, 16 * 1024);
434        assert!(!config.parser.enable_semantics);
435        assert_eq!(config.streaming.operation_timeout_ms, 1000);
436    }
437
438    #[test]
439    fn test_high_throughput_profile() {
440        let config = PjsConfig::high_throughput();
441        assert_eq!(config.streaming.max_frame_size, 256 * 1024);
442        assert!(config.parser.enable_semantics);
443        assert!(config.simd.enable_stats);
444    }
445
446    #[test]
447    fn test_mobile_profile() {
448        let config = PjsConfig::mobile();
449        assert_eq!(config.streaming.max_frame_size, 8 * 1024);
450        assert_eq!(config.compression.string_dict_threshold, 25.0);
451        assert_eq!(config.simd.vectorized_chunk_size, 8);
452    }
453
454    #[test]
455    fn test_compression_with_custom_config() {
456        use crate::compression::{CompressionConfig, SchemaAnalyzer};
457        use serde_json::json;
458
459        // Create custom compression config with lower thresholds
460        let compression_config = CompressionConfig {
461            string_dict_threshold: 10.0, // Lower threshold for testing
462            min_frequency_count: 1,
463            ..Default::default()
464        };
465
466        let mut analyzer = SchemaAnalyzer::with_config(compression_config);
467
468        // Test data that should trigger dictionary compression with low threshold
469        let data = json!({
470            "users": [
471                {"status": "active", "role": "user"},
472                {"status": "active", "role": "user"}
473            ]
474        });
475
476        let strategy = analyzer.analyze(&data).unwrap();
477
478        // With lower threshold, should detect dictionary compression opportunity
479        match strategy {
480            crate::compression::CompressionStrategy::Dictionary { .. }
481            | crate::compression::CompressionStrategy::Hybrid { .. } => {
482                // Expected with low threshold
483            }
484            _ => {
485                // Also acceptable, depends on specific data characteristics
486            }
487        }
488    }
489}