lnmp_codec/
config.rs

1//! Configuration types for LNMP parsing and encoding.
2
3use crate::equivalence::EquivalenceMapper;
4use crate::normalizer::NormalizationConfig;
5
6use lnmp_core::profile::{LnmpProfile, StrictDeterministicConfig};
7use lnmp_core::StructuralLimits;
8
9/// Parsing mode configuration
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum ParsingMode {
12    /// Strict mode: only accepts canonical LNMP format
13    Strict,
14    /// Loose mode: tolerates formatting variations (default)
15    #[default]
16    Loose,
17}
18
19impl From<LnmpProfile> for ParsingMode {
20    fn from(profile: LnmpProfile) -> Self {
21        match profile {
22            LnmpProfile::Loose => ParsingMode::Loose,
23            LnmpProfile::Standard => ParsingMode::Loose, // Standard outputs canonical but accepts loose input
24            LnmpProfile::Strict => ParsingMode::Strict,
25        }
26    }
27}
28
29// Default implementation derived via #[derive(Default)] on the enum
30
31/// Controls how text input is pre-processed before strict parsing.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum TextInputMode {
34    /// Do not alter text, return errors on malformed inputs.
35    Strict,
36    /// Run text through the lenient sanitizer before strict parsing.
37    Lenient,
38}
39
40/// Parser configuration
41#[derive(Debug, Clone)]
42pub struct ParserConfig {
43    /// Parsing mode (strict or loose)
44    pub mode: ParsingMode,
45    /// Whether to validate checksums when present (v0.3 feature)
46    pub validate_checksums: bool,
47    /// Whether to normalize text values into numeric/boolean types when possible
48    pub normalize_values: bool,
49    /// Whether to require checksums on all fields (v0.3 feature)
50    pub require_checksums: bool,
51    /// Optional maximum nesting depth; if None, no limit is enforced
52    pub max_nesting_depth: Option<usize>,
53    /// How to handle incoming text before lexing/parsing
54    pub text_input_mode: TextInputMode,
55    /// Optional structural limits (depth/field counts/string lengths)
56    pub structural_limits: Option<StructuralLimits>,
57    /// Optional semantic dictionary for equivalence normalization
58    pub semantic_dictionary: Option<lnmp_sfe::SemanticDictionary>,
59    /// Profile configuration from lnmp-core (v0.5.4)
60    pub profile_config: Option<StrictDeterministicConfig>,
61}
62
63impl Default for ParserConfig {
64    fn default() -> Self {
65        Self {
66            mode: ParsingMode::Loose,
67            validate_checksums: false,
68            normalize_values: true,
69            require_checksums: false,
70            max_nesting_depth: None,
71            text_input_mode: TextInputMode::Strict,
72            structural_limits: None,
73            semantic_dictionary: None,
74            profile_config: None, // None means use standard defaults
75        }
76    }
77}
78
79impl ParserConfig {
80    /// Creates a parser config from an LnmpProfile
81    pub fn from_profile(profile: LnmpProfile) -> Self {
82        let config = profile.config();
83        Self {
84            mode: profile.into(),
85            validate_checksums: config.require_type_hints, // Validate in strict mode
86            normalize_values: !config.canonical_boolean,   // Don't normalize in strict mode
87            require_checksums: false,                      // Checksums still optional
88            max_nesting_depth: None,
89            text_input_mode: TextInputMode::Strict,
90            structural_limits: None,
91            semantic_dictionary: None,
92            profile_config: Some(config),
93        }
94    }
95
96    /// Applies a profile configuration
97    pub fn with_profile_config(mut self, config: StrictDeterministicConfig) -> Self {
98        self.profile_config = Some(config.clone());
99        // Update parsing mode based on config
100        if config.reject_unsorted_fields {
101            self.mode = ParsingMode::Strict;
102        }
103        self
104    }
105
106    /// Applies structural limits to the parser.
107    pub fn with_structural_limits(mut self, limits: StructuralLimits) -> Self {
108        self.structural_limits = Some(limits);
109        self
110    }
111
112    /// Attaches a semantic dictionary for equivalence normalization.
113    pub fn with_semantic_dictionary(mut self, dict: lnmp_sfe::SemanticDictionary) -> Self {
114        self.semantic_dictionary = Some(dict);
115        self
116    }
117}
118
119/// Prompt optimization configuration for LLM-optimized encoding
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
121pub struct PromptOptimizationConfig {
122    /// Whether to minimize symbols for better tokenization
123    pub minimize_symbols: bool,
124    /// Whether to align with common tokenizer boundaries
125    pub align_token_boundaries: bool,
126    /// Whether to optimize array encoding
127    pub optimize_arrays: bool,
128}
129
130// Default implementation derived via #[derive(Default)] on the struct
131
132/// Encoder configuration
133#[derive(Debug, Clone)]
134pub struct EncoderConfig {
135    /// Whether to include type hints in output
136    pub include_type_hints: bool,
137    /// Whether to use canonical format (always true for v0.2)
138    pub canonical: bool,
139    /// Whether to append semantic checksums (v0.3 feature)
140    pub enable_checksums: bool,
141    /// Whether to enable explain mode with inline comments (v0.3 feature)
142    pub enable_explain_mode: bool,
143    /// Prompt optimization configuration (v0.3 feature)
144    pub prompt_optimization: PromptOptimizationConfig,
145    /// Value normalization configuration (v0.3 feature)
146    pub normalization_config: NormalizationConfig,
147    /// Semantic equivalence mapper (v0.3 feature)
148    pub equivalence_mapper: Option<EquivalenceMapper>,
149    /// Optional semantic dictionary for value normalization
150    pub semantic_dictionary: Option<lnmp_sfe::SemanticDictionary>,
151}
152
153impl Default for EncoderConfig {
154    fn default() -> Self {
155        Self {
156            include_type_hints: false,
157            canonical: true,
158            enable_checksums: false,
159            enable_explain_mode: false,
160            prompt_optimization: PromptOptimizationConfig::default(),
161            normalization_config: NormalizationConfig::default(),
162            equivalence_mapper: None,
163            semantic_dictionary: None,
164        }
165    }
166}
167
168impl EncoderConfig {
169    /// Creates a new encoder configuration with default settings
170    pub fn new() -> Self {
171        Self::default()
172    }
173
174    /// Enables semantic checksums
175    pub fn with_checksums(mut self, enable: bool) -> Self {
176        self.enable_checksums = enable;
177        self
178    }
179
180    /// Enables explain mode with inline comments
181    pub fn with_explain_mode(mut self, enable: bool) -> Self {
182        self.enable_explain_mode = enable;
183        self
184    }
185
186    /// Sets prompt optimization configuration
187    pub fn with_prompt_optimization(mut self, config: PromptOptimizationConfig) -> Self {
188        self.prompt_optimization = config;
189        self
190    }
191
192    /// Sets value normalization configuration
193    pub fn with_normalization(mut self, config: NormalizationConfig) -> Self {
194        self.normalization_config = config;
195        self
196    }
197
198    /// Sets semantic equivalence mapper
199    pub fn with_equivalence_mapper(mut self, mapper: EquivalenceMapper) -> Self {
200        self.equivalence_mapper = Some(mapper);
201        self
202    }
203
204    /// Attaches a semantic dictionary for normalization.
205    pub fn with_semantic_dictionary(mut self, dict: lnmp_sfe::SemanticDictionary) -> Self {
206        self.semantic_dictionary = Some(dict);
207        self
208    }
209
210    /// Enables type hints in output
211    pub fn with_type_hints(mut self, enable: bool) -> Self {
212        self.include_type_hints = enable;
213        self
214    }
215
216    /// Sets canonical format mode
217    pub fn with_canonical(mut self, enable: bool) -> Self {
218        self.canonical = enable;
219        self
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_parsing_mode_default() {
229        assert_eq!(ParsingMode::default(), ParsingMode::Loose);
230    }
231
232    #[test]
233    fn test_encoder_config_default() {
234        let config = EncoderConfig::default();
235        assert!(!config.include_type_hints);
236        assert!(config.canonical);
237        assert!(!config.enable_checksums);
238        assert!(!config.enable_explain_mode);
239        assert!(!config.prompt_optimization.minimize_symbols);
240        assert!(!config.prompt_optimization.align_token_boundaries);
241        assert!(!config.prompt_optimization.optimize_arrays);
242        assert!(config.equivalence_mapper.is_none());
243    }
244
245    #[test]
246    fn test_parsing_mode_equality() {
247        assert_eq!(ParsingMode::Strict, ParsingMode::Strict);
248        assert_eq!(ParsingMode::Loose, ParsingMode::Loose);
249        assert_ne!(ParsingMode::Strict, ParsingMode::Loose);
250    }
251
252    #[test]
253    fn test_encoder_config_with_checksums() {
254        let config = EncoderConfig::new().with_checksums(true);
255        assert!(config.enable_checksums);
256    }
257
258    #[test]
259    fn test_encoder_config_with_explain_mode() {
260        let config = EncoderConfig::new().with_explain_mode(true);
261        assert!(config.enable_explain_mode);
262    }
263
264    #[test]
265    fn test_encoder_config_with_prompt_optimization() {
266        let prompt_opt = PromptOptimizationConfig {
267            minimize_symbols: true,
268            align_token_boundaries: true,
269            optimize_arrays: true,
270        };
271        let config = EncoderConfig::new().with_prompt_optimization(prompt_opt);
272        assert!(config.prompt_optimization.minimize_symbols);
273        assert!(config.prompt_optimization.align_token_boundaries);
274        assert!(config.prompt_optimization.optimize_arrays);
275    }
276
277    #[test]
278    fn test_encoder_config_with_normalization() {
279        use crate::normalizer::StringCaseRule;
280
281        let norm_config = NormalizationConfig {
282            string_case: StringCaseRule::Lower,
283            float_precision: Some(2),
284            remove_trailing_zeros: true,
285            semantic_dictionary: None,
286        };
287        let config = EncoderConfig::new().with_normalization(norm_config.clone());
288        assert_eq!(
289            config.normalization_config.string_case,
290            StringCaseRule::Lower
291        );
292        assert_eq!(config.normalization_config.float_precision, Some(2));
293        assert!(config.normalization_config.remove_trailing_zeros);
294    }
295
296    #[test]
297    fn test_encoder_config_with_equivalence_mapper() {
298        let mut mapper = EquivalenceMapper::new();
299        mapper.add_mapping(7, "yes".to_string(), "1".to_string());
300
301        let config = EncoderConfig::new().with_equivalence_mapper(mapper);
302        assert!(config.equivalence_mapper.is_some());
303
304        let mapper_ref = config.equivalence_mapper.as_ref().unwrap();
305        assert_eq!(mapper_ref.map(7, "yes"), Some("1".to_string()));
306    }
307
308    #[test]
309    fn test_encoder_config_with_type_hints() {
310        let config = EncoderConfig::new().with_type_hints(true);
311        assert!(config.include_type_hints);
312    }
313
314    #[test]
315    fn test_encoder_config_with_canonical() {
316        let config = EncoderConfig::new().with_canonical(false);
317        assert!(!config.canonical);
318    }
319
320    #[test]
321    fn test_encoder_config_builder_chain() {
322        let mut mapper = EquivalenceMapper::new();
323        mapper.add_mapping(7, "yes".to_string(), "1".to_string());
324
325        let config = EncoderConfig::new()
326            .with_checksums(true)
327            .with_explain_mode(true)
328            .with_type_hints(true)
329            .with_equivalence_mapper(mapper);
330
331        assert!(config.enable_checksums);
332        assert!(config.enable_explain_mode);
333        assert!(config.include_type_hints);
334        assert!(config.equivalence_mapper.is_some());
335    }
336
337    #[test]
338    fn test_prompt_optimization_config_default() {
339        let config = PromptOptimizationConfig::default();
340        assert!(!config.minimize_symbols);
341        assert!(!config.align_token_boundaries);
342        assert!(!config.optimize_arrays);
343    }
344
345    #[test]
346    fn test_parser_config_default() {
347        let config = ParserConfig::default();
348        assert_eq!(config.mode, ParsingMode::Loose);
349        assert!(!config.validate_checksums);
350        assert!(!config.require_checksums);
351        assert!(config.max_nesting_depth.is_none());
352        assert_eq!(config.text_input_mode, TextInputMode::Strict);
353        assert!(config.structural_limits.is_none());
354    }
355
356    #[test]
357    fn test_parser_config_with_checksum_validation() {
358        let config = ParserConfig {
359            mode: ParsingMode::Strict,
360            validate_checksums: true,
361            normalize_values: false,
362            require_checksums: false,
363            max_nesting_depth: None,
364            text_input_mode: TextInputMode::Strict,
365            structural_limits: None,
366            semantic_dictionary: None,
367            profile_config: None,
368        };
369        assert_eq!(config.mode, ParsingMode::Strict);
370        assert!(config.validate_checksums);
371        assert!(!config.require_checksums);
372    }
373
374    #[test]
375    fn test_parser_config_with_required_checksums() {
376        let config = ParserConfig {
377            mode: ParsingMode::Strict,
378            validate_checksums: true,
379            normalize_values: false,
380            require_checksums: true,
381            max_nesting_depth: None,
382            text_input_mode: TextInputMode::Strict,
383            structural_limits: None,
384            semantic_dictionary: None,
385            profile_config: None,
386        };
387        assert!(config.validate_checksums);
388        assert!(config.require_checksums);
389    }
390
391    #[test]
392    fn test_parser_config_with_structural_limits() {
393        let limits = StructuralLimits {
394            max_fields: 2,
395            ..Default::default()
396        };
397        let config = ParserConfig::default().with_structural_limits(limits.clone());
398        assert_eq!(
399            config.structural_limits.unwrap().max_fields,
400            limits.max_fields
401        );
402    }
403}