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 }
123 }
124
125 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 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 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 format!("{{{}}}", fields.join(";"))
162 }
163
164 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 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 format!("[{}]", encoded_records.join(","))
192 }
193
194 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 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 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
241pub fn canonicalize_record(record: &LnmpRecord) -> LnmpRecord {
249 let mut canonical = LnmpRecord::new();
250
251 let sorted = record.sorted_fields();
253
254 for field in sorted {
255 let canonical_value = canonicalize_value(&field.value);
256
257 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
269fn canonicalize_value(value: &LnmpValue) -> LnmpValue {
271 match value {
272 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 LnmpValue::NestedRecord(nested) => {
281 let canonical_nested = canonicalize_record(nested);
282 LnmpValue::NestedRecord(Box::new(canonical_nested))
283 }
284
285 LnmpValue::NestedArray(arr) => {
287 let canonical_arr: Vec<LnmpRecord> = arr.iter().map(canonicalize_record).collect();
288 LnmpValue::NestedArray(canonical_arr)
289 }
290 }
291}
292
293fn 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 LnmpValue::Int(_) | LnmpValue::Float(_) | LnmpValue::Bool(_) => false,
308 }
309}
310
311pub 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
323fn 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 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, ""); }
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 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 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 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 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 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 assert_eq!(output, "F5=1\nF50=2\nF100=3");
678 }
679
680 #[test]
681 fn test_deterministic_sorting_with_duplicates() {
682 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 assert_eq!(output, "F5=1\nF10=first\nF10=second");
702 }
703
704 #[test]
705 fn test_canonical_whitespace_formatting() {
706 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 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 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 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 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 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 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 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 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 assert_eq!(fields.len(), 2);
907 assert_eq!(fields[0].fid, 10);
908 assert_eq!(fields[1].fid, 50);
909
910 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 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 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 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 assert_eq!(fields[0].fid, 50);
1010 assert_eq!(fields[1].fid, 100);
1011
1012 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 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 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 assert_eq!(fields.len(), 0);
1056 }
1057
1058 #[test]
1059 fn test_canonicalize_preserves_values() {
1060 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 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 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 assert_eq!(fields[0].fid, 10);
1166 assert_eq!(fields[1].fid, 50);
1167 assert_eq!(fields[2].fid, 100);
1168
1169 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 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 #[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 assert_eq!(output, "F50={F7=1;F12=1}");
1212 }
1213
1214 #[test]
1215 fn test_encode_empty_nested_record() {
1216 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, ""); }
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 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 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, ""); }
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 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 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 assert_eq!(output, "F60=[{F10=1;F20=2}]");
1415 }
1416
1417 #[test]
1418 fn test_encode_mixed_nested_structures() {
1419 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 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 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 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 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 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 #[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 assert!(output.contains('#'));
1638 assert!(output.starts_with("F12=14532#"));
1639
1640 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 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 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 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 let output1 = encoder.encode(&record);
1733 let output2 = encoder.encode(&record);
1734 let output3 = encoder.encode(&record);
1735
1736 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 assert!(output.contains('#'));
1766 assert!(output.starts_with("F50:r={F12:i=1}#"));
1768
1769 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 assert!(output.contains('#'));
1800 assert!(output.starts_with("F60:ra=[{F12:i=1}]#"));
1801
1802 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 assert_ne!(output1, output2);
1835
1836 let checksum1 = output1.split('#').nth(1).unwrap();
1838 let checksum2 = output2.split('#').nth(1).unwrap();
1839 assert_ne!(checksum1, checksum2);
1840 }
1841
1842 #[test]
1845 fn test_canonicalize_field_ordering_multiple_levels() {
1846 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 assert_eq!(fields[0].fid, 1000);
1894 assert_eq!(fields[1].fid, 2000);
1895 assert_eq!(fields[2].fid, 3000);
1896
1897 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 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 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 if let LnmpValue::NestedArray(arr) = &fields[0].value {
1963 assert_eq!(arr.len(), 2);
1964
1965 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 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 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()), });
1993 record.add_field(LnmpField {
1994 fid: 30,
1995 value: LnmpValue::StringArray(vec![]), });
1997 record.add_field(LnmpField {
1998 fid: 40,
1999 value: LnmpValue::NestedRecord(Box::new(LnmpRecord::new())), });
2001 record.add_field(LnmpField {
2002 fid: 50,
2003 value: LnmpValue::NestedArray(vec![]), });
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 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 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()), });
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![]), });
2047
2048 let canonical = canonicalize_record(&outer);
2049 let fields = canonical.fields();
2050
2051 assert_eq!(fields.len(), 1);
2053 assert_eq!(fields[0].fid, 100);
2054
2055 if let LnmpValue::NestedRecord(nested) = &fields[0].value {
2057 let nested_fields = nested.fields();
2058 assert_eq!(nested_fields.len(), 2); 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 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 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 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 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 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()), });
2208 record.add_field(LnmpField {
2209 fid: 30,
2210 value: LnmpValue::StringArray(vec![]), });
2212
2213 assert!(validate_round_trip_stability(&record));
2214 }
2215
2216 #[test]
2217 fn test_canonicalize_round_trip_stability_mixed_structures() {
2218 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 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 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}