Skip to main content

toon/decode/
validation.rs

1use crate::decode::parser::ArrayHeaderInfo;
2use crate::decode::scanner::{BlankLineInfo, Depth, ParsedLine};
3use crate::error::{Result, ToonError};
4use crate::shared::constants::{COLON, LIST_ITEM_PREFIX};
5use crate::shared::string_utils::find_unquoted_char;
6
7/// Assert the expected count in strict mode.
8///
9/// # Errors
10///
11/// Returns an error when strict mode is enabled and counts differ.
12pub fn assert_expected_count(
13    actual: usize,
14    expected: usize,
15    item_type: &str,
16    strict: bool,
17) -> Result<()> {
18    if strict && actual != expected {
19        return Err(ToonError::message(format!(
20            "Expected {expected} {item_type}, but got {actual}"
21        )));
22    }
23    Ok(())
24}
25
26/// Validate that there are no extra list items beyond the expected count.
27///
28/// # Errors
29///
30/// Returns an error in strict mode when extra list items are found.
31pub fn validate_no_extra_list_items(
32    next_line: Option<&ParsedLine>,
33    item_depth: Depth,
34    expected_count: usize,
35    strict: bool,
36) -> Result<()> {
37    if strict {
38        if let Some(line) = next_line {
39            if line.depth == item_depth && line.content.starts_with(LIST_ITEM_PREFIX) {
40                return Err(ToonError::message(format!(
41                    "Expected {expected_count} list array items, but found more"
42                )));
43            }
44        }
45    }
46    Ok(())
47}
48
49/// Validate that there are no extra tabular rows beyond the expected count.
50///
51/// # Errors
52///
53/// Returns an error in strict mode when extra tabular rows are found.
54pub fn validate_no_extra_tabular_rows(
55    next_line: Option<&ParsedLine>,
56    row_depth: Depth,
57    header: &ArrayHeaderInfo,
58    strict: bool,
59) -> Result<()> {
60    if strict {
61        if let Some(line) = next_line {
62            if line.depth == row_depth
63                && !line.content.starts_with(LIST_ITEM_PREFIX)
64                && is_data_row(&line.content, header.delimiter)
65            {
66                return Err(ToonError::message(format!(
67                    "Expected {} tabular rows, but found more",
68                    header.length
69                )));
70            }
71        }
72    }
73    Ok(())
74}
75
76/// Validate that no blank lines appear within the specified range.
77///
78/// # Errors
79///
80/// Returns an error in strict mode when blank lines appear within the range.
81pub fn validate_no_blank_lines_in_range(
82    start_line: usize,
83    end_line: usize,
84    blank_lines: &[BlankLineInfo],
85    strict: bool,
86    context: &str,
87) -> Result<()> {
88    if !strict {
89        return Ok(());
90    }
91
92    if let Some(first_blank) = blank_lines
93        .iter()
94        .find(|blank| blank.line_number > start_line && blank.line_number < end_line)
95    {
96        return Err(ToonError::message(format!(
97            "Line {}: Blank lines inside {context} are not allowed in strict mode",
98            first_blank.line_number
99        )));
100    }
101
102    Ok(())
103}
104
105fn is_data_row(content: &str, delimiter: char) -> bool {
106    // Find first unquoted colon and delimiter to properly handle quoted strings
107    let colon_pos = find_unquoted_char(content, COLON, 0);
108    let delimiter_pos = find_unquoted_char(content, delimiter, 0);
109
110    // If no unquoted colon, it's definitely a data row
111    if colon_pos.is_none() {
112        return true;
113    }
114
115    // If delimiter comes before colon (outside quotes), it's a data row
116    if let Some(delimiter_pos) = delimiter_pos {
117        if let Some(colon_pos) = colon_pos {
118            return delimiter_pos < colon_pos;
119        }
120    }
121
122    false
123}