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
7pub 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
26pub 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 && let Some(line) = next_line
39 && line.depth == item_depth
40 && line.content.starts_with(LIST_ITEM_PREFIX)
41 {
42 return Err(ToonError::message(format!(
43 "Expected {expected_count} list array items, but found more"
44 )));
45 }
46 Ok(())
47}
48
49pub 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 && let Some(line) = next_line
62 && 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 Ok(())
72}
73
74pub fn validate_no_blank_lines_in_range(
80 start_line: usize,
81 end_line: usize,
82 blank_lines: &[BlankLineInfo],
83 strict: bool,
84 context: &str,
85) -> Result<()> {
86 if !strict {
87 return Ok(());
88 }
89
90 if let Some(first_blank) = blank_lines
91 .iter()
92 .find(|blank| blank.line_number > start_line && blank.line_number < end_line)
93 {
94 return Err(ToonError::message(format!(
95 "Line {}: Blank lines inside {context} are not allowed in strict mode",
96 first_blank.line_number
97 )));
98 }
99
100 Ok(())
101}
102
103fn is_data_row(content: &str, delimiter: char) -> bool {
104 let colon_pos = find_unquoted_char(content, COLON, 0);
106 let delimiter_pos = find_unquoted_char(content, delimiter, 0);
107
108 if colon_pos.is_none() {
110 return true;
111 }
112
113 if let Some(delimiter_pos) = delimiter_pos
115 && let Some(colon_pos) = colon_pos
116 {
117 return delimiter_pos < colon_pos;
118 }
119
120 false
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 fn make_line(content: &str, depth: usize, line_number: usize) -> ParsedLine {
128 ParsedLine {
129 raw: content.to_string(),
130 indent: depth * 2,
131 content: content.to_string(),
132 depth,
133 line_number,
134 }
135 }
136
137 #[test]
138 fn assert_expected_count_matches_is_ok() {
139 assert!(assert_expected_count(3, 3, "items", true).is_ok());
140 }
141
142 #[test]
143 fn assert_expected_count_mismatch_strict_errors() {
144 assert!(assert_expected_count(2, 3, "items", true).is_err());
145 }
146
147 #[test]
148 fn assert_expected_count_mismatch_lax_ok() {
149 assert!(assert_expected_count(2, 3, "items", false).is_ok());
150 }
151
152 #[test]
153 fn validate_no_extra_list_items_no_next_is_ok() {
154 assert!(validate_no_extra_list_items(None, 1, 3, true).is_ok());
155 }
156
157 #[test]
158 fn validate_no_extra_list_items_wrong_depth_is_ok() {
159 let line = make_line("- one", 0, 5);
160 assert!(validate_no_extra_list_items(Some(&line), 1, 3, true).is_ok());
161 }
162
163 #[test]
164 fn validate_no_extra_list_items_same_depth_strict_errors() {
165 let line = make_line("- extra", 1, 10);
166 assert!(validate_no_extra_list_items(Some(&line), 1, 3, true).is_err());
167 }
168
169 #[test]
170 fn validate_no_extra_list_items_same_depth_lax_ok() {
171 let line = make_line("- extra", 1, 10);
172 assert!(validate_no_extra_list_items(Some(&line), 1, 3, false).is_ok());
173 }
174
175 #[test]
176 fn validate_no_extra_list_items_non_list_content_ok() {
177 let line = make_line("k: v", 1, 10);
178 assert!(validate_no_extra_list_items(Some(&line), 1, 3, true).is_ok());
179 }
180
181 fn make_tabular_header(delimiter: char) -> ArrayHeaderInfo {
182 ArrayHeaderInfo {
183 key: None,
184 key_was_quoted: false,
185 length: 2,
186 delimiter,
187 fields: Some(vec![]),
188 }
189 }
190
191 #[test]
192 fn validate_no_extra_tabular_rows_accepts_non_data_row() {
193 let header = make_tabular_header(',');
194 let line = make_line("k: v", 1, 10);
195 assert!(validate_no_extra_tabular_rows(Some(&line), 1, &header, true).is_ok());
196 }
197
198 #[test]
199 fn validate_no_extra_tabular_rows_rejects_data_row_strict() {
200 let header = make_tabular_header(',');
201 let line = make_line("extra,data", 1, 10);
202 assert!(validate_no_extra_tabular_rows(Some(&line), 1, &header, true).is_err());
203 }
204
205 #[test]
206 fn validate_no_extra_tabular_rows_wrong_depth_ok() {
207 let header = make_tabular_header(',');
208 let line = make_line("extra,data", 0, 10);
209 assert!(validate_no_extra_tabular_rows(Some(&line), 1, &header, true).is_ok());
210 }
211
212 #[test]
213 fn validate_no_extra_tabular_rows_list_item_ok() {
214 let header = make_tabular_header(',');
215 let line = make_line("- extra", 1, 10);
216 assert!(validate_no_extra_tabular_rows(Some(&line), 1, &header, true).is_ok());
217 }
218
219 #[test]
220 fn validate_no_blank_lines_in_range_none_is_ok() {
221 let blanks: Vec<BlankLineInfo> = Vec::new();
222 assert!(validate_no_blank_lines_in_range(1, 5, &blanks, true, "ctx").is_ok());
223 }
224
225 #[test]
226 fn validate_no_blank_lines_in_range_outside_window_ok() {
227 let blanks = vec![BlankLineInfo {
228 line_number: 10,
229 indent: 0,
230 depth: 0,
231 }];
232 assert!(validate_no_blank_lines_in_range(1, 5, &blanks, true, "ctx").is_ok());
233 }
234
235 #[test]
236 fn validate_no_blank_lines_in_range_inside_window_errors_strict() {
237 let blanks = vec![BlankLineInfo {
238 line_number: 3,
239 indent: 0,
240 depth: 0,
241 }];
242 let err =
243 validate_no_blank_lines_in_range(1, 5, &blanks, true, "tabular array").unwrap_err();
244 let msg = format!("{err}");
245 assert!(msg.contains("Line 3"));
246 assert!(msg.contains("tabular array"));
247 }
248
249 #[test]
250 fn validate_no_blank_lines_in_range_lax_ok() {
251 let blanks = vec![BlankLineInfo {
252 line_number: 3,
253 indent: 0,
254 depth: 0,
255 }];
256 assert!(validate_no_blank_lines_in_range(1, 5, &blanks, false, "ctx").is_ok());
257 }
258
259 #[test]
260 fn is_data_row_without_colon() {
261 assert!(is_data_row("a,b,c", ','));
262 }
263
264 #[test]
265 fn is_data_row_with_delimiter_before_colon() {
266 assert!(is_data_row("a,b:c", ','));
267 }
268
269 #[test]
270 fn is_data_row_with_colon_before_delimiter_is_key_value() {
271 assert!(!is_data_row("a:b,c", ','));
272 }
273
274 #[test]
275 fn is_data_row_with_quoted_colon_inside_string() {
276 assert!(is_data_row("\"a:b\",c", ','));
278 }
279}