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