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