1use std::borrow::Cow;
4
5use crate::config::{ParserConfig, ParsingMode, TextInputMode};
6use crate::error::LnmpError;
7use crate::lexer::{Lexer, Token};
8use crate::normalizer::ValueNormalizer;
9use lnmp_core::checksum::SemanticChecksum;
10use lnmp_core::{FieldId, LnmpField, LnmpRecord, LnmpValue, TypeHint};
11use lnmp_sanitize::{sanitize_lnmp_text, SanitizationConfig};
12
13pub struct Parser<'a> {
15 lexer: Lexer<'a>,
16 current_token: Token,
17 config: ParserConfig,
18 nesting_depth: usize,
20 normalizer: Option<ValueNormalizer>,
21}
22
23impl<'a> Parser<'a> {
24 pub fn new(input: &'a str) -> Result<Self, LnmpError> {
26 Self::with_config(input, ParserConfig::default())
27 }
28
29 pub fn new_strict(input: &'a str) -> Result<Self, LnmpError> {
31 Self::with_config(
32 input,
33 ParserConfig {
34 text_input_mode: TextInputMode::Strict,
35 ..ParserConfig::default()
36 },
37 )
38 }
39
40 pub fn new_lenient(input: &'a str) -> Result<Self, LnmpError> {
42 Self::with_config(
43 input,
44 ParserConfig {
45 text_input_mode: TextInputMode::Lenient,
46 ..ParserConfig::default()
47 },
48 )
49 }
50
51 pub fn with_mode(input: &'a str, mode: ParsingMode) -> Result<Self, LnmpError> {
53 let config = ParserConfig {
54 mode,
55 ..Default::default()
56 };
57 Self::with_config(input, config)
58 }
59
60 pub fn with_config(input: &'a str, config: ParserConfig) -> Result<Self, LnmpError> {
62 let input_cow = match config.text_input_mode {
63 TextInputMode::Strict => Cow::Borrowed(input),
64 TextInputMode::Lenient => sanitize_lnmp_text(input, &SanitizationConfig::default()),
65 };
66
67 if config.mode == ParsingMode::Strict {
68 Self::check_for_comments(input_cow.as_ref())?;
69 }
70
71 let mut lexer = match input_cow {
72 Cow::Borrowed(s) => Lexer::new(s),
73 Cow::Owned(s) => {
74 let span_map = crate::lexer::build_span_map(s.as_str(), input);
75 Lexer::new_owned_with_original(s, input.to_string(), span_map)
76 }
77 };
78 let current_token = lexer.next_token()?;
79
80 let normalizer = config.semantic_dictionary.as_ref().map(|dict| {
81 ValueNormalizer::new(crate::normalizer::NormalizationConfig {
82 semantic_dictionary: Some(dict.clone()),
83 ..crate::normalizer::NormalizationConfig::default()
84 })
85 });
86
87 Ok(Self {
88 lexer,
89 current_token,
90 config,
91 nesting_depth: 0,
92 normalizer,
93 })
94 }
95
96 pub fn mode(&self) -> ParsingMode {
98 self.config.mode
99 }
100
101 fn advance(&mut self) -> Result<(), LnmpError> {
103 self.current_token = self.lexer.next_token()?;
104 Ok(())
105 }
106
107 fn expect(&mut self, expected: Token) -> Result<(), LnmpError> {
109 if self.current_token == expected {
110 self.advance()
111 } else {
112 let (line, column) = self.lexer.position_original();
113 Err(LnmpError::UnexpectedToken {
114 expected: format!("{:?}", expected),
115 found: self.current_token.clone(),
116 line,
117 column,
118 })
119 }
120 }
121
122 fn skip_newlines(&mut self) -> Result<(), LnmpError> {
124 while self.current_token == Token::Newline {
125 self.advance()?;
126 }
127 Ok(())
128 }
129
130 fn skip_comment(&mut self) -> Result<(), LnmpError> {
132 self.advance()?;
134
135 while self.current_token != Token::Newline && self.current_token != Token::Eof {
137 self.advance()?;
138 }
139
140 Ok(())
141 }
142
143 fn check_for_comments(input: &str) -> Result<(), LnmpError> {
145 for (line_idx, line) in input.lines().enumerate() {
148 let trimmed = line.trim();
149 if trimmed.starts_with('#') {
151 return Err(LnmpError::StrictModeViolation {
152 reason: "Comments are not allowed in strict mode".to_string(),
153 line: line_idx + 1,
154 column: 1,
155 });
156 }
157 }
158 Ok(())
159 }
160}
161
162impl<'a> Parser<'a> {
163 fn parse_field_id(&mut self) -> Result<FieldId, LnmpError> {
165 let (line, column) = self.lexer.position_original();
166
167 self.expect(Token::FieldPrefix)?;
169
170 match &self.current_token {
172 Token::Number(num_str) => {
173 let num_str = num_str.clone();
174 self.advance()?;
175
176 match num_str.parse::<u16>() {
178 Ok(fid) => Ok(fid),
179 Err(_) => Err(LnmpError::InvalidFieldId {
180 value: num_str,
181 line,
182 column,
183 }),
184 }
185 }
186 Token::UnquotedString(s) => {
187 Err(LnmpError::InvalidFieldId {
189 value: s.clone(),
190 line,
191 column,
192 })
193 }
194 _ => Err(LnmpError::UnexpectedToken {
195 expected: "field ID number".to_string(),
196 found: self.current_token.clone(),
197 line,
198 column,
199 }),
200 }
201 }
202
203 #[allow(dead_code)]
205 fn parse_value(&mut self) -> Result<LnmpValue, LnmpError> {
206 self.parse_value_with_hint(None)
207 }
208
209 fn parse_value_with_hint(
211 &mut self,
212 type_hint: Option<TypeHint>,
213 ) -> Result<LnmpValue, LnmpError> {
214 let (line, column) = self.lexer.position_original();
215
216 match &self.current_token {
217 Token::Number(num_str) => {
218 let num_str = num_str.clone();
219 self.advance()?;
220 if type_hint == Some(TypeHint::Bool) {
222 if num_str == "0" {
223 return Ok(LnmpValue::Bool(false));
224 } else if num_str == "1" {
225 return Ok(LnmpValue::Bool(true));
226 } else {
227 return Err(LnmpError::InvalidValue {
228 field_id: 0,
229 reason: format!("invalid boolean value: {}", num_str),
230 line,
231 column,
232 });
233 }
234 }
235
236 if self.config.normalize_values {
238 if num_str == "0" {
239 return Ok(LnmpValue::Bool(false));
240 } else if num_str == "1" {
241 return Ok(LnmpValue::Bool(true));
242 }
243 }
244
245 if num_str.contains('.') {
247 match num_str.parse::<f64>() {
248 Ok(f) => Ok(LnmpValue::Float(f)),
249 Err(_) => Err(LnmpError::InvalidValue {
250 field_id: 0,
251 reason: format!("invalid float: {}", num_str),
252 line,
253 column,
254 }),
255 }
256 } else {
257 match num_str.parse::<i64>() {
259 Ok(i) => Ok(LnmpValue::Int(i)),
260 Err(_) => Err(LnmpError::InvalidValue {
261 field_id: 0,
262 reason: format!("invalid integer: {}", num_str),
263 line,
264 column,
265 }),
266 }
267 }
268 }
269 Token::QuotedString(s) => {
270 let s = s.clone();
271 self.advance()?;
272 Ok(LnmpValue::String(s))
273 }
274 Token::UnquotedString(s) => {
275 let s = s.clone();
276 self.advance()?;
277 if type_hint == Some(TypeHint::Bool) || self.config.normalize_values {
280 match s.to_ascii_lowercase().as_str() {
281 "true" | "yes" => return Ok(LnmpValue::Bool(true)),
282 "false" | "no" => return Ok(LnmpValue::Bool(false)),
283 _ => {}
284 }
285 }
286 Ok(LnmpValue::String(s))
287 }
288 Token::LeftBracket => self.parse_string_array_or_nested_array_with_hint(type_hint),
289 Token::LeftBrace => self.parse_nested_record(),
290 _ => Err(LnmpError::UnexpectedToken {
291 expected: "value".to_string(),
292 found: self.current_token.clone(),
293 line,
294 column,
295 }),
296 }
297 }
298
299 #[allow(dead_code)]
301 fn parse_string_array_or_nested_array(&mut self) -> Result<LnmpValue, LnmpError> {
302 self.parse_string_array_or_nested_array_with_hint(None)
303 }
304
305 fn parse_string_array_or_nested_array_with_hint(
307 &mut self,
308 type_hint: Option<TypeHint>,
309 ) -> Result<LnmpValue, LnmpError> {
310 self.expect(Token::LeftBracket)?;
311
312 if self.current_token == Token::RightBracket {
314 self.advance()?;
315 if type_hint == Some(TypeHint::RecordArray) {
317 return Ok(LnmpValue::NestedArray(Vec::new()));
318 }
319 return Ok(LnmpValue::StringArray(Vec::new()));
321 }
322
323 if self.current_token == Token::LeftBrace {
325 self.parse_nested_array()
326 } else {
327 self.parse_string_array()
328 }
329 }
330
331 fn parse_string_array(&mut self) -> Result<LnmpValue, LnmpError> {
333 let (line, column) = self.lexer.position_original();
334 let mut items = Vec::new();
335
336 loop {
337 match &self.current_token {
339 Token::QuotedString(s) => {
340 items.push(s.clone());
341 self.advance()?;
342 }
343 Token::UnquotedString(s) => {
344 items.push(s.clone());
345 self.advance()?;
346 }
347 _ => {
348 return Err(LnmpError::UnexpectedToken {
349 expected: "string".to_string(),
350 found: self.current_token.clone(),
351 line,
352 column,
353 });
354 }
355 }
356
357 match &self.current_token {
359 Token::Comma => {
360 self.advance()?;
361 }
363 Token::RightBracket => {
364 self.advance()?;
365 break;
366 }
367 _ => {
368 return Err(LnmpError::UnexpectedToken {
369 expected: "comma or closing bracket".to_string(),
370 found: self.current_token.clone(),
371 line,
372 column,
373 });
374 }
375 }
376 }
377
378 Ok(LnmpValue::StringArray(items))
379 }
380
381 fn parse_nested_record(&mut self) -> Result<LnmpValue, LnmpError> {
383 let (line, column) = self.lexer.position_original();
384 self.expect(Token::LeftBrace)?;
385
386 self.nesting_depth += 1;
388 if let Some(max) = self.config.max_nesting_depth {
389 if self.nesting_depth > max {
390 let actual = self.nesting_depth;
391 self.nesting_depth = self.nesting_depth.saturating_sub(1);
392 return Err(LnmpError::NestingTooDeep {
393 max_depth: max,
394 actual_depth: actual,
395 line,
396 column,
397 });
398 }
399 }
400
401 let result = (|| -> Result<LnmpValue, LnmpError> {
403 let mut record = LnmpRecord::new();
404
405 if self.current_token == Token::RightBrace {
407 self.advance()?;
408 return Ok(LnmpValue::NestedRecord(Box::new(record)));
409 }
410
411 loop {
413 let field = self.parse_field_assignment()?;
414 if self.config.mode == ParsingMode::Strict && record.get_field(field.fid).is_some()
416 {
417 let (line, column) = self.lexer.position_original();
418 return Err(LnmpError::DuplicateFieldId {
419 field_id: field.fid,
420 line,
421 column,
422 });
423 }
424 record.add_field(field);
425
426 match &self.current_token {
428 Token::Semicolon => {
429 self.advance()?;
430 if self.current_token == Token::RightBrace {
432 self.advance()?;
433 break;
434 }
435 }
437 Token::RightBrace => {
438 self.advance()?;
439 break;
440 }
441 _ => {
442 return Err(LnmpError::UnexpectedToken {
443 expected: "semicolon or closing brace".to_string(),
444 found: self.current_token.clone(),
445 line,
446 column,
447 });
448 }
449 }
450 }
451
452 let sorted_record = LnmpRecord::from_sorted_fields(record.sorted_fields());
454
455 Ok(LnmpValue::NestedRecord(Box::new(sorted_record)))
456 })();
457
458 self.nesting_depth = self.nesting_depth.saturating_sub(1);
460 result
461 }
462
463 fn parse_nested_array(&mut self) -> Result<LnmpValue, LnmpError> {
465 let (line, column) = self.lexer.position_original();
466
467 self.nesting_depth += 1;
469 if let Some(max) = self.config.max_nesting_depth {
470 if self.nesting_depth > max {
471 let actual = self.nesting_depth;
472 self.nesting_depth = self.nesting_depth.saturating_sub(1);
473 return Err(LnmpError::NestingTooDeep {
474 max_depth: max,
475 actual_depth: actual,
476 line,
477 column,
478 });
479 }
480 }
481
482 let result = (|| -> Result<LnmpValue, LnmpError> {
483 let mut records = Vec::new();
484
485 loop {
486 if self.current_token != Token::LeftBrace {
488 return Err(LnmpError::UnexpectedToken {
489 expected: "left brace for nested record".to_string(),
490 found: self.current_token.clone(),
491 line,
492 column,
493 });
494 }
495
496 match self.parse_nested_record()? {
498 LnmpValue::NestedRecord(record) => {
499 records.push(*record);
500 }
501 _ => unreachable!("parse_nested_record always returns NestedRecord"),
502 }
503
504 match &self.current_token {
506 Token::Comma => {
507 self.advance()?;
508 }
510 Token::RightBracket => {
511 self.advance()?;
512 break;
513 }
514 _ => {
515 return Err(LnmpError::UnexpectedToken {
516 expected: "comma or closing bracket".to_string(),
517 found: self.current_token.clone(),
518 line,
519 column,
520 });
521 }
522 }
523 }
524
525 Ok(LnmpValue::NestedArray(records))
526 })();
527
528 self.nesting_depth = self.nesting_depth.saturating_sub(1);
530 result
531 }
532
533 fn parse_type_hint(&mut self) -> Result<Option<TypeHint>, LnmpError> {
535 if let Token::TypeHint(hint_str) = &self.current_token {
536 let hint_str = hint_str.clone();
537 self.advance()?;
538
539 match TypeHint::parse(&hint_str) {
540 Some(hint) => Ok(Some(hint)),
541 None => {
542 let (line, column) = self.lexer.position_original();
543 Err(LnmpError::InvalidTypeHint {
544 hint: hint_str,
545 line,
546 column,
547 })
548 }
549 }
550 } else {
551 Ok(None)
552 }
553 }
554
555 fn parse_field_assignment(&mut self) -> Result<LnmpField, LnmpError> {
557 let fid = self.parse_field_id()?;
558
559 let type_hint = self.parse_type_hint()?;
561
562 self.expect(Token::Equals)?;
563 let value = self.parse_value_with_hint(type_hint)?;
564
565 if let Some(hint) = type_hint {
567 if !hint.validates(&value) {
568 let (line, column) = self.lexer.position_original();
569 return Err(LnmpError::TypeHintMismatch {
570 field_id: fid,
571 expected_type: hint.as_str().to_string(),
572 actual_value: format!("{:?}", value),
573 line,
574 column,
575 });
576 }
577 }
578
579 if self.current_token == Token::Hash {
581 self.parse_and_validate_checksum(fid, type_hint, &value)?;
582 } else if self.config.require_checksums {
583 let (line, column) = self.lexer.position_original();
584 return Err(LnmpError::ChecksumMismatch {
585 field_id: fid,
586 expected: "checksum required".to_string(),
587 found: "no checksum".to_string(),
588 line,
589 column,
590 });
591 }
592
593 let normalized_value = if let Some(norm) = &self.normalizer {
595 norm.normalize_with_fid(Some(fid), &value)
596 } else {
597 value
598 };
599
600 Ok(LnmpField {
601 fid,
602 value: normalized_value,
603 })
604 }
605
606 fn parse_and_validate_checksum(
608 &mut self,
609 fid: FieldId,
610 type_hint: Option<TypeHint>,
611 value: &LnmpValue,
612 ) -> Result<(), LnmpError> {
613 let (line, column) = self.lexer.position_original();
614
615 self.expect(Token::Hash)?;
617
618 let mut checksum_str = String::new();
621
622 loop {
624 match &self.current_token {
625 Token::Number(s) => {
626 checksum_str.push_str(s);
627 self.advance()?;
628 }
629 Token::UnquotedString(s) => {
630 checksum_str.push_str(s);
631 self.advance()?;
632 }
633 Token::Newline | Token::Semicolon | Token::Eof => {
634 break;
635 }
636 _ => {
637 return Err(LnmpError::UnexpectedToken {
638 expected: "checksum (8 hex characters)".to_string(),
639 found: self.current_token.clone(),
640 line,
641 column,
642 });
643 }
644 }
645
646 if checksum_str.len() >= 8 {
648 break;
649 }
650 }
651
652 let provided_checksum =
654 SemanticChecksum::parse(&checksum_str).ok_or_else(|| LnmpError::InvalidChecksum {
655 field_id: fid,
656 reason: format!("invalid checksum format: {}", checksum_str),
657 line,
658 column,
659 })?;
660
661 if self.config.validate_checksums {
663 let computed_checksum = SemanticChecksum::compute(fid, type_hint, value);
664 if provided_checksum != computed_checksum {
665 return Err(LnmpError::ChecksumMismatch {
666 field_id: fid,
667 expected: SemanticChecksum::format(computed_checksum),
668 found: SemanticChecksum::format(provided_checksum),
669 line,
670 column,
671 });
672 }
673 }
674
675 Ok(())
676 }
677
678 fn validate_field_order(&self, record: &LnmpRecord) -> Result<(), LnmpError> {
680 let fields = record.fields();
681 for i in 1..fields.len() {
682 if fields[i].fid < fields[i - 1].fid {
683 let (line, column) = self.lexer.position_original();
684 return Err(LnmpError::StrictModeViolation {
685 reason: format!(
686 "Fields must be sorted by FID in strict mode (F{} appears after F{})",
687 fields[i].fid,
688 fields[i - 1].fid
689 ),
690 line,
691 column,
692 });
693 }
694 }
695 Ok(())
696 }
697
698 fn validate_separator(&self, is_semicolon: bool) -> Result<(), LnmpError> {
703 if self.config.mode == ParsingMode::Strict && is_semicolon {
704 let (line, column) = self.lexer.position_original();
705 return Err(LnmpError::StrictModeViolation {
706 reason: "Semicolons are not allowed in strict mode (use newlines)".to_string(),
707 line,
708 column,
709 });
710 }
711 Ok(())
712 }
713
714 pub fn parse_record(&mut self) -> Result<LnmpRecord, LnmpError> {
716 let mut record = LnmpRecord::new();
717
718 self.skip_newlines()?;
720 while self.current_token == Token::Hash {
721 self.skip_comment()?;
722 if self.current_token == Token::Newline {
723 self.advance()?;
724 }
725 self.skip_newlines()?;
726 }
727
728 while self.current_token != Token::Eof {
730 let field = self.parse_field_assignment()?;
731 if self.config.mode == ParsingMode::Strict && record.get_field(field.fid).is_some() {
733 let (line, column) = self.lexer.position_original();
734 return Err(LnmpError::DuplicateFieldId {
735 field_id: field.fid,
736 line,
737 column,
738 });
739 }
740 record.add_field(field);
741
742 match &self.current_token {
744 Token::Semicolon => {
745 self.validate_separator(true)?;
746 self.advance()?;
747 }
748 Token::Newline => {
749 self.advance()?;
750 self.skip_newlines()?;
751 while self.current_token == Token::Hash {
753 self.skip_comment()?;
754 if self.current_token == Token::Newline {
755 self.advance()?;
756 }
757 self.skip_newlines()?;
758 }
759 }
760 Token::Eof => break,
761 _ => {
762 let (line, column) = self.lexer.position_original();
763 return Err(LnmpError::UnexpectedToken {
764 expected: "semicolon, newline, or EOF".to_string(),
765 found: self.current_token.clone(),
766 line,
767 column,
768 });
769 }
770 }
771 }
772
773 if self.config.mode == ParsingMode::Strict {
775 self.validate_field_order(&record)?;
776 }
777
778 if let Some(limits) = &self.config.structural_limits {
780 if let Err(err) = limits.validate_record(&record) {
781 let (line, column) = self.lexer.position_original();
782 return Err(LnmpError::InvalidNestedStructure {
783 reason: format!("structural limits violated: {}", err),
784 line,
785 column,
786 });
787 }
788 }
789
790 Ok(record)
791 }
792}
793
794#[cfg(test)]
795#[allow(clippy::approx_constant)]
796mod tests {
797 use super::*;
798
799 #[test]
800 fn test_parse_integer() {
801 let mut parser = Parser::new("F1=42").unwrap();
802 let record = parser.parse_record().unwrap();
803 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
804 }
805
806 #[test]
807 fn test_parse_negative_integer() {
808 let mut parser = Parser::new("F1=-123").unwrap();
809 let record = parser.parse_record().unwrap();
810 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(-123));
811 }
812
813 #[test]
814 fn test_parse_float() {
815 let mut parser = Parser::new("F2=3.14").unwrap();
816 let record = parser.parse_record().unwrap();
817 assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
818 }
819
820 #[test]
821 fn test_parse_bool_true() {
822 let mut parser = Parser::new("F3=1").unwrap();
823 let record = parser.parse_record().unwrap();
824 assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Bool(true));
825 }
826
827 #[test]
828 fn test_parse_bool_false() {
829 let mut parser = Parser::new("F3=0").unwrap();
830 let record = parser.parse_record().unwrap();
831 assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Bool(false));
832 }
833
834 #[test]
835 fn test_duplicate_field_id_strict_mode_error() {
836 let mut parser = Parser::with_mode("F1=1\nF1=2", ParsingMode::Strict).unwrap();
837 let err = parser.parse_record().unwrap_err();
838 match err {
839 LnmpError::DuplicateFieldId { field_id, .. } => {
840 assert_eq!(field_id, 1);
841 }
842 _ => panic!("expected DuplicateFieldId error, got: {:?}", err),
843 }
844 }
845
846 #[test]
847 fn test_duplicate_field_id_loose_mode_allows() {
848 let mut parser = Parser::new("F1=1;F1=2").unwrap();
849 let record = parser.parse_record().unwrap();
850 let fields = record.fields();
851 assert_eq!(fields.len(), 2);
852 assert_eq!(fields[0].fid, 1);
853 assert_eq!(fields[1].fid, 1);
854 }
855
856 #[test]
857 fn test_duplicate_field_id_in_nested_record_strict_mode_error() {
858 let mut parser = Parser::with_mode("F50={F1=1;F1=2}", ParsingMode::Strict).unwrap();
859 let err = parser.parse_record().unwrap_err();
860 match err {
861 LnmpError::DuplicateFieldId { field_id, .. } => {
862 assert_eq!(field_id, 1);
863 }
864 _ => panic!("expected DuplicateFieldId error, got: {:?}", err),
865 }
866 }
867
868 #[test]
869 fn test_duplicate_field_id_in_nested_record_loose_mode_allows() {
870 let mut parser = Parser::new("F50={F1=1;F1=2}").unwrap();
871 let record = parser.parse_record().unwrap();
872 let nested = record.get_field(50).unwrap();
873 if let LnmpValue::NestedRecord(nested_record) = &nested.value {
874 assert_eq!(nested_record.fields().len(), 2);
875 assert_eq!(nested_record.fields()[0].fid, 1);
876 assert_eq!(nested_record.fields()[1].fid, 1);
877 } else {
878 panic!("expected nested record");
879 }
880 }
881
882 #[test]
883 fn test_parse_quoted_string() {
884 let mut parser = Parser::new(r#"F4="hello world""#).unwrap();
885 let record = parser.parse_record().unwrap();
886 assert_eq!(
887 record.get_field(4).unwrap().value,
888 LnmpValue::String("hello world".to_string())
889 );
890 }
891
892 #[test]
893 fn test_parse_unquoted_string() {
894 let mut parser = Parser::new("F5=test_value").unwrap();
895 let record = parser.parse_record().unwrap();
896 assert_eq!(
897 record.get_field(5).unwrap().value,
898 LnmpValue::String("test_value".to_string())
899 );
900 }
901
902 #[test]
903 fn test_parse_string_array() {
904 let mut parser = Parser::new(r#"F6=["admin","dev","user"]"#).unwrap();
905 let record = parser.parse_record().unwrap();
906 assert_eq!(
907 record.get_field(6).unwrap().value,
908 LnmpValue::StringArray(vec![
909 "admin".to_string(),
910 "dev".to_string(),
911 "user".to_string()
912 ])
913 );
914 }
915
916 #[test]
917 fn test_parse_empty_string_array() {
918 let mut parser = Parser::new("F6=[]").unwrap();
919 let record = parser.parse_record().unwrap();
920 assert_eq!(
921 record.get_field(6).unwrap().value,
922 LnmpValue::StringArray(vec![])
923 );
924 }
925
926 #[test]
927 fn test_parse_multiline_record() {
928 let input = "F12=14532\nF7=1\nF20=\"Halil\"";
929 let mut parser = Parser::new(input).unwrap();
930 let record = parser.parse_record().unwrap();
931
932 assert_eq!(record.fields().len(), 3);
933 assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
934 assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
935 assert_eq!(
936 record.get_field(20).unwrap().value,
937 LnmpValue::String("Halil".to_string())
938 );
939 }
940
941 #[test]
942 fn test_parse_inline_record() {
943 let input = r#"F12=14532;F7=1;F23=["admin","dev"]"#;
944 let mut parser = Parser::new(input).unwrap();
945 let record = parser.parse_record().unwrap();
946
947 assert_eq!(record.fields().len(), 3);
948 assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
949 assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
950 assert_eq!(
951 record.get_field(23).unwrap().value,
952 LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()])
953 );
954 }
955
956 #[test]
957 fn test_parse_with_comments() {
958 let input = "# This is a comment\nF1=42\n# Another comment\nF2=3.14";
959 let mut parser = Parser::new(input).unwrap();
960 let record = parser.parse_record().unwrap();
961
962 assert_eq!(record.fields().len(), 2);
963 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
964 assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
965 }
966
967 #[test]
968 fn test_parse_with_whitespace() {
969 let input = "F1 = 42 ; F2 = 3.14";
970 let mut parser = Parser::new(input).unwrap();
971 let record = parser.parse_record().unwrap();
972
973 assert_eq!(record.fields().len(), 2);
974 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
975 assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
976 }
977
978 #[test]
979 fn test_parse_empty_input() {
980 let mut parser = Parser::new("").unwrap();
981 let record = parser.parse_record().unwrap();
982 assert_eq!(record.fields().len(), 0);
983 }
984
985 #[test]
986 fn test_parse_only_comments() {
987 let input = "# Comment 1\n# Comment 2\n# Comment 3";
988 let mut parser = Parser::new(input).unwrap();
989 let record = parser.parse_record().unwrap();
990 assert_eq!(record.fields().len(), 0);
991 }
992
993 #[test]
994 fn test_parse_field_id_out_of_range() {
995 let result = Parser::new("F99999=42");
996 assert!(result.is_ok());
997 let mut parser = result.unwrap();
998 let result = parser.parse_record();
999 assert!(result.is_err());
1000 match result {
1001 Err(LnmpError::InvalidFieldId { .. }) => {}
1002 _ => panic!("Expected InvalidFieldId error"),
1003 }
1004 }
1005
1006 #[test]
1007 fn test_parse_missing_equals() {
1008 let mut parser = Parser::new("F1 42").unwrap();
1009 let result = parser.parse_record();
1010 assert!(result.is_err());
1011 match result {
1012 Err(LnmpError::UnexpectedToken { .. }) => {}
1013 _ => panic!("Expected UnexpectedToken error"),
1014 }
1015 }
1016
1017 #[test]
1018 fn test_parse_missing_value() {
1019 let mut parser = Parser::new("F1=").unwrap();
1020 let result = parser.parse_record();
1021 assert!(result.is_err());
1022 }
1023
1024 #[test]
1025 fn test_parse_invalid_field_prefix() {
1026 let result = Parser::new("G1=42");
1027 assert!(result.is_ok());
1028 let mut parser = result.unwrap();
1029 let result = parser.parse_record();
1030 assert!(result.is_err());
1031 }
1032
1033 #[test]
1034 fn test_parse_string_with_escapes() {
1035 let input = r#"F1="hello \"world\"""#;
1036 let mut parser = Parser::new(input).unwrap();
1037 let record = parser.parse_record().unwrap();
1038 assert_eq!(
1039 record.get_field(1).unwrap().value,
1040 LnmpValue::String("hello \"world\"".to_string())
1041 );
1042 }
1043
1044 #[test]
1045 fn test_parse_mixed_separators() {
1046 let input = "F1=1;F2=2\nF3=3;F4=4";
1047 let mut parser = Parser::new(input).unwrap();
1048 let record = parser.parse_record().unwrap();
1049 assert_eq!(record.fields().len(), 4);
1050 }
1051
1052 #[test]
1053 fn test_parse_large_field_id() {
1054 let mut parser = Parser::new("F65535=42").unwrap();
1055 let record = parser.parse_record().unwrap();
1056 assert_eq!(record.get_field(65535).unwrap().value, LnmpValue::Int(42));
1057 }
1058
1059 #[test]
1060 fn test_parse_spec_example() {
1061 let input = r#"F12=14532;F7=1;F23=["admin","dev"]"#;
1062 let mut parser = Parser::new(input).unwrap();
1063 let record = parser.parse_record().unwrap();
1064
1065 assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
1066 assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
1067 assert_eq!(
1068 record.get_field(23).unwrap().value,
1069 LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()])
1070 );
1071 }
1072
1073 #[test]
1074 fn test_strict_mode_rejects_unsorted_fields() {
1075 use crate::config::ParsingMode;
1076
1077 let input = r#"F12=14532
1079F7=1
1080F23=["admin","dev"]"#;
1081
1082 let mut parser = Parser::with_mode(input, ParsingMode::Strict).unwrap();
1083 let result = parser.parse_record();
1084
1085 assert!(result.is_err());
1086 match result {
1087 Err(LnmpError::StrictModeViolation { reason, .. }) => {
1088 assert!(reason.contains("sorted"));
1089 }
1090 _ => panic!("Expected StrictModeViolation error"),
1091 }
1092 }
1093
1094 #[test]
1095 fn test_strict_mode_accepts_sorted_fields() {
1096 use crate::config::ParsingMode;
1097
1098 let input = r#"F7=1
1100F12=14532
1101F23=["admin","dev"]"#;
1102
1103 let mut parser = Parser::with_mode(input, ParsingMode::Strict).unwrap();
1104 let record = parser.parse_record().unwrap();
1105
1106 assert_eq!(record.fields().len(), 3);
1107 assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
1108 assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
1109 }
1110
1111 #[test]
1112 fn test_strict_mode_rejects_semicolons() {
1113 use crate::config::ParsingMode;
1114
1115 let input = "F1=1;F2=2";
1116
1117 let mut parser = Parser::with_mode(input, ParsingMode::Strict).unwrap();
1118 let result = parser.parse_record();
1119
1120 assert!(result.is_err());
1121 match result {
1122 Err(LnmpError::StrictModeViolation { reason, .. }) => {
1123 assert!(reason.contains("Semicolons"));
1124 }
1125 _ => panic!("Expected StrictModeViolation error"),
1126 }
1127 }
1128
1129 #[test]
1130 fn test_strict_mode_rejects_comments() {
1131 use crate::config::ParsingMode;
1132
1133 let input = "# This is a comment\nF1=42";
1134
1135 let result = Parser::with_mode(input, ParsingMode::Strict);
1136
1137 assert!(result.is_err());
1138 match result {
1139 Err(LnmpError::StrictModeViolation { reason, .. }) => {
1140 assert!(reason.contains("Comments"));
1141 }
1142 _ => panic!("Expected StrictModeViolation error"),
1143 }
1144 }
1145
1146 #[test]
1147 fn test_loose_mode_accepts_unsorted_fields() {
1148 use crate::config::ParsingMode;
1149
1150 let input = r#"F23=["admin","dev"]
1152F7=1
1153F12=14532"#;
1154
1155 let mut parser = Parser::with_mode(input, ParsingMode::Loose).unwrap();
1156 let record = parser.parse_record().unwrap();
1157
1158 assert_eq!(record.fields().len(), 3);
1159 assert_eq!(record.fields()[0].fid, 23);
1161 assert_eq!(record.fields()[1].fid, 7);
1162 assert_eq!(record.fields()[2].fid, 12);
1163 }
1164
1165 #[test]
1166 fn test_loose_mode_accepts_semicolons() {
1167 use crate::config::ParsingMode;
1168
1169 let input = "F1=1;F2=2;F3=3";
1170
1171 let mut parser = Parser::with_mode(input, ParsingMode::Loose).unwrap();
1172 let record = parser.parse_record().unwrap();
1173
1174 assert_eq!(record.fields().len(), 3);
1175 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Bool(true));
1176 assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Int(2));
1177 assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Int(3));
1178 }
1179
1180 #[test]
1181 fn test_loose_mode_accepts_whitespace() {
1182 use crate::config::ParsingMode;
1183
1184 let input = "F1 = 42 ; F2 = 3.14";
1185
1186 let mut parser = Parser::with_mode(input, ParsingMode::Loose).unwrap();
1187 let record = parser.parse_record().unwrap();
1188
1189 assert_eq!(record.fields().len(), 2);
1190 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1191 assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
1192 }
1193
1194 #[test]
1195 fn test_loose_mode_accepts_comments() {
1196 use crate::config::ParsingMode;
1197
1198 let input = "# Comment\nF1=42\n# Another comment\nF2=3.14";
1199
1200 let mut parser = Parser::with_mode(input, ParsingMode::Loose).unwrap();
1201 let record = parser.parse_record().unwrap();
1202
1203 assert_eq!(record.fields().len(), 2);
1204 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1205 assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
1206 }
1207
1208 #[test]
1209 fn test_default_mode_is_loose() {
1210 let input = "F2=2;F1=1"; let mut parser = Parser::new(input).unwrap();
1213 assert_eq!(parser.mode(), ParsingMode::Loose);
1214
1215 let record = parser.parse_record().unwrap();
1216 assert_eq!(record.fields().len(), 2);
1217 }
1218
1219 #[test]
1220 fn test_strict_mode_with_sorted_no_semicolons() {
1221 use crate::config::ParsingMode;
1222
1223 let input = "F1=1\nF2=2\nF3=3";
1224
1225 let mut parser = Parser::with_mode(input, ParsingMode::Strict).unwrap();
1226 let record = parser.parse_record().unwrap();
1227
1228 assert_eq!(record.fields().len(), 3);
1229 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Bool(true));
1230 assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Int(2));
1231 assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Int(3));
1232 }
1233
1234 #[test]
1235 fn test_parse_type_hint_integer() {
1236 let input = "F12:i=14532";
1237 let mut parser = Parser::new(input).unwrap();
1238 let record = parser.parse_record().unwrap();
1239
1240 assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
1241 }
1242
1243 #[test]
1244 fn test_parse_type_hint_float() {
1245 let input = "F5:f=3.14";
1246 let mut parser = Parser::new(input).unwrap();
1247 let record = parser.parse_record().unwrap();
1248
1249 assert_eq!(record.get_field(5).unwrap().value, LnmpValue::Float(3.14));
1250 }
1251
1252 #[test]
1253 fn test_parse_type_hint_bool() {
1254 let input = "F7:b=1";
1255 let mut parser = Parser::new(input).unwrap();
1256 let record = parser.parse_record().unwrap();
1257
1258 assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
1259 }
1260
1261 #[test]
1262 fn test_parse_type_hint_string() {
1263 let input = r#"F10:s="test""#;
1264 let mut parser = Parser::new(input).unwrap();
1265 let record = parser.parse_record().unwrap();
1266
1267 assert_eq!(
1268 record.get_field(10).unwrap().value,
1269 LnmpValue::String("test".to_string())
1270 );
1271 }
1272
1273 #[test]
1274 fn test_parse_type_hint_string_array() {
1275 let input = r#"F23:sa=["admin","dev"]"#;
1276 let mut parser = Parser::new(input).unwrap();
1277 let record = parser.parse_record().unwrap();
1278
1279 assert_eq!(
1280 record.get_field(23).unwrap().value,
1281 LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()])
1282 );
1283 }
1284
1285 #[test]
1286 fn test_parse_all_type_hints() {
1287 let input = r#"F1:i=42
1288F2:f=3.14
1289F3:b=1
1290F4:s=test
1291F5:sa=[a,b]"#;
1292 let mut parser = Parser::new(input).unwrap();
1293 let record = parser.parse_record().unwrap();
1294
1295 assert_eq!(record.fields().len(), 5);
1296 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1297 assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Float(3.14));
1298 assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Bool(true));
1299 assert_eq!(
1300 record.get_field(4).unwrap().value,
1301 LnmpValue::String("test".to_string())
1302 );
1303 assert_eq!(
1304 record.get_field(5).unwrap().value,
1305 LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()])
1306 );
1307 }
1308
1309 #[test]
1310 fn test_type_hint_mismatch_int_vs_float() {
1311 let input = "F1:i=3.14"; let mut parser = Parser::new(input).unwrap();
1313 let result = parser.parse_record();
1314
1315 assert!(result.is_err());
1316 match result {
1317 Err(LnmpError::TypeHintMismatch {
1318 field_id,
1319 expected_type,
1320 ..
1321 }) => {
1322 assert_eq!(field_id, 1);
1323 assert_eq!(expected_type, "i");
1324 }
1325 _ => panic!("Expected TypeHintMismatch error"),
1326 }
1327 }
1328
1329 #[test]
1330 fn test_type_hint_mismatch_float_vs_int() {
1331 let input = "F2:f=42"; let mut parser = Parser::new(input).unwrap();
1333 let result = parser.parse_record();
1334
1335 assert!(result.is_err());
1336 match result {
1337 Err(LnmpError::TypeHintMismatch {
1338 field_id,
1339 expected_type,
1340 ..
1341 }) => {
1342 assert_eq!(field_id, 2);
1343 assert_eq!(expected_type, "f");
1344 }
1345 _ => panic!("Expected TypeHintMismatch error"),
1346 }
1347 }
1348
1349 #[test]
1350 fn test_type_hint_mismatch_string_vs_int() {
1351 let input = "F3:s=42"; let mut parser = Parser::new(input).unwrap();
1353 let result = parser.parse_record();
1354
1355 assert!(result.is_err());
1356 match result {
1357 Err(LnmpError::TypeHintMismatch { field_id, .. }) => {
1358 assert_eq!(field_id, 3);
1359 }
1360 _ => panic!("Expected TypeHintMismatch error"),
1361 }
1362 }
1363
1364 #[test]
1365 fn test_invalid_type_hint() {
1366 let input = "F1:xyz=42"; let mut parser = Parser::new(input).unwrap();
1368 let result = parser.parse_record();
1369
1370 assert!(result.is_err());
1371 match result {
1372 Err(LnmpError::InvalidTypeHint { hint, .. }) => {
1373 assert_eq!(hint, "xyz");
1374 }
1375 _ => panic!("Expected InvalidTypeHint error"),
1376 }
1377 }
1378
1379 #[test]
1380 fn test_field_without_type_hint_still_works() {
1381 let input = "F1=42\nF2:i=100";
1382 let mut parser = Parser::new(input).unwrap();
1383 let record = parser.parse_record().unwrap();
1384
1385 assert_eq!(record.fields().len(), 2);
1386 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1387 assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Int(100));
1388 }
1389
1390 #[test]
1391 fn test_type_hint_with_whitespace() {
1392 let input = "F12 :i =14532";
1393 let mut parser = Parser::new(input).unwrap();
1394 let record = parser.parse_record().unwrap();
1395
1396 assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
1397 }
1398
1399 #[test]
1400 fn test_comment_lines_ignored_in_loose_mode() {
1401 let input = "# This is a comment\nF1=42\n# Another comment\nF2=100";
1402 let mut parser = Parser::new(input).unwrap();
1403 let record = parser.parse_record().unwrap();
1404
1405 assert_eq!(record.fields().len(), 2);
1406 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1407 assert_eq!(record.get_field(2).unwrap().value, LnmpValue::Int(100));
1408 }
1409
1410 #[test]
1411 fn test_hash_in_quoted_string_preserved() {
1412 let input = r#"F1="This # is not a comment""#;
1413 let mut parser = Parser::new(input).unwrap();
1414 let record = parser.parse_record().unwrap();
1415
1416 assert_eq!(
1417 record.get_field(1).unwrap().value,
1418 LnmpValue::String("This # is not a comment".to_string())
1419 );
1420 }
1421
1422 #[test]
1423 fn test_comment_after_whitespace() {
1424 let input = " # Comment with leading whitespace\nF1=42";
1425 let mut parser = Parser::new(input).unwrap();
1426 let record = parser.parse_record().unwrap();
1427
1428 assert_eq!(record.fields().len(), 1);
1429 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1430 }
1431
1432 #[test]
1433 fn test_inline_comments_not_supported() {
1434 let input = "F1=test"; let mut parser = Parser::new(input).unwrap();
1438 let record = parser.parse_record().unwrap();
1439
1440 assert_eq!(
1441 record.get_field(1).unwrap().value,
1442 LnmpValue::String("test".to_string())
1443 );
1444 }
1445
1446 #[test]
1447 fn test_encoder_never_outputs_comments() {
1448 use crate::encoder::Encoder;
1449
1450 let mut record = LnmpRecord::new();
1451 record.add_field(LnmpField {
1452 fid: 1,
1453 value: LnmpValue::String("test".to_string()),
1454 });
1455
1456 let encoder = Encoder::new();
1457 let output = encoder.encode(&record);
1458
1459 assert!(!output.contains('#'));
1461 assert_eq!(output, "F1=test");
1462 }
1463
1464 #[test]
1465 fn test_multiple_comment_lines() {
1466 let input = "# Comment 1\n# Comment 2\n# Comment 3\nF1=42";
1467 let mut parser = Parser::new(input).unwrap();
1468 let record = parser.parse_record().unwrap();
1469
1470 assert_eq!(record.fields().len(), 1);
1471 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1472 }
1473
1474 #[test]
1475 fn test_comment_at_end_of_file() {
1476 let input = "F1=42\n# Comment at end";
1477 let mut parser = Parser::new(input).unwrap();
1478 let record = parser.parse_record().unwrap();
1479
1480 assert_eq!(record.fields().len(), 1);
1481 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1482 }
1483
1484 #[test]
1485 fn test_empty_comment_line() {
1486 let input = "#\nF1=42";
1487 let mut parser = Parser::new(input).unwrap();
1488 let record = parser.parse_record().unwrap();
1489
1490 assert_eq!(record.fields().len(), 1);
1491 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1492 }
1493
1494 #[test]
1496 fn test_parse_simple_nested_record() {
1497 let input = "F50={F12=1;F7=1}";
1498 let mut parser = Parser::new(input).unwrap();
1499 let record = parser.parse_record().unwrap();
1500
1501 assert_eq!(record.fields().len(), 1);
1502 let field = record.get_field(50).unwrap();
1503
1504 match &field.value {
1505 LnmpValue::NestedRecord(nested) => {
1506 assert_eq!(nested.fields().len(), 2);
1507 assert_eq!(nested.fields()[0].fid, 7);
1509 assert_eq!(nested.fields()[0].value, LnmpValue::Bool(true));
1510 assert_eq!(nested.fields()[1].fid, 12);
1511 assert_eq!(nested.fields()[1].value, LnmpValue::Bool(true));
1512 }
1513 _ => panic!("Expected NestedRecord"),
1514 }
1515 }
1516
1517 #[test]
1518 fn test_parse_nested_record_with_various_types() {
1519 let input = r#"F50={F12=14532;F7=1;F23=["admin","dev"]}"#;
1520 let mut parser = Parser::new(input).unwrap();
1521 let record = parser.parse_record().unwrap();
1522
1523 assert_eq!(record.fields().len(), 1);
1524 let field = record.get_field(50).unwrap();
1525
1526 match &field.value {
1527 LnmpValue::NestedRecord(nested) => {
1528 assert_eq!(nested.fields().len(), 3);
1529 assert_eq!(nested.get_field(7).unwrap().value, LnmpValue::Bool(true));
1531 assert_eq!(nested.get_field(12).unwrap().value, LnmpValue::Int(14532));
1532 assert_eq!(
1533 nested.get_field(23).unwrap().value,
1534 LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()])
1535 );
1536 }
1537 _ => panic!("Expected NestedRecord"),
1538 }
1539 }
1540
1541 #[test]
1542 fn test_parse_empty_nested_record() {
1543 let input = "F50={}";
1544 let mut parser = Parser::new(input).unwrap();
1545 let record = parser.parse_record().unwrap();
1546
1547 assert_eq!(record.fields().len(), 1);
1548 let field = record.get_field(50).unwrap();
1549
1550 match &field.value {
1551 LnmpValue::NestedRecord(nested) => {
1552 assert_eq!(nested.fields().len(), 0);
1553 }
1554 _ => panic!("Expected NestedRecord"),
1555 }
1556 }
1557
1558 #[test]
1559 fn test_parse_deeply_nested_record() {
1560 let input = "F100={F1=user;F2={F10=nested;F11=data}}";
1561 let mut parser = Parser::new(input).unwrap();
1562 let record = parser.parse_record().unwrap();
1563
1564 assert_eq!(record.fields().len(), 1);
1565 let field = record.get_field(100).unwrap();
1566
1567 match &field.value {
1568 LnmpValue::NestedRecord(nested) => {
1569 assert_eq!(nested.fields().len(), 2);
1570 assert_eq!(
1571 nested.get_field(1).unwrap().value,
1572 LnmpValue::String("user".to_string())
1573 );
1574
1575 match &nested.get_field(2).unwrap().value {
1577 LnmpValue::NestedRecord(inner) => {
1578 assert_eq!(inner.fields().len(), 2);
1579 assert_eq!(
1580 inner.get_field(10).unwrap().value,
1581 LnmpValue::String("nested".to_string())
1582 );
1583 assert_eq!(
1584 inner.get_field(11).unwrap().value,
1585 LnmpValue::String("data".to_string())
1586 );
1587 }
1588 _ => panic!("Expected nested NestedRecord"),
1589 }
1590 }
1591 _ => panic!("Expected NestedRecord"),
1592 }
1593 }
1594
1595 #[test]
1596 fn test_parse_nested_record_fields_sorted() {
1597 let input = "F50={F12=1;F7=0;F23=test}";
1599 let mut parser = Parser::new(input).unwrap();
1600 let record = parser.parse_record().unwrap();
1601
1602 let field = record.get_field(50).unwrap();
1603 match &field.value {
1604 LnmpValue::NestedRecord(nested) => {
1605 assert_eq!(nested.fields()[0].fid, 7);
1607 assert_eq!(nested.fields()[1].fid, 12);
1608 assert_eq!(nested.fields()[2].fid, 23);
1609 }
1610 _ => panic!("Expected NestedRecord"),
1611 }
1612 }
1613
1614 #[test]
1615 fn test_parse_nested_record_with_trailing_semicolon() {
1616 let input = "F50={F12=1;F7=1;}";
1617 let mut parser = Parser::new(input).unwrap();
1618 let record = parser.parse_record().unwrap();
1619
1620 assert_eq!(record.fields().len(), 1);
1621 let field = record.get_field(50).unwrap();
1622
1623 match &field.value {
1624 LnmpValue::NestedRecord(nested) => {
1625 assert_eq!(nested.fields().len(), 2);
1626 }
1627 _ => panic!("Expected NestedRecord"),
1628 }
1629 }
1630
1631 #[test]
1632 fn test_parse_nested_array_basic() {
1633 let input = "F60=[{F12=1},{F12=2},{F12=3}]";
1634 let mut parser = Parser::new(input).unwrap();
1635 let record = parser.parse_record().unwrap();
1636
1637 assert_eq!(record.fields().len(), 1);
1638 let field = record.get_field(60).unwrap();
1639
1640 match &field.value {
1641 LnmpValue::NestedArray(records) => {
1642 assert_eq!(records.len(), 3);
1643 assert_eq!(
1644 records[0].get_field(12).unwrap().value,
1645 LnmpValue::Bool(true)
1646 );
1647 assert_eq!(records[1].get_field(12).unwrap().value, LnmpValue::Int(2));
1648 assert_eq!(records[2].get_field(12).unwrap().value, LnmpValue::Int(3));
1649 }
1650 _ => panic!("Expected NestedArray"),
1651 }
1652 }
1653
1654 #[test]
1655 fn test_parse_nested_array_with_multiple_fields() {
1656 let input = "F200=[{F1=alice;F2=admin},{F1=bob;F2=user}]";
1657 let mut parser = Parser::new(input).unwrap();
1658 let record = parser.parse_record().unwrap();
1659
1660 assert_eq!(record.fields().len(), 1);
1661 let field = record.get_field(200).unwrap();
1662
1663 match &field.value {
1664 LnmpValue::NestedArray(records) => {
1665 assert_eq!(records.len(), 2);
1666
1667 assert_eq!(
1669 records[0].get_field(1).unwrap().value,
1670 LnmpValue::String("alice".to_string())
1671 );
1672 assert_eq!(
1673 records[0].get_field(2).unwrap().value,
1674 LnmpValue::String("admin".to_string())
1675 );
1676
1677 assert_eq!(
1679 records[1].get_field(1).unwrap().value,
1680 LnmpValue::String("bob".to_string())
1681 );
1682 assert_eq!(
1683 records[1].get_field(2).unwrap().value,
1684 LnmpValue::String("user".to_string())
1685 );
1686 }
1687 _ => panic!("Expected NestedArray"),
1688 }
1689 }
1690
1691 #[test]
1692 fn test_parse_empty_nested_array() {
1693 let input = "F60=[]";
1694 let mut parser = Parser::new(input).unwrap();
1695 let record = parser.parse_record().unwrap();
1696
1697 assert_eq!(record.fields().len(), 1);
1698 let field = record.get_field(60).unwrap();
1699
1700 match &field.value {
1702 LnmpValue::StringArray(items) => {
1703 assert_eq!(items.len(), 0);
1704 }
1705 _ => panic!("Expected StringArray for empty array"),
1706 }
1707 }
1708
1709 #[test]
1710 fn test_parse_empty_nested_array_with_type_hint() {
1711 let input = "F60:ra=[]";
1712 let mut parser = Parser::new(input).unwrap();
1713 let record = parser.parse_record().unwrap();
1714
1715 assert_eq!(record.fields().len(), 1);
1716 let field = record.get_field(60).unwrap();
1717
1718 match &field.value {
1720 LnmpValue::NestedArray(records) => {
1721 assert_eq!(records.len(), 0);
1722 }
1723 _ => panic!("Expected NestedArray for empty array with :ra type hint"),
1724 }
1725 }
1726
1727 #[test]
1728 fn test_parse_nested_record_with_type_hints() {
1729 let input = "F50:r={F12:i=14532;F7:b=1}";
1730 let mut parser = Parser::new(input).unwrap();
1731 let record = parser.parse_record().unwrap();
1732
1733 assert_eq!(record.fields().len(), 1);
1734 let field = record.get_field(50).unwrap();
1735
1736 match &field.value {
1737 LnmpValue::NestedRecord(nested) => {
1738 assert_eq!(nested.fields().len(), 2);
1739 assert_eq!(nested.get_field(7).unwrap().value, LnmpValue::Bool(true));
1740 assert_eq!(nested.get_field(12).unwrap().value, LnmpValue::Int(14532));
1741 }
1742 _ => panic!("Expected NestedRecord"),
1743 }
1744 }
1745
1746 #[test]
1747 fn test_parse_nested_array_with_type_hint() {
1748 let input = "F60:ra=[{F12=1},{F12=2}]";
1749 let mut parser = Parser::new(input).unwrap();
1750 let record = parser.parse_record().unwrap();
1751
1752 assert_eq!(record.fields().len(), 1);
1753 let field = record.get_field(60).unwrap();
1754
1755 match &field.value {
1756 LnmpValue::NestedArray(records) => {
1757 assert_eq!(records.len(), 2);
1758 }
1759 _ => panic!("Expected NestedArray"),
1760 }
1761 }
1762
1763 #[test]
1764 fn test_parse_nested_array_preserves_order() {
1765 let input = "F60=[{F1=first},{F1=second},{F1=third}]";
1767 let mut parser = Parser::new(input).unwrap();
1768 let record = parser.parse_record().unwrap();
1769
1770 let field = record.get_field(60).unwrap();
1771 match &field.value {
1772 LnmpValue::NestedArray(records) => {
1773 assert_eq!(records.len(), 3);
1774 assert_eq!(
1775 records[0].get_field(1).unwrap().value,
1776 LnmpValue::String("first".to_string())
1777 );
1778 assert_eq!(
1779 records[1].get_field(1).unwrap().value,
1780 LnmpValue::String("second".to_string())
1781 );
1782 assert_eq!(
1783 records[2].get_field(1).unwrap().value,
1784 LnmpValue::String("third".to_string())
1785 );
1786 }
1787 _ => panic!("Expected NestedArray"),
1788 }
1789 }
1790
1791 #[test]
1792 fn test_parse_nested_array_with_complex_records() {
1793 let input = r#"F100=[{F1=alice;F2=30;F3=1},{F1=bob;F2=25;F3=0}]"#;
1795 let mut parser = Parser::new(input).unwrap();
1796 let record = parser.parse_record().unwrap();
1797
1798 let field = record.get_field(100).unwrap();
1799 match &field.value {
1800 LnmpValue::NestedArray(records) => {
1801 assert_eq!(records.len(), 2);
1802
1803 let rec1 = &records[0];
1805 assert_eq!(
1806 rec1.get_field(1).unwrap().value,
1807 LnmpValue::String("alice".to_string())
1808 );
1809 assert_eq!(rec1.get_field(2).unwrap().value, LnmpValue::Int(30));
1810 assert_eq!(rec1.get_field(3).unwrap().value, LnmpValue::Bool(true));
1811
1812 let rec2 = &records[1];
1814 assert_eq!(
1815 rec2.get_field(1).unwrap().value,
1816 LnmpValue::String("bob".to_string())
1817 );
1818 assert_eq!(rec2.get_field(2).unwrap().value, LnmpValue::Int(25));
1819 assert_eq!(rec2.get_field(3).unwrap().value, LnmpValue::Bool(false));
1820 }
1821 _ => panic!("Expected NestedArray"),
1822 }
1823 }
1824
1825 #[test]
1826 fn test_parse_nested_array_single_element() {
1827 let input = "F60=[{F1=only}]";
1828 let mut parser = Parser::new(input).unwrap();
1829 let record = parser.parse_record().unwrap();
1830
1831 let field = record.get_field(60).unwrap();
1832 match &field.value {
1833 LnmpValue::NestedArray(records) => {
1834 assert_eq!(records.len(), 1);
1835 assert_eq!(
1836 records[0].get_field(1).unwrap().value,
1837 LnmpValue::String("only".to_string())
1838 );
1839 }
1840 _ => panic!("Expected NestedArray"),
1841 }
1842 }
1843
1844 #[test]
1845 fn test_parse_nested_array_with_all_value_types() {
1846 let input = r#"F100=[{F1=42;F2=3.14;F3=1;F4=test;F5=["a","b"]}]"#;
1848 let mut parser = Parser::new(input).unwrap();
1849 let record = parser.parse_record().unwrap();
1850
1851 let field = record.get_field(100).unwrap();
1852 match &field.value {
1853 LnmpValue::NestedArray(records) => {
1854 assert_eq!(records.len(), 1);
1855 let rec = &records[0];
1856
1857 assert_eq!(rec.get_field(1).unwrap().value, LnmpValue::Int(42));
1859 assert_eq!(rec.get_field(2).unwrap().value, LnmpValue::Float(3.14));
1860 assert_eq!(rec.get_field(3).unwrap().value, LnmpValue::Bool(true));
1861 assert_eq!(
1862 rec.get_field(4).unwrap().value,
1863 LnmpValue::String("test".to_string())
1864 );
1865 assert_eq!(
1866 rec.get_field(5).unwrap().value,
1867 LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()])
1868 );
1869 }
1870 _ => panic!("Expected NestedArray"),
1871 }
1872 }
1873
1874 #[test]
1875 fn test_parse_mixed_top_level_and_nested() {
1876 let input = "F1=42;F50={F12=1;F7=1};F100=test";
1877 let mut parser = Parser::new(input).unwrap();
1878 let record = parser.parse_record().unwrap();
1879
1880 assert_eq!(record.fields().len(), 3);
1881 assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(42));
1882 assert_eq!(
1883 record.get_field(100).unwrap().value,
1884 LnmpValue::String("test".to_string())
1885 );
1886
1887 match &record.get_field(50).unwrap().value {
1888 LnmpValue::NestedRecord(nested) => {
1889 assert_eq!(nested.fields().len(), 2);
1890 }
1891 _ => panic!("Expected NestedRecord"),
1892 }
1893 }
1894
1895 #[test]
1896 fn test_parse_three_level_nesting() {
1897 let input = "F1={F2={F3={F4=deep}}}";
1898 let mut parser = Parser::new(input).unwrap();
1899 let record = parser.parse_record().unwrap();
1900
1901 assert_eq!(record.fields().len(), 1);
1902
1903 match &record.get_field(1).unwrap().value {
1905 LnmpValue::NestedRecord(level1) => {
1906 match &level1.get_field(2).unwrap().value {
1908 LnmpValue::NestedRecord(level2) => {
1909 match &level2.get_field(3).unwrap().value {
1911 LnmpValue::NestedRecord(level3) => {
1912 assert_eq!(
1913 level3.get_field(4).unwrap().value,
1914 LnmpValue::String("deep".to_string())
1915 );
1916 }
1917 _ => panic!("Expected NestedRecord at level 3"),
1918 }
1919 }
1920 _ => panic!("Expected NestedRecord at level 2"),
1921 }
1922 }
1923 _ => panic!("Expected NestedRecord at level 1"),
1924 }
1925 }
1926
1927 #[test]
1928 fn test_nesting_too_deep_error() {
1929 use crate::config::ParserConfig;
1930
1931 let input = "F1={F2={F3={F4={F5={F6={F7={F8={F9={F10={F11={F12=1}}}}}}}}}}}}";
1932 let config = ParserConfig {
1933 max_nesting_depth: Some(10),
1934 ..Default::default()
1935 };
1936 let mut parser = Parser::with_config(input, config).unwrap();
1937 let result = parser.parse_record();
1938 assert!(result.is_err());
1939 match result {
1940 Err(LnmpError::NestingTooDeep {
1941 max_depth,
1942 actual_depth,
1943 ..
1944 }) => {
1945 assert_eq!(max_depth, 10);
1946 assert!(actual_depth > max_depth);
1947 }
1948 _ => panic!("Expected NestingTooDeep error"),
1949 }
1950 }
1951
1952 #[test]
1953 fn test_structural_limits_rejects_field_count() {
1954 use crate::config::ParserConfig;
1955 use lnmp_core::StructuralLimits;
1956
1957 let input = "F1=1\nF2=2";
1958 let config = ParserConfig {
1959 structural_limits: Some(StructuralLimits {
1960 max_fields: 1,
1961 ..Default::default()
1962 }),
1963 ..Default::default()
1964 };
1965
1966 let mut parser = Parser::with_config(input, config).unwrap();
1967 let result = parser.parse_record();
1968 match result {
1969 Err(LnmpError::InvalidNestedStructure { reason, .. }) => {
1970 assert!(reason.contains("maximum field count exceeded"));
1971 }
1972 _ => panic!("Expected InvalidNestedStructure due to structural limits"),
1973 }
1974 }
1975
1976 #[test]
1978 fn test_parse_field_with_checksum_ignored() {
1979 use lnmp_core::checksum::SemanticChecksum;
1980
1981 let value = LnmpValue::Int(14532);
1983 let checksum = SemanticChecksum::compute(12, None, &value);
1984 let checksum_str = SemanticChecksum::format(checksum);
1985
1986 let input = format!("F12=14532#{}", checksum_str);
1987
1988 let mut parser = Parser::new(&input).unwrap();
1990 let record = parser.parse_record().unwrap();
1991
1992 assert_eq!(record.fields().len(), 1);
1993 assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
1994 }
1995
1996 #[test]
1997 fn test_parse_field_with_valid_checksum() {
1998 use crate::config::ParserConfig;
1999 use lnmp_core::checksum::SemanticChecksum;
2000
2001 let value = LnmpValue::Int(14532);
2003 let checksum = SemanticChecksum::compute(12, None, &value);
2004 let checksum_str = SemanticChecksum::format(checksum);
2005
2006 let input = format!("F12=14532#{}", checksum_str);
2007
2008 let config = ParserConfig {
2010 validate_checksums: true,
2011 ..Default::default()
2012 };
2013 let mut parser = Parser::with_config(&input, config).unwrap();
2014 let record = parser.parse_record().unwrap();
2015
2016 assert_eq!(record.fields().len(), 1);
2017 assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
2018 }
2019
2020 #[test]
2021 fn test_parse_field_with_invalid_checksum() {
2022 use crate::config::ParserConfig;
2023
2024 let input = "F12=14532#DEADBEEF";
2026
2027 let config = ParserConfig {
2029 validate_checksums: true,
2030 ..Default::default()
2031 };
2032 let mut parser = Parser::with_config(input, config).unwrap();
2033 let result = parser.parse_record();
2034
2035 assert!(result.is_err());
2036 match result {
2037 Err(LnmpError::ChecksumMismatch { field_id, .. }) => {
2038 assert_eq!(field_id, 12);
2039 }
2040 _ => panic!("Expected ChecksumMismatch error"),
2041 }
2042 }
2043
2044 #[test]
2045 fn test_parse_field_with_checksum_and_type_hint() {
2046 use crate::config::ParserConfig;
2047 use lnmp_core::checksum::SemanticChecksum;
2048
2049 let value = LnmpValue::Int(14532);
2051 let checksum = SemanticChecksum::compute(12, Some(TypeHint::Int), &value);
2052 let checksum_str = SemanticChecksum::format(checksum);
2053
2054 let input = format!("F12:i=14532#{}", checksum_str);
2055
2056 let config = ParserConfig {
2058 validate_checksums: true,
2059 ..Default::default()
2060 };
2061 let mut parser = Parser::with_config(&input, config).unwrap();
2062 let record = parser.parse_record().unwrap();
2063
2064 assert_eq!(record.fields().len(), 1);
2065 assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
2066 }
2067
2068 #[test]
2069 fn test_parse_applies_semantic_dictionary_equivalence() {
2070 use crate::config::ParserConfig;
2071
2072 let mut dict = lnmp_sfe::SemanticDictionary::new();
2073 dict.add_equivalence(23, "admin".to_string(), "administrator".to_string());
2074
2075 let config = ParserConfig {
2076 semantic_dictionary: Some(dict),
2077 ..Default::default()
2078 };
2079
2080 let mut parser = Parser::with_config("F23=[admin]", config).unwrap();
2081 let record = parser.parse_record().unwrap();
2082 match record.get_field(23).unwrap().value.clone() {
2083 LnmpValue::StringArray(vals) => {
2084 assert_eq!(vals, vec!["administrator".to_string()]);
2085 }
2086 other => panic!("unexpected value {:?}", other),
2087 }
2088 }
2089
2090 #[test]
2091 fn test_parse_multiple_fields_with_checksums() {
2092 use crate::config::ParserConfig;
2093 use lnmp_core::checksum::SemanticChecksum;
2094
2095 let value1 = LnmpValue::Int(14532);
2096 let checksum1 = SemanticChecksum::compute(12, None, &value1);
2097 let checksum_str1 = SemanticChecksum::format(checksum1);
2098
2099 let value2 = LnmpValue::Bool(true);
2100 let checksum2 = SemanticChecksum::compute(7, None, &value2);
2101 let checksum_str2 = SemanticChecksum::format(checksum2);
2102
2103 let input = format!("F12=14532#{}\nF7=1#{}", checksum_str1, checksum_str2);
2104
2105 let config = ParserConfig {
2107 validate_checksums: true,
2108 ..Default::default()
2109 };
2110 let mut parser = Parser::with_config(&input, config).unwrap();
2111 let record = parser.parse_record().unwrap();
2112
2113 assert_eq!(record.fields().len(), 2);
2114 assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
2115 assert_eq!(record.get_field(7).unwrap().value, LnmpValue::Bool(true));
2116 }
2117
2118 #[test]
2119 fn test_parse_field_require_checksum_missing() {
2120 use crate::config::ParserConfig;
2121
2122 let input = "F12=14532";
2123
2124 let config = ParserConfig {
2126 require_checksums: true,
2127 ..Default::default()
2128 };
2129 let mut parser = Parser::with_config(input, config).unwrap();
2130 let result = parser.parse_record();
2131
2132 assert!(result.is_err());
2133 match result {
2134 Err(LnmpError::ChecksumMismatch { field_id, .. }) => {
2135 assert_eq!(field_id, 12);
2136 }
2137 _ => panic!("Expected ChecksumMismatch error"),
2138 }
2139 }
2140
2141 #[test]
2142 fn test_parse_field_with_checksum_after_comment() {
2143 use crate::config::ParserConfig;
2144 use lnmp_core::checksum::SemanticChecksum;
2145
2146 let value = LnmpValue::Int(14532);
2147 let checksum = SemanticChecksum::compute(12, None, &value);
2148 let checksum_str = SemanticChecksum::format(checksum);
2149
2150 let input = format!("# Comment\nF12=14532#{}", checksum_str);
2151
2152 let config = ParserConfig {
2154 validate_checksums: true,
2155 ..Default::default()
2156 };
2157 let mut parser = Parser::with_config(&input, config).unwrap();
2158 let record = parser.parse_record().unwrap();
2159
2160 assert_eq!(record.fields().len(), 1);
2161 assert_eq!(record.get_field(12).unwrap().value, LnmpValue::Int(14532));
2162 }
2163
2164 #[test]
2165 fn test_round_trip_with_checksum() {
2166 use crate::config::{EncoderConfig, ParserConfig};
2167 use crate::encoder::Encoder;
2168
2169 let mut record = LnmpRecord::new();
2170 record.add_field(LnmpField {
2171 fid: 12,
2172 value: LnmpValue::Int(14532),
2173 });
2174
2175 let encoder_config = EncoderConfig {
2177 enable_checksums: true,
2178 ..Default::default()
2179 };
2180 let encoder = Encoder::with_config(encoder_config);
2181 let output = encoder.encode(&record);
2182
2183 let parser_config = ParserConfig {
2185 validate_checksums: true,
2186 ..Default::default()
2187 };
2188 let mut parser = Parser::with_config(&output, parser_config).unwrap();
2189 let parsed = parser.parse_record().unwrap();
2190
2191 assert_eq!(
2192 record.get_field(12).unwrap().value,
2193 parsed.get_field(12).unwrap().value
2194 );
2195 }
2196}