lnmp_codec/
encoder.rs

1//! Encoder for converting structured records into LNMP text format.
2
3use crate::config::EncoderConfig;
4use lnmp_core::checksum::SemanticChecksum;
5use lnmp_core::{LnmpField, LnmpRecord, LnmpValue, TypeHint};
6
7/// Encoder for LNMP text format
8pub struct Encoder {
9    use_semicolons: bool,
10    config: EncoderConfig,
11    normalizer: Option<crate::normalizer::ValueNormalizer>,
12}
13
14impl Encoder {
15    /// Creates a new encoder with default settings (canonical format)
16    pub fn new() -> Self {
17        Self {
18            // Canonical format uses newlines between top-level fields
19            use_semicolons: false,
20            config: EncoderConfig::default(),
21            normalizer: None,
22        }
23    }
24
25    /// Creates a new encoder with custom configuration
26    pub fn with_config(config: EncoderConfig) -> Self {
27        let normalizer = config.semantic_dictionary.as_ref().map(|dict| {
28            crate::normalizer::ValueNormalizer::new(crate::normalizer::NormalizationConfig {
29                semantic_dictionary: Some(dict.clone()),
30                ..crate::normalizer::NormalizationConfig::default()
31            })
32        });
33
34        Self {
35            // If canonical is enabled, prefer newlines; otherwise use semicolons for inline format
36            use_semicolons: !config.canonical,
37            config,
38            normalizer,
39        }
40    }
41
42    /// Creates a new encoder with specified format (deprecated - use new() for canonical format)
43    #[deprecated(
44        note = "Use new() for canonical format. Semicolons are not part of v0.2 canonical format."
45    )]
46    pub fn with_semicolons(use_semicolons: bool) -> Self {
47        Self {
48            use_semicolons,
49            config: EncoderConfig::default(),
50            normalizer: None,
51        }
52    }
53
54    /// Encodes a complete record into LNMP text format (canonical format with sorted fields)
55    pub fn encode(&self, record: &LnmpRecord) -> String {
56        // Canonicalize the record first (sorts fields and nested structures)
57        let canonical = canonicalize_record(record);
58
59        let fields: Vec<String> = canonical
60            .fields()
61            .iter()
62            .map(|field| {
63                let normalized_value = if let Some(norm) = &self.normalizer {
64                    norm.normalize_with_fid(Some(field.fid), &field.value)
65                } else {
66                    field.value.clone()
67                };
68                let normalized_field = LnmpField {
69                    fid: field.fid,
70                    value: normalized_value,
71                };
72                self.encode_field(&normalized_field)
73            })
74            .collect();
75
76        if self.use_semicolons {
77            fields.join(";")
78        } else {
79            fields.join("\n")
80        }
81    }
82
83    /// Encodes a single field (F<fid>=<value> or F<fid>:<type>=<value> or with checksum)
84    fn encode_field(&self, field: &LnmpField) -> String {
85        let type_hint = if self.config.include_type_hints {
86            Some(self.get_type_hint(&field.value))
87        } else {
88            None
89        };
90
91        let base = if let Some(hint) = type_hint {
92            format!(
93                "F{}:{}={}",
94                field.fid,
95                hint.as_str(),
96                self.encode_value(&field.value)
97            )
98        } else {
99            format!("F{}={}", field.fid, self.encode_value(&field.value))
100        };
101
102        // Append checksum if enabled
103        if self.config.enable_checksums {
104            let checksum = SemanticChecksum::compute(field.fid, type_hint, &field.value);
105            let checksum_str = SemanticChecksum::format(checksum);
106            format!("{}#{}", base, checksum_str)
107        } else {
108            base
109        }
110    }
111
112    /// Gets the type hint for a value
113    fn get_type_hint(&self, value: &LnmpValue) -> TypeHint {
114        match value {
115            LnmpValue::Int(_) => TypeHint::Int,
116            LnmpValue::Float(_) => TypeHint::Float,
117            LnmpValue::Bool(_) => TypeHint::Bool,
118            LnmpValue::String(_) => TypeHint::String,
119            LnmpValue::StringArray(_) => TypeHint::StringArray,
120            LnmpValue::NestedRecord(_) => TypeHint::Record,
121            LnmpValue::NestedArray(_) => TypeHint::RecordArray,
122        }
123    }
124
125    /// Encodes a value based on its type
126    fn encode_value(&self, value: &LnmpValue) -> String {
127        match value {
128            LnmpValue::Int(i) => i.to_string(),
129            LnmpValue::Float(f) => f.to_string(),
130            LnmpValue::Bool(b) => {
131                if *b {
132                    "1".to_string()
133                } else {
134                    "0".to_string()
135                }
136            }
137            LnmpValue::String(s) => self.encode_string(s),
138            LnmpValue::StringArray(arr) => {
139                // Canonical format: no spaces after commas
140                let items: Vec<String> = arr.iter().map(|s| self.encode_string(s)).collect();
141                format!("[{}]", items.join(","))
142            }
143            LnmpValue::NestedRecord(record) => self.encode_nested_record(record),
144            LnmpValue::NestedArray(records) => self.encode_nested_array(records),
145        }
146    }
147
148    /// Encodes a nested record {F<fid>=<value>;F<fid>=<value>}
149    fn encode_nested_record(&self, record: &LnmpRecord) -> String {
150        if record.fields().is_empty() {
151            return "{}".to_string();
152        }
153
154        let fields: Vec<String> = record
155            .fields()
156            .iter()
157            .map(|field| self.encode_field_without_checksum(field))
158            .collect();
159
160        // Nested records always use semicolons as separators
161        format!("{{{}}}", fields.join(";"))
162    }
163
164    /// Encodes a field without checksum (for use in nested structures)
165    fn encode_field_without_checksum(&self, field: &LnmpField) -> String {
166        if self.config.include_type_hints {
167            let type_hint = self.get_type_hint(&field.value);
168            format!(
169                "F{}:{}={}",
170                field.fid,
171                type_hint.as_str(),
172                self.encode_value(&field.value)
173            )
174        } else {
175            format!("F{}={}", field.fid, self.encode_value(&field.value))
176        }
177    }
178
179    /// Encodes a nested array [{...},{...}]
180    fn encode_nested_array(&self, records: &[LnmpRecord]) -> String {
181        if records.is_empty() {
182            return "[]".to_string();
183        }
184
185        let encoded_records: Vec<String> = records
186            .iter()
187            .map(|record| self.encode_nested_record(record))
188            .collect();
189
190        // Canonical format: no spaces after commas
191        format!("[{}]", encoded_records.join(","))
192    }
193
194    /// Encodes a string, adding quotes and escapes if needed
195    fn encode_string(&self, s: &str) -> String {
196        if self.needs_quoting(s) {
197            format!("\"{}\"", self.escape_string(s))
198        } else {
199            s.to_string()
200        }
201    }
202
203    /// Checks if a string needs quoting
204    fn needs_quoting(&self, s: &str) -> bool {
205        if s.is_empty() || looks_like_literal(s) {
206            return true;
207        }
208
209        for ch in s.chars() {
210            if !is_safe_unquoted_char(ch) {
211                return true;
212            }
213        }
214
215        false
216    }
217
218    /// Escapes special characters in a string
219    fn escape_string(&self, s: &str) -> String {
220        let mut result = String::new();
221        for ch in s.chars() {
222            match ch {
223                '"' => result.push_str("\\\""),
224                '\\' => result.push_str("\\\\"),
225                '\n' => result.push_str("\\n"),
226                '\r' => result.push_str("\\r"),
227                '\t' => result.push_str("\\t"),
228                _ => result.push(ch),
229            }
230        }
231        result
232    }
233}
234
235impl Default for Encoder {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241/// Canonicalizes a record by recursively sorting fields and normalizing nested structures
242///
243/// This function ensures deterministic encoding by:
244/// - Sorting fields by FID at every nesting level (depth-first)
245/// - Recursively canonicalizing nested records and arrays
246/// - Omitting redundant empty fields (empty strings, empty arrays, empty nested structures)
247/// - Maintaining structural integrity
248pub fn canonicalize_record(record: &LnmpRecord) -> LnmpRecord {
249    let mut canonical = LnmpRecord::new();
250
251    // Sort fields by FID (stable sort preserves insertion order for duplicates)
252    let sorted = record.sorted_fields();
253
254    for field in sorted {
255        let canonical_value = canonicalize_value(&field.value);
256
257        // Omit redundant empty fields
258        if !is_empty_value(&canonical_value) {
259            canonical.add_field(LnmpField {
260                fid: field.fid,
261                value: canonical_value,
262            });
263        }
264    }
265
266    canonical
267}
268
269/// Canonicalizes a value by recursively processing nested structures
270fn canonicalize_value(value: &LnmpValue) -> LnmpValue {
271    match value {
272        // Primitive values are already canonical
273        LnmpValue::Int(i) => LnmpValue::Int(*i),
274        LnmpValue::Float(f) => LnmpValue::Float(*f),
275        LnmpValue::Bool(b) => LnmpValue::Bool(*b),
276        LnmpValue::String(s) => LnmpValue::String(s.clone()),
277        LnmpValue::StringArray(arr) => LnmpValue::StringArray(arr.clone()),
278
279        // Recursively canonicalize nested record
280        LnmpValue::NestedRecord(nested) => {
281            let canonical_nested = canonicalize_record(nested);
282            LnmpValue::NestedRecord(Box::new(canonical_nested))
283        }
284
285        // Recursively canonicalize each record in nested array
286        LnmpValue::NestedArray(arr) => {
287            let canonical_arr: Vec<LnmpRecord> = arr.iter().map(canonicalize_record).collect();
288            LnmpValue::NestedArray(canonical_arr)
289        }
290    }
291}
292
293/// Checks if a value is considered "empty" and should be omitted during canonicalization
294///
295/// Empty values include:
296/// - Empty strings
297/// - Empty string arrays
298/// - Empty nested records (records with no fields)
299/// - Empty nested arrays (arrays with no elements)
300fn is_empty_value(value: &LnmpValue) -> bool {
301    match value {
302        LnmpValue::String(s) => s.is_empty(),
303        LnmpValue::StringArray(arr) => arr.is_empty(),
304        LnmpValue::NestedRecord(record) => record.fields().is_empty(),
305        LnmpValue::NestedArray(arr) => arr.is_empty(),
306        // Non-empty primitive values are never considered empty
307        LnmpValue::Int(_) | LnmpValue::Float(_) | LnmpValue::Bool(_) => false,
308    }
309}
310
311/// Validates that canonicalization is idempotent (round-trip stable)
312///
313/// Verifies that canonicalize(canonicalize(x)) == canonicalize(x)
314/// This ensures that the canonicalization process is stable and deterministic.
315///
316/// Returns true if the record is round-trip stable, false otherwise.
317pub fn validate_round_trip_stability(record: &LnmpRecord) -> bool {
318    let canonical_once = canonicalize_record(record);
319    let canonical_twice = canonicalize_record(&canonical_once);
320    canonical_once == canonical_twice
321}
322
323/// Checks if a character is safe for unquoted strings
324fn is_safe_unquoted_char(ch: char) -> bool {
325    ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '.'
326}
327
328fn looks_like_literal(s: &str) -> bool {
329    if s.trim().is_empty() {
330        return true;
331    }
332
333    let lower = s.to_ascii_lowercase();
334    matches!(lower.as_str(), "true" | "false" | "yes" | "no")
335        || s.parse::<i64>().is_ok()
336        || s.parse::<f64>().is_ok()
337}
338
339#[cfg(test)]
340#[allow(clippy::approx_constant)]
341mod tests {
342    use super::*;
343    use lnmp_core::LnmpField;
344
345    #[test]
346    fn test_encode_integer() {
347        let mut record = LnmpRecord::new();
348        record.add_field(LnmpField {
349            fid: 1,
350            value: LnmpValue::Int(42),
351        });
352
353        let encoder = Encoder::new();
354        let output = encoder.encode(&record);
355        assert_eq!(output, "F1=42");
356    }
357
358    #[test]
359    fn test_encode_negative_integer() {
360        let mut record = LnmpRecord::new();
361        record.add_field(LnmpField {
362            fid: 1,
363            value: LnmpValue::Int(-123),
364        });
365
366        let encoder = Encoder::new();
367        let output = encoder.encode(&record);
368        assert_eq!(output, "F1=-123");
369    }
370
371    #[test]
372    fn test_encode_float() {
373        let mut record = LnmpRecord::new();
374        record.add_field(LnmpField {
375            fid: 2,
376            value: LnmpValue::Float(3.14),
377        });
378
379        let encoder = Encoder::new();
380        let output = encoder.encode(&record);
381        assert_eq!(output, "F2=3.14");
382    }
383
384    #[test]
385    fn test_encode_bool_true() {
386        let mut record = LnmpRecord::new();
387        record.add_field(LnmpField {
388            fid: 3,
389            value: LnmpValue::Bool(true),
390        });
391
392        let encoder = Encoder::new();
393        let output = encoder.encode(&record);
394        assert_eq!(output, "F3=1");
395    }
396
397    #[test]
398    fn test_encode_bool_false() {
399        let mut record = LnmpRecord::new();
400        record.add_field(LnmpField {
401            fid: 3,
402            value: LnmpValue::Bool(false),
403        });
404
405        let encoder = Encoder::new();
406        let output = encoder.encode(&record);
407        assert_eq!(output, "F3=0");
408    }
409
410    #[test]
411    fn test_encode_unquoted_string() {
412        let mut record = LnmpRecord::new();
413        record.add_field(LnmpField {
414            fid: 4,
415            value: LnmpValue::String("test_value".to_string()),
416        });
417
418        let encoder = Encoder::new();
419        let output = encoder.encode(&record);
420        assert_eq!(output, "F4=test_value");
421    }
422
423    #[test]
424    fn test_encode_quoted_string() {
425        let mut record = LnmpRecord::new();
426        record.add_field(LnmpField {
427            fid: 4,
428            value: LnmpValue::String("hello world".to_string()),
429        });
430
431        let encoder = Encoder::new();
432        let output = encoder.encode(&record);
433        assert_eq!(output, r#"F4="hello world""#);
434    }
435
436    #[test]
437    fn test_encode_string_with_escapes() {
438        let mut record = LnmpRecord::new();
439        record.add_field(LnmpField {
440            fid: 4,
441            value: LnmpValue::String("hello \"world\"".to_string()),
442        });
443
444        let encoder = Encoder::new();
445        let output = encoder.encode(&record);
446        assert_eq!(output, r#"F4="hello \"world\"""#);
447    }
448
449    #[test]
450    fn test_encode_string_with_newline() {
451        let mut record = LnmpRecord::new();
452        record.add_field(LnmpField {
453            fid: 4,
454            value: LnmpValue::String("line1\nline2".to_string()),
455        });
456
457        let encoder = Encoder::new();
458        let output = encoder.encode(&record);
459        assert_eq!(output, r#"F4="line1\nline2""#);
460    }
461
462    #[test]
463    fn test_encode_string_with_backslash() {
464        let mut record = LnmpRecord::new();
465        record.add_field(LnmpField {
466            fid: 4,
467            value: LnmpValue::String("back\\slash".to_string()),
468        });
469
470        let encoder = Encoder::new();
471        let output = encoder.encode(&record);
472        assert_eq!(output, r#"F4="back\\slash""#);
473    }
474
475    #[test]
476    fn test_encode_string_array() {
477        let mut record = LnmpRecord::new();
478        record.add_field(LnmpField {
479            fid: 5,
480            value: LnmpValue::StringArray(vec![
481                "admin".to_string(),
482                "dev".to_string(),
483                "user".to_string(),
484            ]),
485        });
486
487        let encoder = Encoder::new();
488        let output = encoder.encode(&record);
489        assert_eq!(output, "F5=[admin,dev,user]");
490    }
491
492    #[test]
493    fn test_encode_string_array_with_quoted_strings() {
494        let mut record = LnmpRecord::new();
495        record.add_field(LnmpField {
496            fid: 5,
497            value: LnmpValue::StringArray(vec!["hello world".to_string(), "test".to_string()]),
498        });
499
500        let encoder = Encoder::new();
501        let output = encoder.encode(&record);
502        assert_eq!(output, r#"F5=["hello world",test]"#);
503    }
504
505    #[test]
506    fn test_encode_empty_string_array() {
507        // Empty string arrays are omitted during canonicalization (Requirement 9.3)
508        let mut record = LnmpRecord::new();
509        record.add_field(LnmpField {
510            fid: 5,
511            value: LnmpValue::StringArray(vec![]),
512        });
513
514        let encoder = Encoder::new();
515        let output = encoder.encode(&record);
516        assert_eq!(output, ""); // Empty field is omitted
517    }
518
519    #[test]
520    fn test_encode_multiline_format() {
521        let mut record = LnmpRecord::new();
522        record.add_field(LnmpField {
523            fid: 12,
524            value: LnmpValue::Int(14532),
525        });
526        record.add_field(LnmpField {
527            fid: 7,
528            value: LnmpValue::Bool(true),
529        });
530        record.add_field(LnmpField {
531            fid: 20,
532            value: LnmpValue::String("Halil".to_string()),
533        });
534
535        let encoder = Encoder::new();
536        let output = encoder.encode(&record);
537        // Canonical format: fields are sorted by FID
538        assert_eq!(output, "F7=1\nF12=14532\nF20=Halil");
539    }
540
541    #[test]
542    #[allow(deprecated)]
543    fn test_encode_inline_format() {
544        let mut record = LnmpRecord::new();
545        record.add_field(LnmpField {
546            fid: 12,
547            value: LnmpValue::Int(14532),
548        });
549        record.add_field(LnmpField {
550            fid: 7,
551            value: LnmpValue::Bool(true),
552        });
553        record.add_field(LnmpField {
554            fid: 23,
555            value: LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()]),
556        });
557
558        let encoder = Encoder::with_semicolons(true);
559        let output = encoder.encode(&record);
560        // Fields are sorted even with semicolons
561        assert_eq!(output, "F7=1;F12=14532;F23=[admin,dev]");
562    }
563
564    #[test]
565    fn test_encode_empty_record() {
566        let record = LnmpRecord::new();
567        let encoder = Encoder::new();
568        let output = encoder.encode(&record);
569        assert_eq!(output, "");
570    }
571
572    #[test]
573    #[allow(deprecated)]
574    fn test_encode_spec_example() {
575        let mut record = LnmpRecord::new();
576        record.add_field(LnmpField {
577            fid: 12,
578            value: LnmpValue::Int(14532),
579        });
580        record.add_field(LnmpField {
581            fid: 7,
582            value: LnmpValue::Bool(true),
583        });
584        record.add_field(LnmpField {
585            fid: 23,
586            value: LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()]),
587        });
588
589        let encoder = Encoder::with_semicolons(true);
590        let output = encoder.encode(&record);
591        // Fields are sorted by FID
592        assert_eq!(output, "F7=1;F12=14532;F23=[admin,dev]");
593    }
594
595    #[test]
596    fn test_needs_quoting() {
597        let encoder = Encoder::new();
598
599        assert!(!encoder.needs_quoting("simple"));
600        assert!(!encoder.needs_quoting("test_value"));
601        assert!(!encoder.needs_quoting("file-name"));
602        assert!(!encoder.needs_quoting("version1.2.3"));
603
604        assert!(encoder.needs_quoting("hello world"));
605        assert!(encoder.needs_quoting("test@example"));
606        assert!(encoder.needs_quoting(""));
607        assert!(encoder.needs_quoting("test;value"));
608    }
609
610    #[test]
611    fn test_escape_string() {
612        let encoder = Encoder::new();
613
614        assert_eq!(encoder.escape_string("simple"), "simple");
615        assert_eq!(
616            encoder.escape_string("hello \"world\""),
617            r#"hello \"world\""#
618        );
619        assert_eq!(encoder.escape_string("back\\slash"), r#"back\\slash"#);
620        assert_eq!(encoder.escape_string("line1\nline2"), r#"line1\nline2"#);
621        assert_eq!(encoder.escape_string("tab\there"), r#"tab\there"#);
622        assert_eq!(encoder.escape_string("return\rhere"), r#"return\rhere"#);
623    }
624
625    #[test]
626    #[allow(deprecated)]
627    fn test_round_trip() {
628        use crate::parser::Parser;
629
630        let input = r#"F12=14532;F7=1;F23=["admin","dev"]"#;
631        let mut parser = Parser::new(input).unwrap();
632        let record = parser.parse_record().unwrap();
633
634        let encoder = Encoder::with_semicolons(true);
635        let output = encoder.encode(&record);
636
637        // Parse again to verify
638        let mut parser2 = Parser::new(&output).unwrap();
639        let record2 = parser2.parse_record().unwrap();
640
641        assert_eq!(record.fields().len(), record2.fields().len());
642        assert_eq!(
643            record.get_field(12).unwrap().value,
644            record2.get_field(12).unwrap().value
645        );
646        assert_eq!(
647            record.get_field(7).unwrap().value,
648            record2.get_field(7).unwrap().value
649        );
650        assert_eq!(
651            record.get_field(23).unwrap().value,
652            record2.get_field(23).unwrap().value
653        );
654    }
655
656    #[test]
657    fn test_deterministic_field_sorting() {
658        // Create record with fields in random order
659        let mut record = LnmpRecord::new();
660        record.add_field(LnmpField {
661            fid: 100,
662            value: LnmpValue::Int(3),
663        });
664        record.add_field(LnmpField {
665            fid: 5,
666            value: LnmpValue::Int(1),
667        });
668        record.add_field(LnmpField {
669            fid: 50,
670            value: LnmpValue::Int(2),
671        });
672
673        let encoder = Encoder::new();
674        let output = encoder.encode(&record);
675
676        // Fields should be sorted by FID
677        assert_eq!(output, "F5=1\nF50=2\nF100=3");
678    }
679
680    #[test]
681    fn test_deterministic_sorting_with_duplicates() {
682        // Test that stable sort preserves insertion order for duplicate FIDs
683        let mut record = LnmpRecord::new();
684        record.add_field(LnmpField {
685            fid: 10,
686            value: LnmpValue::String("first".to_string()),
687        });
688        record.add_field(LnmpField {
689            fid: 5,
690            value: LnmpValue::Int(1),
691        });
692        record.add_field(LnmpField {
693            fid: 10,
694            value: LnmpValue::String("second".to_string()),
695        });
696
697        let encoder = Encoder::new();
698        let output = encoder.encode(&record);
699
700        // F5 first, then both F10s in insertion order
701        assert_eq!(output, "F5=1\nF10=first\nF10=second");
702    }
703
704    #[test]
705    fn test_canonical_whitespace_formatting() {
706        // Test that there's no whitespace around equals signs
707        let mut record = LnmpRecord::new();
708        record.add_field(LnmpField {
709            fid: 1,
710            value: LnmpValue::Int(42),
711        });
712
713        let encoder = Encoder::new();
714        let output = encoder.encode(&record);
715
716        // No spaces around =
717        assert_eq!(output, "F1=42");
718        assert!(!output.contains(" = "));
719        assert!(!output.contains("= "));
720        assert!(!output.contains(" ="));
721    }
722
723    #[test]
724    fn test_array_formatting_no_spaces() {
725        // Test that arrays have no spaces after commas
726        let mut record = LnmpRecord::new();
727        record.add_field(LnmpField {
728            fid: 1,
729            value: LnmpValue::StringArray(vec![
730                "one".to_string(),
731                "two".to_string(),
732                "three".to_string(),
733            ]),
734        });
735
736        let encoder = Encoder::new();
737        let output = encoder.encode(&record);
738
739        // No spaces after commas in array
740        assert_eq!(output, "F1=[one,two,three]");
741        assert!(!output.contains(", "));
742    }
743
744    #[test]
745    fn test_encoding_with_type_hints() {
746        use crate::config::EncoderConfig;
747
748        let mut record = LnmpRecord::new();
749        record.add_field(LnmpField {
750            fid: 12,
751            value: LnmpValue::Int(14532),
752        });
753        record.add_field(LnmpField {
754            fid: 7,
755            value: LnmpValue::Bool(true),
756        });
757        record.add_field(LnmpField {
758            fid: 5,
759            value: LnmpValue::Float(3.14),
760        });
761
762        let config = EncoderConfig::new().with_type_hints(true);
763        let encoder = Encoder::with_config(config);
764        let output = encoder.encode(&record);
765
766        // Fields sorted with type hints
767        assert_eq!(output, "F5:f=3.14\nF7:b=1\nF12:i=14532");
768    }
769
770    #[test]
771    fn test_encoding_without_type_hints() {
772        use crate::config::EncoderConfig;
773
774        let mut record = LnmpRecord::new();
775        record.add_field(LnmpField {
776            fid: 12,
777            value: LnmpValue::Int(14532),
778        });
779        record.add_field(LnmpField {
780            fid: 7,
781            value: LnmpValue::Bool(true),
782        });
783
784        let config = EncoderConfig::new();
785        let encoder = Encoder::with_config(config);
786        let output = encoder.encode(&record);
787
788        // Fields sorted without type hints
789        assert_eq!(output, "F7=1\nF12=14532");
790        assert!(!output.contains(':'));
791    }
792
793    #[test]
794    fn test_all_type_hints() {
795        use crate::config::EncoderConfig;
796
797        let mut record = LnmpRecord::new();
798        record.add_field(LnmpField {
799            fid: 1,
800            value: LnmpValue::Int(42),
801        });
802        record.add_field(LnmpField {
803            fid: 2,
804            value: LnmpValue::Float(3.14),
805        });
806        record.add_field(LnmpField {
807            fid: 3,
808            value: LnmpValue::Bool(true),
809        });
810        record.add_field(LnmpField {
811            fid: 4,
812            value: LnmpValue::String("test".to_string()),
813        });
814        record.add_field(LnmpField {
815            fid: 5,
816            value: LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()]),
817        });
818
819        let config = EncoderConfig::new().with_type_hints(true);
820
821        let encoder = Encoder::with_config(config);
822        let output = encoder.encode(&record);
823
824        assert_eq!(output, "F1:i=42\nF2:f=3.14\nF3:b=1\nF4:s=test\nF5:sa=[a,b]");
825    }
826
827    #[test]
828    fn test_multiple_encode_cycles_identical() {
829        // Test that encoding multiple times produces identical output
830        let mut record = LnmpRecord::new();
831        record.add_field(LnmpField {
832            fid: 23,
833            value: LnmpValue::Int(3),
834        });
835        record.add_field(LnmpField {
836            fid: 7,
837            value: LnmpValue::Int(2),
838        });
839        record.add_field(LnmpField {
840            fid: 12,
841            value: LnmpValue::Int(1),
842        });
843
844        let encoder = Encoder::new();
845        let output1 = encoder.encode(&record);
846        let output2 = encoder.encode(&record);
847        let output3 = encoder.encode(&record);
848
849        assert_eq!(output1, output2);
850        assert_eq!(output2, output3);
851    }
852
853    #[test]
854    fn test_canonicalize_record_basic() {
855        // Test basic field sorting
856        let mut record = LnmpRecord::new();
857        record.add_field(LnmpField {
858            fid: 100,
859            value: LnmpValue::Int(3),
860        });
861        record.add_field(LnmpField {
862            fid: 5,
863            value: LnmpValue::Int(1),
864        });
865        record.add_field(LnmpField {
866            fid: 50,
867            value: LnmpValue::Int(2),
868        });
869
870        let canonical = canonicalize_record(&record);
871        let fields = canonical.fields();
872
873        assert_eq!(fields.len(), 3);
874        assert_eq!(fields[0].fid, 5);
875        assert_eq!(fields[1].fid, 50);
876        assert_eq!(fields[2].fid, 100);
877    }
878
879    #[test]
880    fn test_canonicalize_record_with_nested_record() {
881        // Test nested record canonicalization
882        let mut inner_record = LnmpRecord::new();
883        inner_record.add_field(LnmpField {
884            fid: 12,
885            value: LnmpValue::Int(1),
886        });
887        inner_record.add_field(LnmpField {
888            fid: 7,
889            value: LnmpValue::Bool(true),
890        });
891
892        let mut outer_record = LnmpRecord::new();
893        outer_record.add_field(LnmpField {
894            fid: 50,
895            value: LnmpValue::NestedRecord(Box::new(inner_record)),
896        });
897        outer_record.add_field(LnmpField {
898            fid: 10,
899            value: LnmpValue::String("test".to_string()),
900        });
901
902        let canonical = canonicalize_record(&outer_record);
903        let fields = canonical.fields();
904
905        // Outer fields should be sorted
906        assert_eq!(fields.len(), 2);
907        assert_eq!(fields[0].fid, 10);
908        assert_eq!(fields[1].fid, 50);
909
910        // Inner fields should also be sorted
911        if let LnmpValue::NestedRecord(nested) = &fields[1].value {
912            let nested_fields = nested.fields();
913            assert_eq!(nested_fields.len(), 2);
914            assert_eq!(nested_fields[0].fid, 7);
915            assert_eq!(nested_fields[1].fid, 12);
916        } else {
917            panic!("Expected nested record");
918        }
919    }
920
921    #[test]
922    fn test_canonicalize_record_with_nested_array() {
923        // Test nested array canonicalization
924        let mut record1 = LnmpRecord::new();
925        record1.add_field(LnmpField {
926            fid: 20,
927            value: LnmpValue::Int(2),
928        });
929        record1.add_field(LnmpField {
930            fid: 10,
931            value: LnmpValue::Int(1),
932        });
933
934        let mut record2 = LnmpRecord::new();
935        record2.add_field(LnmpField {
936            fid: 30,
937            value: LnmpValue::Int(4),
938        });
939        record2.add_field(LnmpField {
940            fid: 15,
941            value: LnmpValue::Int(3),
942        });
943
944        let mut outer_record = LnmpRecord::new();
945        outer_record.add_field(LnmpField {
946            fid: 60,
947            value: LnmpValue::NestedArray(vec![record1, record2]),
948        });
949
950        let canonical = canonicalize_record(&outer_record);
951        let fields = canonical.fields();
952
953        assert_eq!(fields.len(), 1);
954        assert_eq!(fields[0].fid, 60);
955
956        // Each record in the array should have sorted fields
957        if let LnmpValue::NestedArray(arr) = &fields[0].value {
958            assert_eq!(arr.len(), 2);
959
960            let arr_fields1 = arr[0].fields();
961            assert_eq!(arr_fields1[0].fid, 10);
962            assert_eq!(arr_fields1[1].fid, 20);
963
964            let arr_fields2 = arr[1].fields();
965            assert_eq!(arr_fields2[0].fid, 15);
966            assert_eq!(arr_fields2[1].fid, 30);
967        } else {
968            panic!("Expected nested array");
969        }
970    }
971
972    #[test]
973    fn test_canonicalize_deeply_nested_structure() {
974        // Test 3-level deep nesting
975        let mut level3 = LnmpRecord::new();
976        level3.add_field(LnmpField {
977            fid: 3,
978            value: LnmpValue::Int(3),
979        });
980        level3.add_field(LnmpField {
981            fid: 1,
982            value: LnmpValue::Int(1),
983        });
984
985        let mut level2 = LnmpRecord::new();
986        level2.add_field(LnmpField {
987            fid: 20,
988            value: LnmpValue::NestedRecord(Box::new(level3)),
989        });
990        level2.add_field(LnmpField {
991            fid: 10,
992            value: LnmpValue::Int(2),
993        });
994
995        let mut level1 = LnmpRecord::new();
996        level1.add_field(LnmpField {
997            fid: 100,
998            value: LnmpValue::NestedRecord(Box::new(level2)),
999        });
1000        level1.add_field(LnmpField {
1001            fid: 50,
1002            value: LnmpValue::String("test".to_string()),
1003        });
1004
1005        let canonical = canonicalize_record(&level1);
1006        let fields = canonical.fields();
1007
1008        // Level 1 should be sorted
1009        assert_eq!(fields[0].fid, 50);
1010        assert_eq!(fields[1].fid, 100);
1011
1012        // Level 2 should be sorted
1013        if let LnmpValue::NestedRecord(level2_rec) = &fields[1].value {
1014            let level2_fields = level2_rec.fields();
1015            assert_eq!(level2_fields[0].fid, 10);
1016            assert_eq!(level2_fields[1].fid, 20);
1017
1018            // Level 3 should be sorted
1019            if let LnmpValue::NestedRecord(level3_rec) = &level2_fields[1].value {
1020                let level3_fields = level3_rec.fields();
1021                assert_eq!(level3_fields[0].fid, 1);
1022                assert_eq!(level3_fields[1].fid, 3);
1023            } else {
1024                panic!("Expected level 3 nested record");
1025            }
1026        } else {
1027            panic!("Expected level 2 nested record");
1028        }
1029    }
1030
1031    #[test]
1032    fn test_canonicalize_empty_record() {
1033        let record = LnmpRecord::new();
1034        let canonical = canonicalize_record(&record);
1035        assert_eq!(canonical.fields().len(), 0);
1036    }
1037
1038    #[test]
1039    fn test_canonicalize_empty_nested_structures() {
1040        // Empty nested structures should be omitted during canonicalization (Requirement 9.3)
1041        let mut record = LnmpRecord::new();
1042        record.add_field(LnmpField {
1043            fid: 50,
1044            value: LnmpValue::NestedRecord(Box::new(LnmpRecord::new())),
1045        });
1046        record.add_field(LnmpField {
1047            fid: 60,
1048            value: LnmpValue::NestedArray(vec![]),
1049        });
1050
1051        let canonical = canonicalize_record(&record);
1052        let fields = canonical.fields();
1053
1054        // Empty nested structures should be omitted
1055        assert_eq!(fields.len(), 0);
1056    }
1057
1058    #[test]
1059    fn test_canonicalize_preserves_values() {
1060        // Test that canonicalization doesn't modify values
1061        let mut record = LnmpRecord::new();
1062        record.add_field(LnmpField {
1063            fid: 1,
1064            value: LnmpValue::Int(42),
1065        });
1066        record.add_field(LnmpField {
1067            fid: 2,
1068            value: LnmpValue::Float(3.14159),
1069        });
1070        record.add_field(LnmpField {
1071            fid: 3,
1072            value: LnmpValue::Bool(true),
1073        });
1074        record.add_field(LnmpField {
1075            fid: 4,
1076            value: LnmpValue::String("test value".to_string()),
1077        });
1078        record.add_field(LnmpField {
1079            fid: 5,
1080            value: LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()]),
1081        });
1082
1083        let canonical = canonicalize_record(&record);
1084
1085        assert_eq!(canonical.get_field(1).unwrap().value, LnmpValue::Int(42));
1086        assert_eq!(
1087            canonical.get_field(2).unwrap().value,
1088            LnmpValue::Float(3.14159)
1089        );
1090        assert_eq!(canonical.get_field(3).unwrap().value, LnmpValue::Bool(true));
1091        assert_eq!(
1092            canonical.get_field(4).unwrap().value,
1093            LnmpValue::String("test value".to_string())
1094        );
1095        assert_eq!(
1096            canonical.get_field(5).unwrap().value,
1097            LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()])
1098        );
1099    }
1100
1101    #[test]
1102    fn test_canonicalize_idempotent() {
1103        // Test that canonicalizing twice produces the same result
1104        let mut record = LnmpRecord::new();
1105        record.add_field(LnmpField {
1106            fid: 100,
1107            value: LnmpValue::Int(3),
1108        });
1109        record.add_field(LnmpField {
1110            fid: 5,
1111            value: LnmpValue::Int(1),
1112        });
1113        record.add_field(LnmpField {
1114            fid: 50,
1115            value: LnmpValue::Int(2),
1116        });
1117
1118        let canonical1 = canonicalize_record(&record);
1119        let canonical2 = canonicalize_record(&canonical1);
1120
1121        assert_eq!(canonical1, canonical2);
1122    }
1123
1124    #[test]
1125    fn test_canonicalize_mixed_nested_structures() {
1126        // Test record with both nested records and nested arrays
1127        let mut inner_record = LnmpRecord::new();
1128        inner_record.add_field(LnmpField {
1129            fid: 15,
1130            value: LnmpValue::Int(5),
1131        });
1132        inner_record.add_field(LnmpField {
1133            fid: 5,
1134            value: LnmpValue::Int(3),
1135        });
1136
1137        let mut array_record = LnmpRecord::new();
1138        array_record.add_field(LnmpField {
1139            fid: 25,
1140            value: LnmpValue::Int(7),
1141        });
1142        array_record.add_field(LnmpField {
1143            fid: 20,
1144            value: LnmpValue::Int(6),
1145        });
1146
1147        let mut outer_record = LnmpRecord::new();
1148        outer_record.add_field(LnmpField {
1149            fid: 100,
1150            value: LnmpValue::NestedRecord(Box::new(inner_record)),
1151        });
1152        outer_record.add_field(LnmpField {
1153            fid: 50,
1154            value: LnmpValue::NestedArray(vec![array_record]),
1155        });
1156        outer_record.add_field(LnmpField {
1157            fid: 10,
1158            value: LnmpValue::String("test".to_string()),
1159        });
1160
1161        let canonical = canonicalize_record(&outer_record);
1162        let fields = canonical.fields();
1163
1164        // Outer fields sorted
1165        assert_eq!(fields[0].fid, 10);
1166        assert_eq!(fields[1].fid, 50);
1167        assert_eq!(fields[2].fid, 100);
1168
1169        // Nested array fields sorted
1170        if let LnmpValue::NestedArray(arr) = &fields[1].value {
1171            let arr_fields = arr[0].fields();
1172            assert_eq!(arr_fields[0].fid, 20);
1173            assert_eq!(arr_fields[1].fid, 25);
1174        } else {
1175            panic!("Expected nested array");
1176        }
1177
1178        // Nested record fields sorted
1179        if let LnmpValue::NestedRecord(nested) = &fields[2].value {
1180            let nested_fields = nested.fields();
1181            assert_eq!(nested_fields[0].fid, 5);
1182            assert_eq!(nested_fields[1].fid, 15);
1183        } else {
1184            panic!("Expected nested record");
1185        }
1186    }
1187
1188    // Nested structure encoding tests
1189    #[test]
1190    fn test_encode_simple_nested_record() {
1191        let mut inner = LnmpRecord::new();
1192        inner.add_field(LnmpField {
1193            fid: 12,
1194            value: LnmpValue::Int(1),
1195        });
1196        inner.add_field(LnmpField {
1197            fid: 7,
1198            value: LnmpValue::Bool(true),
1199        });
1200
1201        let mut record = LnmpRecord::new();
1202        record.add_field(LnmpField {
1203            fid: 50,
1204            value: LnmpValue::NestedRecord(Box::new(inner)),
1205        });
1206
1207        let encoder = Encoder::new();
1208        let output = encoder.encode(&record);
1209
1210        // Fields should be sorted within nested record
1211        assert_eq!(output, "F50={F7=1;F12=1}");
1212    }
1213
1214    #[test]
1215    fn test_encode_empty_nested_record() {
1216        // Empty nested records are omitted during canonicalization (Requirement 9.3)
1217        let mut record = LnmpRecord::new();
1218        record.add_field(LnmpField {
1219            fid: 50,
1220            value: LnmpValue::NestedRecord(Box::new(LnmpRecord::new())),
1221        });
1222
1223        let encoder = Encoder::new();
1224        let output = encoder.encode(&record);
1225        assert_eq!(output, ""); // Empty field is omitted
1226    }
1227
1228    #[test]
1229    fn test_encode_nested_record_with_various_types() {
1230        let mut inner = LnmpRecord::new();
1231        inner.add_field(LnmpField {
1232            fid: 12,
1233            value: LnmpValue::Int(14532),
1234        });
1235        inner.add_field(LnmpField {
1236            fid: 7,
1237            value: LnmpValue::Bool(true),
1238        });
1239        inner.add_field(LnmpField {
1240            fid: 23,
1241            value: LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()]),
1242        });
1243
1244        let mut record = LnmpRecord::new();
1245        record.add_field(LnmpField {
1246            fid: 50,
1247            value: LnmpValue::NestedRecord(Box::new(inner)),
1248        });
1249
1250        let encoder = Encoder::new();
1251        let output = encoder.encode(&record);
1252
1253        // Fields sorted: F7, F12, F23
1254        assert_eq!(output, "F50={F7=1;F12=14532;F23=[admin,dev]}");
1255    }
1256
1257    #[test]
1258    fn test_encode_deeply_nested_record() {
1259        let mut level3 = LnmpRecord::new();
1260        level3.add_field(LnmpField {
1261            fid: 1,
1262            value: LnmpValue::String("deep".to_string()),
1263        });
1264
1265        let mut level2 = LnmpRecord::new();
1266        level2.add_field(LnmpField {
1267            fid: 2,
1268            value: LnmpValue::NestedRecord(Box::new(level3)),
1269        });
1270
1271        let mut level1 = LnmpRecord::new();
1272        level1.add_field(LnmpField {
1273            fid: 3,
1274            value: LnmpValue::NestedRecord(Box::new(level2)),
1275        });
1276
1277        let encoder = Encoder::new();
1278        let output = encoder.encode(&level1);
1279        assert_eq!(output, "F3={F2={F1=deep}}");
1280    }
1281
1282    #[test]
1283    fn test_encode_simple_nested_array() {
1284        let mut rec1 = LnmpRecord::new();
1285        rec1.add_field(LnmpField {
1286            fid: 12,
1287            value: LnmpValue::Int(1),
1288        });
1289
1290        let mut rec2 = LnmpRecord::new();
1291        rec2.add_field(LnmpField {
1292            fid: 12,
1293            value: LnmpValue::Int(2),
1294        });
1295
1296        let mut rec3 = LnmpRecord::new();
1297        rec3.add_field(LnmpField {
1298            fid: 12,
1299            value: LnmpValue::Int(3),
1300        });
1301
1302        let mut record = LnmpRecord::new();
1303        record.add_field(LnmpField {
1304            fid: 60,
1305            value: LnmpValue::NestedArray(vec![rec1, rec2, rec3]),
1306        });
1307
1308        let encoder = Encoder::new();
1309        let output = encoder.encode(&record);
1310        assert_eq!(output, "F60=[{F12=1},{F12=2},{F12=3}]");
1311    }
1312
1313    #[test]
1314    fn test_encode_empty_nested_array() {
1315        // Empty nested arrays are omitted during canonicalization (Requirement 9.3)
1316        let mut record = LnmpRecord::new();
1317        record.add_field(LnmpField {
1318            fid: 60,
1319            value: LnmpValue::NestedArray(vec![]),
1320        });
1321
1322        let encoder = Encoder::new();
1323        let output = encoder.encode(&record);
1324        assert_eq!(output, ""); // Empty field is omitted
1325    }
1326
1327    #[test]
1328    fn test_encode_nested_array_with_multiple_fields() {
1329        let mut rec1 = LnmpRecord::new();
1330        rec1.add_field(LnmpField {
1331            fid: 1,
1332            value: LnmpValue::String("alice".to_string()),
1333        });
1334        rec1.add_field(LnmpField {
1335            fid: 2,
1336            value: LnmpValue::String("admin".to_string()),
1337        });
1338
1339        let mut rec2 = LnmpRecord::new();
1340        rec2.add_field(LnmpField {
1341            fid: 1,
1342            value: LnmpValue::String("bob".to_string()),
1343        });
1344        rec2.add_field(LnmpField {
1345            fid: 2,
1346            value: LnmpValue::String("user".to_string()),
1347        });
1348
1349        let mut record = LnmpRecord::new();
1350        record.add_field(LnmpField {
1351            fid: 200,
1352            value: LnmpValue::NestedArray(vec![rec1, rec2]),
1353        });
1354
1355        let encoder = Encoder::new();
1356        let output = encoder.encode(&record);
1357        assert_eq!(output, "F200=[{F1=alice;F2=admin},{F1=bob;F2=user}]");
1358    }
1359
1360    #[test]
1361    fn test_encode_nested_array_preserves_order() {
1362        // Requirement 5.3: Element order must be preserved
1363        let mut rec1 = LnmpRecord::new();
1364        rec1.add_field(LnmpField {
1365            fid: 1,
1366            value: LnmpValue::String("first".to_string()),
1367        });
1368
1369        let mut rec2 = LnmpRecord::new();
1370        rec2.add_field(LnmpField {
1371            fid: 1,
1372            value: LnmpValue::String("second".to_string()),
1373        });
1374
1375        let mut rec3 = LnmpRecord::new();
1376        rec3.add_field(LnmpField {
1377            fid: 1,
1378            value: LnmpValue::String("third".to_string()),
1379        });
1380
1381        let mut record = LnmpRecord::new();
1382        record.add_field(LnmpField {
1383            fid: 60,
1384            value: LnmpValue::NestedArray(vec![rec1, rec2, rec3]),
1385        });
1386
1387        let encoder = Encoder::new();
1388        let output = encoder.encode(&record);
1389        assert_eq!(output, "F60=[{F1=first},{F1=second},{F1=third}]");
1390    }
1391
1392    #[test]
1393    fn test_encode_nested_array_fields_sorted() {
1394        // Fields within each nested record should be sorted
1395        let mut rec1 = LnmpRecord::new();
1396        rec1.add_field(LnmpField {
1397            fid: 20,
1398            value: LnmpValue::Int(2),
1399        });
1400        rec1.add_field(LnmpField {
1401            fid: 10,
1402            value: LnmpValue::Int(1),
1403        });
1404
1405        let mut record = LnmpRecord::new();
1406        record.add_field(LnmpField {
1407            fid: 60,
1408            value: LnmpValue::NestedArray(vec![rec1]),
1409        });
1410
1411        let encoder = Encoder::new();
1412        let output = encoder.encode(&record);
1413        // Fields should be sorted: F10, F20
1414        assert_eq!(output, "F60=[{F10=1;F20=2}]");
1415    }
1416
1417    #[test]
1418    fn test_encode_mixed_nested_structures() {
1419        // Test record with both nested record and nested array
1420        let mut inner_record = LnmpRecord::new();
1421        inner_record.add_field(LnmpField {
1422            fid: 1,
1423            value: LnmpValue::String("nested".to_string()),
1424        });
1425
1426        let mut array_rec = LnmpRecord::new();
1427        array_rec.add_field(LnmpField {
1428            fid: 2,
1429            value: LnmpValue::Int(42),
1430        });
1431
1432        let mut record = LnmpRecord::new();
1433        record.add_field(LnmpField {
1434            fid: 50,
1435            value: LnmpValue::NestedRecord(Box::new(inner_record)),
1436        });
1437        record.add_field(LnmpField {
1438            fid: 60,
1439            value: LnmpValue::NestedArray(vec![array_rec]),
1440        });
1441        record.add_field(LnmpField {
1442            fid: 10,
1443            value: LnmpValue::String("top".to_string()),
1444        });
1445
1446        let encoder = Encoder::new();
1447        let output = encoder.encode(&record);
1448        // Fields sorted: F10, F50, F60
1449        assert_eq!(output, "F10=top\nF50={F1=nested}\nF60=[{F2=42}]");
1450    }
1451
1452    #[test]
1453    fn test_encode_nested_record_with_type_hints() {
1454        use crate::config::EncoderConfig;
1455
1456        let mut inner = LnmpRecord::new();
1457        inner.add_field(LnmpField {
1458            fid: 12,
1459            value: LnmpValue::Int(14532),
1460        });
1461        inner.add_field(LnmpField {
1462            fid: 7,
1463            value: LnmpValue::Bool(true),
1464        });
1465
1466        let mut record = LnmpRecord::new();
1467        record.add_field(LnmpField {
1468            fid: 50,
1469            value: LnmpValue::NestedRecord(Box::new(inner)),
1470        });
1471
1472        let config = EncoderConfig::new().with_type_hints(true);
1473
1474        let encoder = Encoder::with_config(config);
1475        let output = encoder.encode(&record);
1476
1477        // Type hints should be included
1478        assert_eq!(output, "F50:r={F7:b=1;F12:i=14532}");
1479    }
1480
1481    #[test]
1482    fn test_encode_nested_array_with_type_hints() {
1483        use crate::config::EncoderConfig;
1484
1485        let mut rec1 = LnmpRecord::new();
1486        rec1.add_field(LnmpField {
1487            fid: 12,
1488            value: LnmpValue::Int(1),
1489        });
1490
1491        let mut rec2 = LnmpRecord::new();
1492        rec2.add_field(LnmpField {
1493            fid: 12,
1494            value: LnmpValue::Int(2),
1495        });
1496
1497        let mut record = LnmpRecord::new();
1498        record.add_field(LnmpField {
1499            fid: 60,
1500            value: LnmpValue::NestedArray(vec![rec1, rec2]),
1501        });
1502
1503        let config = EncoderConfig::new().with_type_hints(true);
1504
1505        let encoder = Encoder::with_config(config);
1506        let output = encoder.encode(&record);
1507
1508        // Type hints should be included
1509        assert_eq!(output, "F60:ra=[{F12:i=1},{F12:i=2}]");
1510    }
1511
1512    #[test]
1513    fn test_round_trip_nested_record() {
1514        use crate::parser::Parser;
1515
1516        let mut inner = LnmpRecord::new();
1517        inner.add_field(LnmpField {
1518            fid: 12,
1519            value: LnmpValue::Int(14532),
1520        });
1521        inner.add_field(LnmpField {
1522            fid: 7,
1523            value: LnmpValue::Bool(true),
1524        });
1525
1526        let mut record = LnmpRecord::new();
1527        record.add_field(LnmpField {
1528            fid: 50,
1529            value: LnmpValue::NestedRecord(Box::new(inner)),
1530        });
1531
1532        let encoder = Encoder::new();
1533        let output = encoder.encode(&record);
1534
1535        let mut parser = Parser::new(&output).unwrap();
1536        let parsed = parser.parse_record().unwrap();
1537
1538        // Compare canonical versions since encoder sorts fields
1539        let canonical_original = canonicalize_record(&record);
1540        assert_eq!(canonical_original, parsed);
1541    }
1542
1543    #[test]
1544    fn test_round_trip_nested_array() {
1545        use crate::parser::Parser;
1546
1547        let mut rec1 = LnmpRecord::new();
1548        rec1.add_field(LnmpField {
1549            fid: 1,
1550            value: LnmpValue::String("alice".to_string()),
1551        });
1552
1553        let mut rec2 = LnmpRecord::new();
1554        rec2.add_field(LnmpField {
1555            fid: 1,
1556            value: LnmpValue::String("bob".to_string()),
1557        });
1558
1559        let mut record = LnmpRecord::new();
1560        record.add_field(LnmpField {
1561            fid: 200,
1562            value: LnmpValue::NestedArray(vec![rec1, rec2]),
1563        });
1564
1565        let encoder = Encoder::new();
1566        let output = encoder.encode(&record);
1567
1568        let mut parser = Parser::new(&output).unwrap();
1569        let parsed = parser.parse_record().unwrap();
1570
1571        assert_eq!(record, parsed);
1572    }
1573
1574    #[test]
1575    fn test_round_trip_complex_nested_structure() {
1576        use crate::parser::Parser;
1577
1578        // Create a complex structure with multiple levels of nesting
1579        let mut level3 = LnmpRecord::new();
1580        level3.add_field(LnmpField {
1581            fid: 1,
1582            value: LnmpValue::Int(42),
1583        });
1584
1585        let mut level2 = LnmpRecord::new();
1586        level2.add_field(LnmpField {
1587            fid: 10,
1588            value: LnmpValue::String("nested".to_string()),
1589        });
1590        level2.add_field(LnmpField {
1591            fid: 11,
1592            value: LnmpValue::NestedRecord(Box::new(level3)),
1593        });
1594
1595        let mut array_rec = LnmpRecord::new();
1596        array_rec.add_field(LnmpField {
1597            fid: 5,
1598            value: LnmpValue::Bool(true),
1599        });
1600
1601        let mut record = LnmpRecord::new();
1602        record.add_field(LnmpField {
1603            fid: 100,
1604            value: LnmpValue::NestedRecord(Box::new(level2)),
1605        });
1606        record.add_field(LnmpField {
1607            fid: 200,
1608            value: LnmpValue::NestedArray(vec![array_rec]),
1609        });
1610
1611        let encoder = Encoder::new();
1612        let output = encoder.encode(&record);
1613
1614        let mut parser = Parser::new(&output).unwrap();
1615        let parsed = parser.parse_record().unwrap();
1616
1617        assert_eq!(record, parsed);
1618    }
1619
1620    // Checksum encoding tests
1621    #[test]
1622    fn test_encode_with_checksum() {
1623        use crate::config::EncoderConfig;
1624
1625        let mut record = LnmpRecord::new();
1626        record.add_field(LnmpField {
1627            fid: 12,
1628            value: LnmpValue::Int(14532),
1629        });
1630
1631        let config = EncoderConfig::new().with_checksums(true);
1632
1633        let encoder = Encoder::with_config(config);
1634        let output = encoder.encode(&record);
1635
1636        // Should include checksum
1637        assert!(output.contains('#'));
1638        assert!(output.starts_with("F12=14532#"));
1639
1640        // Checksum should be 8 hex characters
1641        let parts: Vec<&str> = output.split('#').collect();
1642        assert_eq!(parts.len(), 2);
1643        assert_eq!(parts[1].len(), 8);
1644    }
1645
1646    #[test]
1647    fn test_encode_with_checksum_and_type_hints() {
1648        use crate::config::EncoderConfig;
1649
1650        let mut record = LnmpRecord::new();
1651        record.add_field(LnmpField {
1652            fid: 12,
1653            value: LnmpValue::Int(14532),
1654        });
1655
1656        let config = EncoderConfig::new()
1657            .with_type_hints(true)
1658            .with_checksums(true);
1659
1660        let encoder = Encoder::with_config(config);
1661        let output = encoder.encode(&record);
1662
1663        // Should include both type hint and checksum
1664        assert!(output.contains(':'));
1665        assert!(output.contains('#'));
1666        assert!(output.starts_with("F12:i=14532#"));
1667    }
1668
1669    #[test]
1670    fn test_encode_without_checksum() {
1671        use crate::config::EncoderConfig;
1672
1673        let mut record = LnmpRecord::new();
1674        record.add_field(LnmpField {
1675            fid: 12,
1676            value: LnmpValue::Int(14532),
1677        });
1678
1679        let config = EncoderConfig::new();
1680
1681        let encoder = Encoder::with_config(config);
1682        let output = encoder.encode(&record);
1683
1684        // Should not include checksum
1685        assert!(!output.contains('#'));
1686        assert_eq!(output, "F12=14532");
1687    }
1688
1689    #[test]
1690    fn test_encode_multiple_fields_with_checksums() {
1691        use crate::config::EncoderConfig;
1692
1693        let mut record = LnmpRecord::new();
1694        record.add_field(LnmpField {
1695            fid: 12,
1696            value: LnmpValue::Int(14532),
1697        });
1698        record.add_field(LnmpField {
1699            fid: 7,
1700            value: LnmpValue::Bool(true),
1701        });
1702
1703        let config = EncoderConfig::new()
1704            .with_type_hints(true)
1705            .with_checksums(true);
1706
1707        let encoder = Encoder::with_config(config);
1708        let output = encoder.encode(&record);
1709
1710        // Each field should have a checksum
1711        let lines: Vec<&str> = output.lines().collect();
1712        assert_eq!(lines.len(), 2);
1713        assert!(lines[0].contains('#'));
1714        assert!(lines[1].contains('#'));
1715    }
1716
1717    #[test]
1718    fn test_checksum_deterministic() {
1719        use crate::config::EncoderConfig;
1720
1721        let mut record = LnmpRecord::new();
1722        record.add_field(LnmpField {
1723            fid: 12,
1724            value: LnmpValue::Int(14532),
1725        });
1726
1727        let config = EncoderConfig::new().with_type_hints(true);
1728
1729        let encoder = Encoder::with_config(config);
1730
1731        // Encode multiple times
1732        let output1 = encoder.encode(&record);
1733        let output2 = encoder.encode(&record);
1734        let output3 = encoder.encode(&record);
1735
1736        // All outputs should be identical
1737        assert_eq!(output1, output2);
1738        assert_eq!(output2, output3);
1739    }
1740
1741    #[test]
1742    fn test_encode_nested_record_with_checksum() {
1743        use crate::config::EncoderConfig;
1744
1745        let mut inner = LnmpRecord::new();
1746        inner.add_field(LnmpField {
1747            fid: 12,
1748            value: LnmpValue::Int(1),
1749        });
1750
1751        let mut record = LnmpRecord::new();
1752        record.add_field(LnmpField {
1753            fid: 50,
1754            value: LnmpValue::NestedRecord(Box::new(inner)),
1755        });
1756
1757        let config = EncoderConfig::new()
1758            .with_type_hints(true)
1759            .with_checksums(true);
1760
1761        let encoder = Encoder::with_config(config);
1762        let output = encoder.encode(&record);
1763
1764        // Should include checksum for the nested record field
1765        assert!(output.contains('#'));
1766        // The nested record value includes type hints for inner fields
1767        assert!(output.starts_with("F50:r={F12:i=1}#"));
1768
1769        // Verify checksum is 8 hex characters
1770        let parts: Vec<&str> = output.split('#').collect();
1771        assert_eq!(parts.len(), 2);
1772        assert_eq!(parts[1].len(), 8);
1773    }
1774
1775    #[test]
1776    fn test_encode_nested_array_with_checksum() {
1777        use crate::config::EncoderConfig;
1778
1779        let mut rec1 = LnmpRecord::new();
1780        rec1.add_field(LnmpField {
1781            fid: 12,
1782            value: LnmpValue::Int(1),
1783        });
1784
1785        let mut record = LnmpRecord::new();
1786        record.add_field(LnmpField {
1787            fid: 60,
1788            value: LnmpValue::NestedArray(vec![rec1]),
1789        });
1790
1791        let config = EncoderConfig::new()
1792            .with_type_hints(true)
1793            .with_checksums(true);
1794
1795        let encoder = Encoder::with_config(config);
1796        let output = encoder.encode(&record);
1797
1798        // Should include checksum for the nested array field
1799        assert!(output.contains('#'));
1800        assert!(output.starts_with("F60:ra=[{F12:i=1}]#"));
1801
1802        // Verify checksum is 8 hex characters
1803        let parts: Vec<&str> = output.split('#').collect();
1804        assert_eq!(parts.len(), 2);
1805        assert_eq!(parts[1].len(), 8);
1806    }
1807
1808    #[test]
1809    fn test_checksum_different_for_different_values() {
1810        use crate::config::EncoderConfig;
1811
1812        let mut record1 = LnmpRecord::new();
1813        record1.add_field(LnmpField {
1814            fid: 12,
1815            value: LnmpValue::Int(14532),
1816        });
1817
1818        let mut record2 = LnmpRecord::new();
1819        record2.add_field(LnmpField {
1820            fid: 12,
1821            value: LnmpValue::Int(14533),
1822        });
1823
1824        let config = EncoderConfig::new()
1825            .with_type_hints(true)
1826            .with_checksums(true);
1827
1828        let encoder = Encoder::with_config(config);
1829
1830        let output1 = encoder.encode(&record1);
1831        let output2 = encoder.encode(&record2);
1832
1833        // Checksums should be different
1834        assert_ne!(output1, output2);
1835
1836        // Extract checksums
1837        let checksum1 = output1.split('#').nth(1).unwrap();
1838        let checksum2 = output2.split('#').nth(1).unwrap();
1839        assert_ne!(checksum1, checksum2);
1840    }
1841
1842    // Canonicalization tests for v0.5 requirements
1843
1844    #[test]
1845    fn test_canonicalize_field_ordering_multiple_levels() {
1846        // Test field ordering at multiple nesting levels (Requirement 9.1)
1847        let mut level3 = LnmpRecord::new();
1848        level3.add_field(LnmpField {
1849            fid: 30,
1850            value: LnmpValue::Int(3),
1851        });
1852        level3.add_field(LnmpField {
1853            fid: 10,
1854            value: LnmpValue::Int(1),
1855        });
1856        level3.add_field(LnmpField {
1857            fid: 20,
1858            value: LnmpValue::Int(2),
1859        });
1860
1861        let mut level2 = LnmpRecord::new();
1862        level2.add_field(LnmpField {
1863            fid: 300,
1864            value: LnmpValue::NestedRecord(Box::new(level3)),
1865        });
1866        level2.add_field(LnmpField {
1867            fid: 100,
1868            value: LnmpValue::Int(10),
1869        });
1870        level2.add_field(LnmpField {
1871            fid: 200,
1872            value: LnmpValue::Int(20),
1873        });
1874
1875        let mut level1 = LnmpRecord::new();
1876        level1.add_field(LnmpField {
1877            fid: 3000,
1878            value: LnmpValue::NestedRecord(Box::new(level2)),
1879        });
1880        level1.add_field(LnmpField {
1881            fid: 1000,
1882            value: LnmpValue::Int(100),
1883        });
1884        level1.add_field(LnmpField {
1885            fid: 2000,
1886            value: LnmpValue::Int(200),
1887        });
1888
1889        let canonical = canonicalize_record(&level1);
1890        let fields = canonical.fields();
1891
1892        // Level 1 fields should be sorted
1893        assert_eq!(fields[0].fid, 1000);
1894        assert_eq!(fields[1].fid, 2000);
1895        assert_eq!(fields[2].fid, 3000);
1896
1897        // Level 2 fields should be sorted
1898        if let LnmpValue::NestedRecord(level2_rec) = &fields[2].value {
1899            let level2_fields = level2_rec.fields();
1900            assert_eq!(level2_fields[0].fid, 100);
1901            assert_eq!(level2_fields[1].fid, 200);
1902            assert_eq!(level2_fields[2].fid, 300);
1903
1904            // Level 3 fields should be sorted
1905            if let LnmpValue::NestedRecord(level3_rec) = &level2_fields[2].value {
1906                let level3_fields = level3_rec.fields();
1907                assert_eq!(level3_fields[0].fid, 10);
1908                assert_eq!(level3_fields[1].fid, 20);
1909                assert_eq!(level3_fields[2].fid, 30);
1910            } else {
1911                panic!("Expected level 3 nested record");
1912            }
1913        } else {
1914            panic!("Expected level 2 nested record");
1915        }
1916    }
1917
1918    #[test]
1919    fn test_canonicalize_array_record_ordering() {
1920        // Test that records within nested arrays have sorted fields (Requirement 9.2)
1921        let mut rec1 = LnmpRecord::new();
1922        rec1.add_field(LnmpField {
1923            fid: 50,
1924            value: LnmpValue::Int(5),
1925        });
1926        rec1.add_field(LnmpField {
1927            fid: 10,
1928            value: LnmpValue::Int(1),
1929        });
1930        rec1.add_field(LnmpField {
1931            fid: 30,
1932            value: LnmpValue::Int(3),
1933        });
1934
1935        let mut rec2 = LnmpRecord::new();
1936        rec2.add_field(LnmpField {
1937            fid: 80,
1938            value: LnmpValue::Int(8),
1939        });
1940        rec2.add_field(LnmpField {
1941            fid: 20,
1942            value: LnmpValue::Int(2),
1943        });
1944        rec2.add_field(LnmpField {
1945            fid: 60,
1946            value: LnmpValue::Int(6),
1947        });
1948
1949        let mut outer = LnmpRecord::new();
1950        outer.add_field(LnmpField {
1951            fid: 100,
1952            value: LnmpValue::NestedArray(vec![rec1, rec2]),
1953        });
1954
1955        let canonical = canonicalize_record(&outer);
1956        let fields = canonical.fields();
1957
1958        assert_eq!(fields.len(), 1);
1959        assert_eq!(fields[0].fid, 100);
1960
1961        // Each record in the array should have sorted fields
1962        if let LnmpValue::NestedArray(arr) = &fields[0].value {
1963            assert_eq!(arr.len(), 2);
1964
1965            // First record fields should be sorted
1966            let rec1_fields = arr[0].fields();
1967            assert_eq!(rec1_fields[0].fid, 10);
1968            assert_eq!(rec1_fields[1].fid, 30);
1969            assert_eq!(rec1_fields[2].fid, 50);
1970
1971            // Second record fields should be sorted
1972            let rec2_fields = arr[1].fields();
1973            assert_eq!(rec2_fields[0].fid, 20);
1974            assert_eq!(rec2_fields[1].fid, 60);
1975            assert_eq!(rec2_fields[2].fid, 80);
1976        } else {
1977            panic!("Expected nested array");
1978        }
1979    }
1980
1981    #[test]
1982    fn test_canonicalize_empty_field_omission() {
1983        // Test that empty fields are omitted (Requirement 9.3)
1984        let mut record = LnmpRecord::new();
1985        record.add_field(LnmpField {
1986            fid: 10,
1987            value: LnmpValue::Int(42),
1988        });
1989        record.add_field(LnmpField {
1990            fid: 20,
1991            value: LnmpValue::String("".to_string()), // Empty string
1992        });
1993        record.add_field(LnmpField {
1994            fid: 30,
1995            value: LnmpValue::StringArray(vec![]), // Empty array
1996        });
1997        record.add_field(LnmpField {
1998            fid: 40,
1999            value: LnmpValue::NestedRecord(Box::new(LnmpRecord::new())), // Empty nested record
2000        });
2001        record.add_field(LnmpField {
2002            fid: 50,
2003            value: LnmpValue::NestedArray(vec![]), // Empty nested array
2004        });
2005        record.add_field(LnmpField {
2006            fid: 60,
2007            value: LnmpValue::String("not_empty".to_string()),
2008        });
2009
2010        let canonical = canonicalize_record(&record);
2011        let fields = canonical.fields();
2012
2013        // Only non-empty fields should remain
2014        assert_eq!(fields.len(), 2);
2015        assert_eq!(fields[0].fid, 10);
2016        assert_eq!(fields[0].value, LnmpValue::Int(42));
2017        assert_eq!(fields[1].fid, 60);
2018        assert_eq!(fields[1].value, LnmpValue::String("not_empty".to_string()));
2019    }
2020
2021    #[test]
2022    fn test_canonicalize_empty_field_omission_nested() {
2023        // Test that empty fields are omitted in nested structures
2024        let mut inner = LnmpRecord::new();
2025        inner.add_field(LnmpField {
2026            fid: 1,
2027            value: LnmpValue::Int(42),
2028        });
2029        inner.add_field(LnmpField {
2030            fid: 2,
2031            value: LnmpValue::String("".to_string()), // Empty string
2032        });
2033        inner.add_field(LnmpField {
2034            fid: 3,
2035            value: LnmpValue::String("value".to_string()),
2036        });
2037
2038        let mut outer = LnmpRecord::new();
2039        outer.add_field(LnmpField {
2040            fid: 100,
2041            value: LnmpValue::NestedRecord(Box::new(inner)),
2042        });
2043        outer.add_field(LnmpField {
2044            fid: 200,
2045            value: LnmpValue::StringArray(vec![]), // Empty array
2046        });
2047
2048        let canonical = canonicalize_record(&outer);
2049        let fields = canonical.fields();
2050
2051        // Only the nested record should remain (empty array omitted)
2052        assert_eq!(fields.len(), 1);
2053        assert_eq!(fields[0].fid, 100);
2054
2055        // Check nested record has empty field omitted
2056        if let LnmpValue::NestedRecord(nested) = &fields[0].value {
2057            let nested_fields = nested.fields();
2058            assert_eq!(nested_fields.len(), 2); // Only F1 and F3, F2 omitted
2059            assert_eq!(nested_fields[0].fid, 1);
2060            assert_eq!(nested_fields[1].fid, 3);
2061        } else {
2062            panic!("Expected nested record");
2063        }
2064    }
2065
2066    #[test]
2067    fn test_canonicalize_round_trip_stability_simple() {
2068        // Test round-trip stability for simple record (Requirement 9.4)
2069        let mut record = LnmpRecord::new();
2070        record.add_field(LnmpField {
2071            fid: 100,
2072            value: LnmpValue::Int(3),
2073        });
2074        record.add_field(LnmpField {
2075            fid: 5,
2076            value: LnmpValue::Int(1),
2077        });
2078        record.add_field(LnmpField {
2079            fid: 50,
2080            value: LnmpValue::Int(2),
2081        });
2082
2083        assert!(validate_round_trip_stability(&record));
2084    }
2085
2086    #[test]
2087    fn test_canonicalize_round_trip_stability_nested() {
2088        // Test round-trip stability for nested structures
2089        let mut inner = LnmpRecord::new();
2090        inner.add_field(LnmpField {
2091            fid: 30,
2092            value: LnmpValue::Int(3),
2093        });
2094        inner.add_field(LnmpField {
2095            fid: 10,
2096            value: LnmpValue::Int(1),
2097        });
2098        inner.add_field(LnmpField {
2099            fid: 20,
2100            value: LnmpValue::Int(2),
2101        });
2102
2103        let mut outer = LnmpRecord::new();
2104        outer.add_field(LnmpField {
2105            fid: 300,
2106            value: LnmpValue::NestedRecord(Box::new(inner)),
2107        });
2108        outer.add_field(LnmpField {
2109            fid: 100,
2110            value: LnmpValue::Int(10),
2111        });
2112        outer.add_field(LnmpField {
2113            fid: 200,
2114            value: LnmpValue::Int(20),
2115        });
2116
2117        assert!(validate_round_trip_stability(&outer));
2118    }
2119
2120    #[test]
2121    fn test_canonicalize_round_trip_stability_deeply_nested() {
2122        // Test round-trip stability with deeply nested structures (depth 5)
2123        let mut level5 = LnmpRecord::new();
2124        level5.add_field(LnmpField {
2125            fid: 5,
2126            value: LnmpValue::Int(5),
2127        });
2128        level5.add_field(LnmpField {
2129            fid: 1,
2130            value: LnmpValue::Int(1),
2131        });
2132
2133        let mut level4 = LnmpRecord::new();
2134        level4.add_field(LnmpField {
2135            fid: 4,
2136            value: LnmpValue::NestedRecord(Box::new(level5)),
2137        });
2138
2139        let mut level3 = LnmpRecord::new();
2140        level3.add_field(LnmpField {
2141            fid: 3,
2142            value: LnmpValue::NestedRecord(Box::new(level4)),
2143        });
2144
2145        let mut level2 = LnmpRecord::new();
2146        level2.add_field(LnmpField {
2147            fid: 2,
2148            value: LnmpValue::NestedRecord(Box::new(level3)),
2149        });
2150
2151        let mut level1 = LnmpRecord::new();
2152        level1.add_field(LnmpField {
2153            fid: 1,
2154            value: LnmpValue::NestedRecord(Box::new(level2)),
2155        });
2156
2157        assert!(validate_round_trip_stability(&level1));
2158    }
2159
2160    #[test]
2161    fn test_canonicalize_round_trip_stability_with_arrays() {
2162        // Test round-trip stability with nested arrays
2163        let mut rec1 = LnmpRecord::new();
2164        rec1.add_field(LnmpField {
2165            fid: 50,
2166            value: LnmpValue::Int(5),
2167        });
2168        rec1.add_field(LnmpField {
2169            fid: 10,
2170            value: LnmpValue::Int(1),
2171        });
2172
2173        let mut rec2 = LnmpRecord::new();
2174        rec2.add_field(LnmpField {
2175            fid: 80,
2176            value: LnmpValue::Int(8),
2177        });
2178        rec2.add_field(LnmpField {
2179            fid: 20,
2180            value: LnmpValue::Int(2),
2181        });
2182
2183        let mut outer = LnmpRecord::new();
2184        outer.add_field(LnmpField {
2185            fid: 100,
2186            value: LnmpValue::NestedArray(vec![rec1, rec2]),
2187        });
2188        outer.add_field(LnmpField {
2189            fid: 50,
2190            value: LnmpValue::String("test".to_string()),
2191        });
2192
2193        assert!(validate_round_trip_stability(&outer));
2194    }
2195
2196    #[test]
2197    fn test_canonicalize_round_trip_stability_with_empty_fields() {
2198        // Test round-trip stability with empty fields that should be omitted
2199        let mut record = LnmpRecord::new();
2200        record.add_field(LnmpField {
2201            fid: 10,
2202            value: LnmpValue::Int(42),
2203        });
2204        record.add_field(LnmpField {
2205            fid: 20,
2206            value: LnmpValue::String("".to_string()), // Empty string
2207        });
2208        record.add_field(LnmpField {
2209            fid: 30,
2210            value: LnmpValue::StringArray(vec![]), // Empty array
2211        });
2212
2213        assert!(validate_round_trip_stability(&record));
2214    }
2215
2216    #[test]
2217    fn test_canonicalize_round_trip_stability_mixed_structures() {
2218        // Test round-trip stability with mixed nested structures
2219        let mut inner_record = LnmpRecord::new();
2220        inner_record.add_field(LnmpField {
2221            fid: 15,
2222            value: LnmpValue::Int(5),
2223        });
2224        inner_record.add_field(LnmpField {
2225            fid: 5,
2226            value: LnmpValue::Int(3),
2227        });
2228
2229        let mut array_record = LnmpRecord::new();
2230        array_record.add_field(LnmpField {
2231            fid: 25,
2232            value: LnmpValue::Int(7),
2233        });
2234        array_record.add_field(LnmpField {
2235            fid: 20,
2236            value: LnmpValue::Int(6),
2237        });
2238
2239        let mut outer = LnmpRecord::new();
2240        outer.add_field(LnmpField {
2241            fid: 100,
2242            value: LnmpValue::NestedRecord(Box::new(inner_record)),
2243        });
2244        outer.add_field(LnmpField {
2245            fid: 50,
2246            value: LnmpValue::NestedArray(vec![array_record]),
2247        });
2248        outer.add_field(LnmpField {
2249            fid: 10,
2250            value: LnmpValue::String("test".to_string()),
2251        });
2252
2253        assert!(validate_round_trip_stability(&outer));
2254    }
2255
2256    #[test]
2257    fn test_is_empty_value() {
2258        // Test the is_empty_value helper function
2259        assert!(is_empty_value(&LnmpValue::String("".to_string())));
2260        assert!(!is_empty_value(&LnmpValue::String("not_empty".to_string())));
2261
2262        assert!(is_empty_value(&LnmpValue::StringArray(vec![])));
2263        assert!(!is_empty_value(&LnmpValue::StringArray(vec![
2264            "item".to_string()
2265        ])));
2266
2267        assert!(is_empty_value(&LnmpValue::NestedRecord(Box::new(
2268            LnmpRecord::new()
2269        ))));
2270        let mut non_empty_record = LnmpRecord::new();
2271        non_empty_record.add_field(LnmpField {
2272            fid: 1,
2273            value: LnmpValue::Int(42),
2274        });
2275        assert!(!is_empty_value(&LnmpValue::NestedRecord(Box::new(
2276            non_empty_record
2277        ))));
2278
2279        assert!(is_empty_value(&LnmpValue::NestedArray(vec![])));
2280        let mut rec = LnmpRecord::new();
2281        rec.add_field(LnmpField {
2282            fid: 1,
2283            value: LnmpValue::Int(42),
2284        });
2285        assert!(!is_empty_value(&LnmpValue::NestedArray(vec![rec])));
2286
2287        // Primitive values are never empty
2288        assert!(!is_empty_value(&LnmpValue::Int(0)));
2289        assert!(!is_empty_value(&LnmpValue::Int(42)));
2290        assert!(!is_empty_value(&LnmpValue::Float(0.0)));
2291        assert!(!is_empty_value(&LnmpValue::Float(3.14)));
2292        assert!(!is_empty_value(&LnmpValue::Bool(true)));
2293        assert!(!is_empty_value(&LnmpValue::Bool(false)));
2294    }
2295}