1use crate::error::HedlResult;
21use crate::errors::messages;
22use crate::lex::{is_valid_key_token, is_valid_type_name, strip_comment};
23use crate::limits::{Limits, TimeoutContext};
24use std::collections::BTreeMap;
25
26#[derive(Debug, Clone)]
28pub struct Header {
29 pub version: (u32, u32),
31 pub aliases: BTreeMap<String, String>,
33 pub structs: BTreeMap<String, Vec<String>>,
35 pub struct_counts: BTreeMap<String, usize>,
37 pub nests: BTreeMap<String, String>,
39}
40
41impl Header {
42 pub fn new(version: (u32, u32)) -> Self {
44 Self {
45 version,
46 aliases: BTreeMap::new(),
47 structs: BTreeMap::new(),
48 struct_counts: BTreeMap::new(),
49 nests: BTreeMap::new(),
50 }
51 }
52}
53
54pub fn parse_header(
58 lines: &[(usize, &str)],
59 limits: &Limits,
60 timeout_ctx: &TimeoutContext,
61) -> HedlResult<(Header, usize)> {
62 let mut version: Option<(u32, u32)> = None;
63 let mut aliases: BTreeMap<String, String> = BTreeMap::new();
64 let mut structs: BTreeMap<String, Vec<String>> = BTreeMap::new();
65 let mut struct_counts: BTreeMap<String, usize> = BTreeMap::new();
66 let mut nests: BTreeMap<String, String> = BTreeMap::new();
67 let mut first_directive = true;
68
69 for (idx, &(line_num, line)) in lines.iter().enumerate() {
70 if idx % 10_000 == 0 {
72 timeout_ctx.check_timeout(line_num)?;
73 }
74
75 let trimmed = line.trim();
76
77 if trimmed == "---" || trimmed.starts_with("--- ") || trimmed.starts_with("---#") {
79 if line.starts_with(' ') || line.starts_with('\t') {
81 return Err(messages::invalid_separator_whitespace(line_num));
82 }
83
84 if version.is_none() {
85 return Err(messages::missing_version_before_separator(line_num));
86 }
87
88 return Ok((
89 Header {
90 version: version.unwrap(),
91 aliases,
92 structs,
93 struct_counts,
94 nests,
95 },
96 idx + 1,
97 ));
98 }
99
100 if trimmed.is_empty() || trimmed.starts_with('#') {
102 continue;
103 }
104
105 if !trimmed.starts_with('%') {
107 return Err(messages::expected_directive(trimmed, line_num));
108 }
109
110 let colon_pos = trimmed
112 .find(':')
113 .ok_or_else(|| messages::directive_missing_colon(line_num))?;
114
115 let directive_name = &trimmed[..colon_pos];
116 let rest = &trimmed[colon_pos + 1..];
117
118 if !rest.starts_with(' ') {
120 return Err(messages::directive_missing_space_after_colon(line_num));
121 }
122
123 let payload = strip_comment(rest.trim_start());
124
125 match directive_name {
126 "%VERSION" => {
127 if !first_directive {
128 return Err(messages::version_not_first(line_num));
129 }
130 version = Some(parse_version(payload, line_num)?);
131 }
132 "%STRUCT" => {
133 let (type_name, columns, count) = parse_struct(payload, line_num, limits)?;
134 if let Some(existing) = structs.get(&type_name) {
135 if existing != &columns {
136 return Err(messages::struct_redefined(&type_name, line_num));
137 }
138 } else {
139 structs.insert(type_name.clone(), columns);
140 if let Some(c) = count {
141 struct_counts.insert(type_name, c);
142 }
143 }
144 }
145 "%ALIAS" => {
146 let (key, value) = parse_alias(payload, line_num)?;
147 if aliases.contains_key(&key) {
148 return Err(messages::alias_already_defined(&key, line_num));
149 }
150 if aliases.len() >= limits.max_aliases {
151 return Err(messages::too_many_aliases(
152 aliases.len(),
153 limits.max_aliases,
154 line_num,
155 ));
156 }
157 aliases.insert(key, value);
158 }
159 "%NEST" => {
160 let (parent, child) = parse_nest(payload, line_num, &structs)?;
161 if nests.contains_key(&parent) {
162 return Err(messages::nest_multiple_rules(&parent, line_num));
163 }
164 nests.insert(parent, child);
165 }
166 _ => {
167 return Err(messages::unknown_directive(directive_name, line_num));
168 }
169 }
170
171 first_directive = false;
172 }
173
174 Err(messages::missing_separator(
175 lines.last().map(|(n, _)| *n).unwrap_or(1),
176 ))
177}
178
179fn parse_version(payload: &str, line_num: usize) -> HedlResult<(u32, u32)> {
180 let parts: Vec<&str> = payload.split('.').collect();
181 if parts.len() != 2 {
182 return Err(messages::invalid_version_format(payload, line_num));
183 }
184
185 let major: u32 = parts[0]
186 .parse()
187 .map_err(|_| messages::invalid_major_version(parts[0], line_num))?;
188 let minor: u32 = parts[1]
189 .parse()
190 .map_err(|_| messages::invalid_minor_version(parts[1], line_num))?;
191
192 if (parts[0].len() > 1 && parts[0].starts_with('0'))
194 || (parts[1].len() > 1 && parts[1].starts_with('0'))
195 {
196 return Err(messages::version_leading_zeros(line_num));
197 }
198
199 Ok((major, minor))
200}
201
202fn parse_struct(
207 payload: &str,
208 line_num: usize,
209 limits: &Limits,
210) -> HedlResult<(String, Vec<String>, Option<usize>)> {
211 let colon_pos = payload
213 .find(':')
214 .ok_or_else(|| messages::struct_missing_colon(line_num))?;
215
216 let before_colon = payload[..colon_pos].trim();
217
218 let (type_name, count) = {
220 use crate::lex::count_hint::{parse_parenthesized_count, CountParsingConfig};
221 parse_parenthesized_count(before_colon, CountParsingConfig::STRUCT_HINT, line_num)?
222 };
223
224 if !is_valid_type_name(&type_name) {
225 return Err(messages::invalid_type_name(&type_name, line_num));
226 }
227
228 let columns_str = payload[colon_pos + 1..].trim();
229 let columns = parse_column_list(columns_str, line_num, limits)?;
230
231 Ok((type_name.to_string(), columns, count))
232}
233
234fn parse_column_list(s: &str, line_num: usize, limits: &Limits) -> HedlResult<Vec<String>> {
235 let s = s.trim();
236 if !s.starts_with('[') || !s.ends_with(']') {
237 return Err(messages::column_list_not_bracketed(line_num));
238 }
239
240 let inner = &s[1..s.len() - 1];
241 if inner.trim().is_empty() {
242 return Err(messages::column_list_empty(line_num));
243 }
244
245 let mut columns = Vec::new();
246 let mut seen = std::collections::HashSet::new();
247
248 for part in inner.split(',') {
249 let col = part.trim();
250 if col.is_empty() {
251 continue;
252 }
253
254 if !is_valid_key_token(col) {
255 return Err(messages::invalid_column_name(col, line_num));
256 }
257
258 if !seen.insert(col) {
259 return Err(messages::duplicate_column_name(col, line_num));
260 }
261
262 columns.push(col.to_string());
263 }
264
265 if columns.is_empty() {
266 return Err(messages::column_list_empty(line_num));
267 }
268
269 if columns.len() > limits.max_columns {
270 return Err(messages::too_many_columns(
271 columns.len(),
272 limits.max_columns,
273 line_num,
274 ));
275 }
276
277 Ok(columns)
278}
279
280fn parse_alias(payload: &str, line_num: usize) -> HedlResult<(String, String)> {
281 let colon_pos = payload
283 .find(':')
284 .ok_or_else(|| messages::alias_missing_colon(line_num))?;
285
286 let key_part = payload[..colon_pos].trim();
287 if !key_part.starts_with('%') {
288 return Err(messages::alias_key_missing_percent(line_num));
289 }
290
291 let key = &key_part[1..];
292 if !is_valid_key_token(key) {
293 return Err(messages::invalid_alias_key(key, line_num));
294 }
295
296 let value_part = payload[colon_pos + 1..].trim();
297 if !value_part.starts_with('"') || !value_part.ends_with('"') {
298 return Err(messages::alias_value_not_quoted(line_num));
299 }
300
301 let inner = &value_part[1..value_part.len() - 1];
303 let value = inner.replace("\"\"", "\"");
304
305 Ok((key.to_string(), value))
306}
307
308fn parse_nest(
309 payload: &str,
310 line_num: usize,
311 structs: &BTreeMap<String, Vec<String>>,
312) -> HedlResult<(String, String)> {
313 let parts: Vec<&str> = payload.split('>').collect();
315 if parts.len() != 2 {
316 return Err(messages::nest_invalid_syntax(line_num));
317 }
318
319 let parent = parts[0].trim();
320 let child = parts[1].trim();
321
322 if !is_valid_type_name(parent) {
323 return Err(messages::nest_invalid_parent_type(parent, line_num));
324 }
325
326 if !is_valid_type_name(child) {
327 return Err(messages::nest_invalid_child_type(child, line_num));
328 }
329
330 if !structs.contains_key(parent) {
331 return Err(messages::nest_parent_not_defined(parent, line_num));
332 }
333
334 if !structs.contains_key(child) {
335 return Err(messages::nest_child_not_defined(child, line_num));
336 }
337
338 Ok((parent.to_string(), child.to_string()))
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 fn make_lines(s: &str) -> Vec<(usize, &str)> {
346 s.lines().enumerate().map(|(i, l)| (i + 1, l)).collect()
347 }
348
349 fn default_limits() -> Limits {
350 Limits::default()
351 }
352
353 #[test]
356 fn test_parse_minimal_header() {
357 let input = "%VERSION: 1.0\n---";
358 let lines = make_lines(input);
359 let (header, _) =
360 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
361 assert_eq!(header.version, (1, 0));
362 }
363
364 #[test]
365 fn test_header_returns_body_start_index() {
366 let input = "%VERSION: 1.0\n---";
367 let lines = make_lines(input);
368 let (_, body_idx) =
369 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
370 assert_eq!(body_idx, 2); }
372
373 #[test]
374 fn test_header_with_comment() {
375 let input = "%VERSION: 1.0\n# This is a comment\n---";
376 let lines = make_lines(input);
377 let (header, _) =
378 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
379 assert_eq!(header.version, (1, 0));
380 }
381
382 #[test]
383 fn test_header_with_blank_lines() {
384 let input = "%VERSION: 1.0\n\n \n---";
385 let lines = make_lines(input);
386 let (header, _) =
387 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
388 assert_eq!(header.version, (1, 0));
389 }
390
391 #[test]
392 fn test_separator_with_comment() {
393 let input = "%VERSION: 1.0\n---# comment after separator";
394 let lines = make_lines(input);
395 let (header, _) =
396 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
397 assert_eq!(header.version, (1, 0));
398 }
399
400 #[test]
401 fn test_separator_with_space_comment() {
402 let input = "%VERSION: 1.0\n--- # comment";
403 let lines = make_lines(input);
404 let (header, _) =
405 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
406 assert_eq!(header.version, (1, 0));
407 }
408
409 #[test]
412 fn test_version_zero_zero() {
413 let input = "%VERSION: 0.0\n---";
414 let lines = make_lines(input);
415 let (header, _) =
416 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
417 assert_eq!(header.version, (0, 0));
418 }
419
420 #[test]
421 fn test_version_high_numbers() {
422 let input = "%VERSION: 999.999\n---";
423 let lines = make_lines(input);
424 let (header, _) =
425 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
426 assert_eq!(header.version, (999, 999));
427 }
428
429 #[test]
430 fn test_version_leading_zero_error() {
431 let input = "%VERSION: 01.0\n---";
432 let lines = make_lines(input);
433 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
434 assert!(result.is_err());
435 assert!(result.unwrap_err().message.contains("leading zeros"));
436 }
437
438 #[test]
439 fn test_version_minor_leading_zero_error() {
440 let input = "%VERSION: 1.01\n---";
441 let lines = make_lines(input);
442 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
443 assert!(result.is_err());
444 }
445
446 #[test]
447 fn test_version_invalid_format_error() {
448 let input = "%VERSION: 1\n---";
449 let lines = make_lines(input);
450 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
451 assert!(result.is_err());
452 assert!(result
453 .unwrap_err()
454 .message
455 .contains("invalid version format"));
456 }
457
458 #[test]
459 fn test_version_three_parts_error() {
460 let input = "%VERSION: 1.0.0\n---";
461 let lines = make_lines(input);
462 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
463 assert!(result.is_err());
464 }
465
466 #[test]
467 fn test_version_non_numeric_error() {
468 let input = "%VERSION: a.b\n---";
469 let lines = make_lines(input);
470 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
471 assert!(result.is_err());
472 assert!(result.unwrap_err().message.contains("invalid major"));
473 }
474
475 #[test]
476 fn test_version_not_first_error() {
477 let input = "%STRUCT: User: [id,name]\n%VERSION: 1.0\n---";
478 let lines = make_lines(input);
479 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
480 assert!(result.is_err());
481 assert!(result.unwrap_err().message.contains("must be the first"));
482 }
483
484 #[test]
487 fn test_parse_struct() {
488 let input = "%VERSION: 1.0\n%STRUCT: User: [id,name,email]\n---";
489 let lines = make_lines(input);
490 let (header, _) =
491 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
492 assert_eq!(
493 header.structs.get("User"),
494 Some(&vec![
495 "id".to_string(),
496 "name".to_string(),
497 "email".to_string()
498 ])
499 );
500 }
501
502 #[test]
503 fn test_parse_struct_single_column() {
504 let input = "%VERSION: 1.0\n%STRUCT: Point: [x]\n---";
505 let lines = make_lines(input);
506 let (header, _) =
507 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
508 assert_eq!(header.structs.get("Point"), Some(&vec!["x".to_string()]));
509 }
510
511 #[test]
512 fn test_parse_multiple_structs() {
513 let input = "%VERSION: 1.0\n%STRUCT: User: [id,name]\n%STRUCT: Post: [id,title]\n---";
514 let lines = make_lines(input);
515 let (header, _) =
516 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
517 assert!(header.structs.contains_key("User"));
518 assert!(header.structs.contains_key("Post"));
519 }
520
521 #[test]
522 fn test_struct_identical_redefinition_ok() {
523 let input = "%VERSION: 1.0\n%STRUCT: User: [id,name]\n%STRUCT: User: [id,name]\n---";
524 let lines = make_lines(input);
525 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
526 assert!(result.is_ok());
527 }
528
529 #[test]
530 fn test_struct_different_redefinition_error() {
531 let input = "%VERSION: 1.0\n%STRUCT: User: [id,name]\n%STRUCT: User: [id, email]\n---";
532 let lines = make_lines(input);
533 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
534 assert!(result.is_err());
535 assert!(result
536 .unwrap_err()
537 .message
538 .contains("redefined with different"));
539 }
540
541 #[test]
542 fn test_struct_invalid_type_name_error() {
543 let input = "%VERSION: 1.0\n%STRUCT: user: [id]\n---";
544 let lines = make_lines(input);
545 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
546 assert!(result.is_err());
547 assert!(result.unwrap_err().message.contains("invalid type name"));
548 }
549
550 #[test]
551 fn test_struct_invalid_column_name_error() {
552 let input = "%VERSION: 1.0\n%STRUCT: User: [Id]\n---";
553 let lines = make_lines(input);
554 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
555 assert!(result.is_err());
556 assert!(result.unwrap_err().message.contains("invalid column name"));
557 }
558
559 #[test]
560 fn test_struct_duplicate_column_error() {
561 let input = "%VERSION: 1.0\n%STRUCT: User: [id, name, id]\n---";
562 let lines = make_lines(input);
563 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
564 assert!(result.is_err());
565 assert!(result.unwrap_err().message.contains("duplicate column"));
566 }
567
568 #[test]
569 fn test_struct_empty_columns_error() {
570 let input = "%VERSION: 1.0\n%STRUCT: User: []\n---";
571 let lines = make_lines(input);
572 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
573 assert!(result.is_err());
574 assert!(result.unwrap_err().message.contains("cannot be empty"));
575 }
576
577 #[test]
578 fn test_struct_missing_brackets_error() {
579 let input = "%VERSION: 1.0\n%STRUCT: User: id, name\n---";
580 let lines = make_lines(input);
581 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
582 assert!(result.is_err());
583 assert!(result.unwrap_err().message.contains("enclosed in []"));
584 }
585
586 #[test]
587 fn test_struct_too_many_columns_error() {
588 let limits = Limits {
589 max_columns: 2,
590 ..Limits::default()
591 };
592 let input = "%VERSION: 1.0\n%STRUCT: User: [id,name,email]\n---";
593 let lines = make_lines(input);
594 let result = parse_header(&lines, &limits, &TimeoutContext::new(None));
595 assert!(result.is_err());
596 assert!(result.unwrap_err().message.contains("too many columns"));
597 }
598
599 #[test]
602 fn test_parse_alias() {
603 let input = "%VERSION: 1.0\n%ALIAS: %active: \"true\"\n---";
604 let lines = make_lines(input);
605 let (header, _) =
606 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
607 assert_eq!(header.aliases.get("active"), Some(&"true".to_string()));
608 }
609
610 #[test]
611 fn test_parse_alias_empty_value() {
612 let input = "%VERSION: 1.0\n%ALIAS: %empty: \"\"\n---";
613 let lines = make_lines(input);
614 let (header, _) =
615 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
616 assert_eq!(header.aliases.get("empty"), Some(&String::new()));
617 }
618
619 #[test]
620 fn test_parse_alias_escaped_quotes() {
621 let input = "%VERSION: 1.0\n%ALIAS: %quote: \"say \"\"hello\"\"\"\n---";
622 let lines = make_lines(input);
623 let (header, _) =
624 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
625 assert_eq!(
626 header.aliases.get("quote"),
627 Some(&"say \"hello\"".to_string())
628 );
629 }
630
631 #[test]
632 fn test_parse_multiple_aliases() {
633 let input = "%VERSION: 1.0\n%ALIAS: %a: \"1\"\n%ALIAS: %b: \"2\"\n---";
634 let lines = make_lines(input);
635 let (header, _) =
636 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
637 assert_eq!(header.aliases.get("a"), Some(&"1".to_string()));
638 assert_eq!(header.aliases.get("b"), Some(&"2".to_string()));
639 }
640
641 #[test]
642 fn test_alias_duplicate_error() {
643 let input = "%VERSION: 1.0\n%ALIAS: %key: \"a\"\n%ALIAS: %key: \"b\"\n---";
644 let lines = make_lines(input);
645 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
646 assert!(result.is_err());
647 assert!(result.unwrap_err().message.contains("already defined"));
648 }
649
650 #[test]
651 fn test_alias_missing_percent_error() {
652 let input = "%VERSION: 1.0\n%ALIAS: key: \"value\"\n---";
653 let lines = make_lines(input);
654 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
655 assert!(result.is_err());
656 assert!(result.unwrap_err().message.contains("must start with '%'"));
657 }
658
659 #[test]
660 fn test_alias_unquoted_value_error() {
661 let input = "%VERSION: 1.0\n%ALIAS: %key: value\n---";
662 let lines = make_lines(input);
663 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
664 assert!(result.is_err());
665 assert!(result.unwrap_err().message.contains("quoted string"));
666 }
667
668 #[test]
669 fn test_alias_too_many_error() {
670 let limits = Limits {
671 max_aliases: 1,
672 ..Limits::default()
673 };
674 let input = "%VERSION: 1.0\n%ALIAS: %a: \"1\"\n%ALIAS: %b: \"2\"\n---";
675 let lines = make_lines(input);
676 let result = parse_header(&lines, &limits, &TimeoutContext::new(None));
677 assert!(result.is_err());
678 assert!(result.unwrap_err().message.contains("too many aliases"));
679 }
680
681 #[test]
684 fn test_parse_nest() {
685 let input =
686 "%VERSION: 1.0\n%STRUCT: User: [id,name]\n%STRUCT: Post: [id,title]\n%NEST: User > Post\n---";
687 let lines = make_lines(input);
688 let (header, _) =
689 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
690 assert_eq!(header.nests.get("User"), Some(&"Post".to_string()));
691 }
692
693 #[test]
694 fn test_nest_undefined_parent_error() {
695 let input = "%VERSION: 1.0\n%STRUCT: Post: [id,title]\n%NEST: User > Post\n---";
696 let lines = make_lines(input);
697 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
698 assert!(result.is_err());
699 assert!(result.unwrap_err().message.contains("not defined"));
700 }
701
702 #[test]
703 fn test_nest_undefined_child_error() {
704 let input = "%VERSION: 1.0\n%STRUCT: User: [id,name]\n%NEST: User > Post\n---";
705 let lines = make_lines(input);
706 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
707 assert!(result.is_err());
708 assert!(result.unwrap_err().message.contains("not defined"));
709 }
710
711 #[test]
712 fn test_nest_multiple_for_parent_error() {
713 let input = "%VERSION: 1.0\n%STRUCT: A: [id]\n%STRUCT: B: [id]\n%STRUCT: C: [id]\n%NEST: A > B\n%NEST: A > C\n---";
714 let lines = make_lines(input);
715 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
716 assert!(result.is_err());
717 assert!(result.unwrap_err().message.contains("multiple NEST rules"));
718 }
719
720 #[test]
721 fn test_nest_invalid_format_error() {
722 let input = "%VERSION: 1.0\n%STRUCT: User: [id,name]\n%NEST: User\n---";
723 let lines = make_lines(input);
724 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
725 assert!(result.is_err());
726 assert!(result.unwrap_err().message.contains("Parent > Child"));
727 }
728
729 #[test]
730 fn test_nest_invalid_parent_type_name_error() {
731 let input =
732 "%VERSION: 1.0\n%STRUCT: User: [id,name]\n%STRUCT: Post: [id,title]\n%NEST: user > Post\n---";
733 let lines = make_lines(input);
734 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
735 assert!(result.is_err());
736 assert!(result.unwrap_err().message.contains("invalid parent type"));
737 }
738
739 #[test]
742 fn test_missing_version_error() {
743 let input = "%STRUCT: User: [id,name]\n---";
744 let lines = make_lines(input);
745 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
746 assert!(result.is_err());
747 assert!(result.unwrap_err().message.contains("VERSION"));
748 }
749
750 #[test]
751 fn test_missing_separator_error() {
752 let input = "%VERSION: 1.0\na: 1";
753 let lines = make_lines(input);
754 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
755 assert!(result.is_err());
756 assert!(result.unwrap_err().message.contains("directive"));
758 }
759
760 #[test]
761 fn test_indented_separator_error() {
762 let input = "%VERSION: 1.0\n ---";
763 let lines = make_lines(input);
764 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
765 assert!(result.is_err());
766 assert!(result.unwrap_err().message.contains("leading whitespace"));
767 }
768
769 #[test]
770 fn test_unknown_directive_error() {
771 let input = "%VERSION: 1.0\n%UNKNOWN: foo\n---";
772 let lines = make_lines(input);
773 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
774 assert!(result.is_err());
775 assert!(result.unwrap_err().message.contains("unknown directive"));
776 }
777
778 #[test]
779 fn test_directive_missing_colon_error() {
780 let input = "%VERSION 1.0\n---";
781 let lines = make_lines(input);
782 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
783 assert!(result.is_err());
784 assert!(result.unwrap_err().message.contains("missing ':'"));
785 }
786
787 #[test]
788 fn test_directive_missing_space_after_colon_error() {
789 let input = "%VERSION:1.0\n---";
790 let lines = make_lines(input);
791 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
792 assert!(result.is_err());
793 assert!(result.unwrap_err().message.contains("followed by space"));
794 }
795
796 #[test]
797 fn test_non_directive_in_header_error() {
798 let input = "%VERSION: 1.0\nsome text\n---";
799 let lines = make_lines(input);
800 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
801 assert!(result.is_err());
802 assert!(result.unwrap_err().message.contains("expected directive"));
803 }
804
805 #[test]
808 fn test_header_clone() {
809 let input = "%VERSION: 1.0\n%ALIAS: %x: \"1\"\n%STRUCT: User: [id,name]\n---";
810 let lines = make_lines(input);
811 let (header, _) =
812 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
813 let cloned = header.clone();
814 assert_eq!(cloned.version, header.version);
815 assert_eq!(cloned.aliases, header.aliases);
816 assert_eq!(cloned.structs, header.structs);
817 }
818
819 #[test]
820 fn test_header_debug() {
821 let input = "%VERSION: 1.0\n---";
822 let lines = make_lines(input);
823 let (header, _) =
824 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
825 let debug = format!("{:?}", header);
826 assert!(debug.contains("version"));
827 assert!(debug.contains("aliases"));
828 }
829
830 #[test]
833 fn test_empty_input() {
834 let lines: Vec<(usize, &str)> = vec![];
835 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
836 assert!(result.is_err());
837 }
838
839 #[test]
840 fn test_comment_with_directive() {
841 let input = "%VERSION: 1.0 # version comment\n---";
842 let lines = make_lines(input);
843 let (header, _) =
844 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
845 assert_eq!(header.version, (1, 0));
846 }
847
848 #[test]
849 fn test_struct_with_comment() {
850 let input = "%VERSION: 1.0\n%STRUCT: User: [id,name] # columns\n---";
851 let lines = make_lines(input);
852 let (header, _) =
853 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
854 assert!(header.structs.contains_key("User"));
855 }
856
857 #[test]
858 fn test_all_directives_combined() {
859 let input = "%VERSION: 1.0\n%STRUCT: User: [id,name]\n%STRUCT: Post: [id,title]\n%ALIAS: %active: \"true\"\n%NEST: User > Post\n---";
860 let lines = make_lines(input);
861 let (header, _) =
862 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
863 assert_eq!(header.version, (1, 0));
864 assert_eq!(header.structs.len(), 2);
865 assert_eq!(header.aliases.len(), 1);
866 assert_eq!(header.nests.len(), 1);
867 }
868
869 #[test]
872 fn test_struct_with_count() {
873 let input = "%VERSION: 1.0\n%STRUCT: Company (1): [id, name, founded, industry]\n---";
874 let lines = make_lines(input);
875 let (header, _) =
876 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
877 assert_eq!(
878 header.structs.get("Company"),
879 Some(&vec![
880 "id".to_string(),
881 "name".to_string(),
882 "founded".to_string(),
883 "industry".to_string()
884 ])
885 );
886 }
887
888 #[test]
889 fn test_struct_with_higher_count() {
890 let input = "%VERSION: 1.0\n%STRUCT: Division (3): [id, name, head, budget]\n---";
891 let lines = make_lines(input);
892 let (header, _) =
893 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
894 assert_eq!(
895 header.structs.get("Division"),
896 Some(&vec![
897 "id".to_string(),
898 "name".to_string(),
899 "head".to_string(),
900 "budget".to_string()
901 ])
902 );
903 }
904
905 #[test]
906 fn test_struct_with_zero_count() {
907 let input = "%VERSION: 1.0\n%STRUCT: Empty (0): [id]\n---";
908 let lines = make_lines(input);
909 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
910 assert!(result.is_ok());
911 let (header, _) = result.unwrap();
912 assert_eq!(header.structs.get("Empty"), Some(&vec!["id".to_string()]));
913 }
914
915 #[test]
916 fn test_struct_without_count() {
917 let input = "%VERSION: 1.0\n%STRUCT: User: [id,name]\n---";
918 let lines = make_lines(input);
919 let (header, _) =
920 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
921 assert_eq!(
922 header.structs.get("User"),
923 Some(&vec!["id".to_string(), "name".to_string()])
924 );
925 assert_eq!(header.struct_counts.get("User"), None);
926 }
927
928 #[test]
929 fn test_struct_mixed_with_and_without_count() {
930 let input = "%VERSION: 1.0\n%STRUCT: User (5): [id, name]\n%STRUCT: Post: [id,title]\n---";
931 let lines = make_lines(input);
932 let (header, _) =
933 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
934 assert_eq!(header.struct_counts.get("User"), Some(&5));
935 assert_eq!(header.struct_counts.get("Post"), None);
936 }
937
938 #[test]
939 fn test_struct_count_leading_zero_error() {
940 let input = "%VERSION: 1.0\n%STRUCT: User (01): [id]\n---";
941 let lines = make_lines(input);
942 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
943 assert!(result.is_err());
944 assert!(result.unwrap_err().message.contains("leading zeros"));
945 }
946
947 #[test]
948 fn test_struct_count_invalid_number_error() {
949 let input = "%VERSION: 1.0\n%STRUCT: User (abc): [id]\n---";
950 let lines = make_lines(input);
951 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
952 assert!(result.is_err());
953 assert!(result.unwrap_err().message.contains("invalid count"));
954 }
955
956 #[test]
957 fn test_struct_count_negative_error() {
958 let input = "%VERSION: 1.0\n%STRUCT: User (-1): [id]\n---";
959 let lines = make_lines(input);
960 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
961 assert!(result.is_err());
962 assert!(result.unwrap_err().message.contains("invalid count"));
963 }
964
965 #[test]
966 fn test_struct_count_extra_content_after_paren_error() {
967 let input = "%VERSION: 1.0\n%STRUCT: User (5) extra: [id]\n---";
968 let lines = make_lines(input);
969 let result = parse_header(&lines, &default_limits(), &TimeoutContext::new(None));
970 assert!(result.is_err());
971 assert!(result
972 .unwrap_err()
973 .message
974 .contains("unexpected content after count"));
975 }
976
977 #[test]
978 fn test_struct_count_whitespace_before_paren() {
979 let input = "%VERSION: 1.0\n%STRUCT: Company (10): [id, name]\n---";
980 let lines = make_lines(input);
981 let (header, _) =
982 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
983 assert_eq!(header.struct_counts.get("Company"), Some(&10));
984 }
985
986 #[test]
987 fn test_struct_count_whitespace_inside_paren() {
988 let input = "%VERSION: 1.0\n%STRUCT: Company ( 10 ): [id, name]\n---";
989 let lines = make_lines(input);
990 let (header, _) =
991 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
992 assert_eq!(header.struct_counts.get("Company"), Some(&10));
993 }
994
995 #[test]
996 fn test_struct_count_large_number() {
997 let input = "%VERSION: 1.0\n%STRUCT: BigList (999999): [id]\n---";
998 let lines = make_lines(input);
999 let (header, _) =
1000 parse_header(&lines, &default_limits(), &TimeoutContext::new(None)).unwrap();
1001 assert_eq!(header.struct_counts.get("BigList"), Some(&999999));
1002 }
1003}