hedl_core/
header.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Header directive parsing for HEDL.
19
20use 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/// Parsed header data.
27#[derive(Debug, Clone)]
28pub struct Header {
29    /// HEDL format version as (major, minor).
30    pub version: (u32, u32),
31    /// Type aliases mapping alias name to original type.
32    pub aliases: BTreeMap<String, String>,
33    /// Struct definitions mapping struct name to field names.
34    pub structs: BTreeMap<String, Vec<String>>,
35    /// Expected row counts for structs (from count hints).
36    pub struct_counts: BTreeMap<String, usize>,
37    /// Nesting relationships mapping child type to parent type.
38    pub nests: BTreeMap<String, String>,
39}
40
41impl Header {
42    /// Create a new Header with the given version and empty collections.
43    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
54/// Parse the header section from preprocessed lines.
55///
56/// Returns the header data and the index where the body starts.
57pub 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        // Periodic timeout check (every 10,000 iterations to minimize overhead)
71        if idx % 10_000 == 0 {
72            timeout_ctx.check_timeout(line_num)?;
73        }
74
75        let trimmed = line.trim();
76
77        // Check for separator
78        if trimmed == "---" || trimmed.starts_with("--- ") || trimmed.starts_with("---#") {
79            // Validate separator has no leading spaces
80            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        // Skip blank and comment lines
101        if trimmed.is_empty() || trimmed.starts_with('#') {
102            continue;
103        }
104
105        // Must be a directive
106        if !trimmed.starts_with('%') {
107            return Err(messages::expected_directive(trimmed, line_num));
108        }
109
110        // Parse directive
111        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        // Must have space after colon
119        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    // Check for leading zeros
193    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
202/// Parse a struct definition.
203///
204/// Validates the type name, column list, and optional count hint syntax.
205/// Returns the type name, columns, and optional count.
206fn parse_struct(
207    payload: &str,
208    line_num: usize,
209    limits: &Limits,
210) -> HedlResult<(String, Vec<String>, Option<usize>)> {
211    // Format: TypeName: [col1, col2, ...] OR TypeName (N): [col1, col2, ...]
212    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    // Parse optional count hint using shared parser
219    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    // Format: %key: "value"
282    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    // Parse quoted string (handle "" escapes)
302    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    // Format: ParentType > ChildType
314    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    // ==================== Minimal header tests ====================
354
355    #[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); // Index after separator
371    }
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    // ==================== %VERSION tests ====================
410
411    #[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    // ==================== %STRUCT tests ====================
485
486    #[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    // ==================== %ALIAS tests ====================
600
601    #[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    // ==================== %NEST tests ====================
682
683    #[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    // ==================== General error cases ====================
740
741    #[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        // Error is "expected directive starting with '%'" since 'a: 1' is not a directive
757        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    // ==================== Header struct tests ====================
806
807    #[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    // ==================== Edge cases ====================
831
832    #[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    // ==================== %STRUCT with count tests ====================
870
871    #[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}