lnmp_core/
profile.rs

1//! Protocol profiles for controlling determinism and validation requirements
2//!
3//! The profile system allows configuring LNMP behavior for different use cases:
4//! - **Loose**: Maximum compatibility, minimal validation
5//! - **Standard**: Balanced mode with canonical output but flexible input
6//! - **Strict**: Maximum determinism for LLM drift prevention
7//!
8//! # Example
9//!
10//! ```
11//! use lnmp_core::profile::{LnmpProfile, StrictDeterministicConfig};
12//!
13//! // Use strict profile for LLM applications
14//! let config = StrictDeterministicConfig::strict();
15//! assert!(config.require_type_hints);
16//! assert!(config.reject_unsorted_fields);
17//!
18//! // Or use standard profile for general use
19//! let config = StrictDeterministicConfig::standard();
20//! assert!(!config.require_type_hints);
21//! ```
22
23/// LNMP protocol profile
24///
25/// Profiles define different levels of strictness for parsing, encoding,
26/// and validation of LNMP data.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum LnmpProfile {
29    /// Loose mode: Maximum backward compatibility
30    ///
31    /// - Accepts unsorted fields
32    /// - Type hints optional
33    /// - No canonical form enforcement
34    /// - Best for: Legacy data, migration scenarios
35    Loose,
36
37    /// Standard mode: Balanced approach (default)
38    ///
39    /// - Canonical output (sorted fields)
40    /// - Accepts non-canonical input
41    /// - Type hints optional
42    /// - Best for: General purpose use
43    #[default]
44    Standard,
45
46    /// Strict mode: Maximum determinism
47    ///
48    /// - Enforces canonical form in input/output
49    /// - Type hints required
50    /// - Field order validation
51    /// - Best for: LLM applications, drift prevention
52    Strict,
53}
54
55impl LnmpProfile {
56    /// Returns the string representation of the profile
57    pub fn as_str(&self) -> &'static str {
58        match self {
59            LnmpProfile::Loose => "loose",
60            LnmpProfile::Standard => "standard",
61            LnmpProfile::Strict => "strict",
62        }
63    }
64
65    /// Parses a profile from string
66    pub fn parse(s: &str) -> Option<Self> {
67        match s.to_lowercase().as_str() {
68            "loose" => Some(Self::Loose),
69            "standard" => Some(Self::Standard),
70            "strict" => Some(Self::Strict),
71            _ => None,
72        }
73    }
74
75    /// Returns the config for this profile
76    pub fn config(&self) -> StrictDeterministicConfig {
77        match self {
78            LnmpProfile::Loose => StrictDeterministicConfig::loose(),
79            LnmpProfile::Standard => StrictDeterministicConfig::standard(),
80            LnmpProfile::Strict => StrictDeterministicConfig::strict(),
81        }
82    }
83}
84
85/// Configuration for strict deterministic behavior
86///
87/// Controls validation and canonicalization requirements for parsing
88/// and encoding LNMP data.
89#[derive(Debug, Clone, PartialEq)]
90pub struct StrictDeterministicConfig {
91    /// Reject records with unsorted fields
92    ///
93    /// When `true`, parsing will fail if fields are not in ascending FID order.
94    pub reject_unsorted_fields: bool,
95
96    /// Require type hints on all fields
97    ///
98    /// When `true`, parsing will fail if any field lacks a type hint.
99    pub require_type_hints: bool,
100
101    /// Enforce canonical boolean representation (0/1 only)
102    ///
103    /// When `true`, parsing will fail for non-canonical boolean values
104    /// (e.g., "true", "false", "yes", "no").
105    pub canonical_boolean: bool,
106
107    /// Enforce canonical string normalization
108    ///
109    /// When `true`, strings are normalized (trimmed, case-folded if configured).
110    pub canonical_string: bool,
111
112    /// Minimum binary format version
113    ///
114    /// Reject binary data with version lower than this.
115    /// - 0x04: v0.4 (no nested structures)
116    /// - 0x05: v0.5 (nested structures supported)
117    pub min_binary_version: u8,
118
119    /// Validate field ordering in binary format
120    ///
121    /// When `true`, binary decoder validates that fields are in ascending FID order.
122    pub validate_binary_ordering: bool,
123}
124
125impl StrictDeterministicConfig {
126    /// Creates config for strict deterministic mode (v0.5-D)
127    ///
128    /// Maximum strictness for LLM drift prevention:
129    /// - All validations enabled
130    /// - Type hints required
131    /// - Canonical forms enforced
132    /// - Minimum binary version 0x05
133    ///
134    /// # Example
135    ///
136    /// ```
137    /// use lnmp_core::profile::StrictDeterministicConfig;
138    ///
139    /// let config = StrictDeterministicConfig::strict();
140    /// assert!(config.reject_unsorted_fields);
141    /// assert!(config.require_type_hints);
142    /// assert!(config.canonical_boolean);
143    /// assert_eq!(config.min_binary_version, 0x05);
144    /// ```
145    pub fn strict() -> Self {
146        Self {
147            reject_unsorted_fields: true,
148            require_type_hints: true,
149            canonical_boolean: true,
150            canonical_string: true,
151            min_binary_version: 0x05, // v0.5+ supports nested
152            validate_binary_ordering: true,
153        }
154    }
155
156    /// Creates config for standard mode
157    ///
158    /// Balanced approach:
159    /// - Canonical output but accepts non-canonical input
160    /// - Type hints optional
161    /// - Binary v0.4 compatible
162    ///
163    /// # Example
164    ///
165    /// ```
166    /// use lnmp_core::profile::StrictDeterministicConfig;
167    ///
168    /// let config = StrictDeterministicConfig::standard();
169    /// assert!(!config.reject_unsorted_fields);
170    /// assert!(!config.require_type_hints);
171    /// assert!(config.canonical_boolean); // Still normalize output
172    /// ```
173    pub fn standard() -> Self {
174        Self {
175            reject_unsorted_fields: false,
176            require_type_hints: false,
177            canonical_boolean: true, // Normalize in output
178            canonical_string: false,
179            min_binary_version: 0x04,
180            validate_binary_ordering: false,
181        }
182    }
183
184    /// Creates config for loose mode
185    ///
186    /// Maximum backward compatibility:
187    /// - All validations disabled
188    /// - Accepts any input
189    ///
190    /// # Example
191    ///
192    /// ```
193    /// use lnmp_core::profile::StrictDeterministicConfig;
194    ///
195    /// let config = StrictDeterministicConfig::loose();
196    /// assert!(!config.reject_unsorted_fields);
197    /// assert!(!config.require_type_hints);
198    /// assert!(!config.canonical_boolean);
199    /// ```
200    pub fn loose() -> Self {
201        Self {
202            reject_unsorted_fields: false,
203            require_type_hints: false,
204            canonical_boolean: false,
205            canonical_string: false,
206            min_binary_version: 0x04,
207            validate_binary_ordering: false,
208        }
209    }
210
211    /// Returns whether this config enforces strict determinism
212    pub fn is_strict(&self) -> bool {
213        self.reject_unsorted_fields
214            && self.require_type_hints
215            && self.canonical_boolean
216            && self.validate_binary_ordering
217    }
218
219    /// Returns the recommended profile for this config
220    pub fn profile(&self) -> LnmpProfile {
221        if self.is_strict() {
222            LnmpProfile::Strict
223        } else if self.reject_unsorted_fields || self.require_type_hints {
224            LnmpProfile::Standard
225        } else {
226            LnmpProfile::Loose
227        }
228    }
229}
230
231impl Default for StrictDeterministicConfig {
232    fn default() -> Self {
233        Self::standard()
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_profile_as_str() {
243        assert_eq!(LnmpProfile::Loose.as_str(), "loose");
244        assert_eq!(LnmpProfile::Standard.as_str(), "standard");
245        assert_eq!(LnmpProfile::Strict.as_str(), "strict");
246    }
247
248    #[test]
249    fn test_profile_parse() {
250        assert_eq!(LnmpProfile::parse("loose"), Some(LnmpProfile::Loose));
251        assert_eq!(LnmpProfile::parse("standard"), Some(LnmpProfile::Standard));
252        assert_eq!(LnmpProfile::parse("strict"), Some(LnmpProfile::Strict));
253        assert_eq!(LnmpProfile::parse("STRICT"), Some(LnmpProfile::Strict)); // Case insensitive
254        assert_eq!(LnmpProfile::parse("invalid"), None);
255    }
256
257    #[test]
258    fn test_profile_default() {
259        assert_eq!(LnmpProfile::default(), LnmpProfile::Standard);
260    }
261
262    #[test]
263    fn test_profile_config() {
264        let loose_cfg = LnmpProfile::Loose.config();
265        assert!(!loose_cfg.reject_unsorted_fields);
266        assert!(!loose_cfg.require_type_hints);
267
268        let standard_cfg = LnmpProfile::Standard.config();
269        assert!(!standard_cfg.reject_unsorted_fields);
270        assert!(!standard_cfg.require_type_hints);
271        assert!(standard_cfg.canonical_boolean);
272
273        let strict_cfg = LnmpProfile::Strict.config();
274        assert!(strict_cfg.reject_unsorted_fields);
275        assert!(strict_cfg.require_type_hints);
276        assert!(strict_cfg.canonical_boolean);
277    }
278
279    #[test]
280    fn test_strict_config() {
281        let config = StrictDeterministicConfig::strict();
282        assert!(config.reject_unsorted_fields);
283        assert!(config.require_type_hints);
284        assert!(config.canonical_boolean);
285        assert!(config.canonical_string);
286        assert_eq!(config.min_binary_version, 0x05);
287        assert!(config.validate_binary_ordering);
288        assert!(config.is_strict());
289        assert_eq!(config.profile(), LnmpProfile::Strict);
290    }
291
292    #[test]
293    fn test_standard_config() {
294        let config = StrictDeterministicConfig::standard();
295        assert!(!config.reject_unsorted_fields);
296        assert!(!config.require_type_hints);
297        assert!(config.canonical_boolean);
298        assert!(!config.canonical_string);
299        assert_eq!(config.min_binary_version, 0x04);
300        assert!(!config.is_strict());
301    }
302
303    #[test]
304    fn test_loose_config() {
305        let config = StrictDeterministicConfig::loose();
306        assert!(!config.reject_unsorted_fields);
307        assert!(!config.require_type_hints);
308        assert!(!config.canonical_boolean);
309        assert!(!config.canonical_string);
310        assert!(!config.is_strict());
311        assert_eq!(config.profile(), LnmpProfile::Loose);
312    }
313
314    #[test]
315    fn test_config_default() {
316        let config = StrictDeterministicConfig::default();
317        assert_eq!(config, StrictDeterministicConfig::standard());
318    }
319
320    #[test]
321    fn test_is_strict() {
322        let mut config = StrictDeterministicConfig::strict();
323        assert!(config.is_strict());
324
325        config.require_type_hints = false;
326        assert!(!config.is_strict());
327    }
328}