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