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