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}