chordsketch_chordpro/
render_result.rs1use crate::ast::{CapoValidation, DirectiveKind, Line, Metadata, Song};
9use crate::config::Config;
10
11pub const MAX_WARNINGS: usize = 1000;
18
19pub fn push_warning(warnings: &mut Vec<String>, message: impl Into<String>) {
28 if warnings.len() < MAX_WARNINGS {
29 warnings.push(message.into());
30 } else if warnings.len() == MAX_WARNINGS {
31 warnings.push(format!(
32 "additional warnings suppressed; MAX_WARNINGS ({MAX_WARNINGS}) reached"
33 ));
34 }
35}
36
37pub fn validate_capo(metadata: &Metadata, warnings: &mut Vec<String>) {
46 match metadata.capo_validated() {
47 CapoValidation::Unset | CapoValidation::Valid(_) => {}
48 CapoValidation::OutOfRange(n) => {
49 push_warning(
50 warnings,
51 format!("{{capo}} value {n} out of range (expected 1..=24); ignored"),
52 );
53 }
54 CapoValidation::NotInteger(raw) => {
55 push_warning(
56 warnings,
57 format!("{{capo}} value {raw:?} is not a valid integer; ignored"),
58 );
59 }
60 }
61}
62
63pub fn validate_multiple_capo(song: &Song, warnings: &mut Vec<String>) {
80 let mut count = 0usize;
81 for line in &song.lines {
82 if let Line::Directive(directive) = line {
83 if directive.selector.is_some() {
84 continue;
85 }
86 if directive.value.is_none() {
87 continue;
88 }
89 let is_capo = match directive.kind {
90 DirectiveKind::Capo => true,
91 DirectiveKind::Meta(ref key) => key.eq_ignore_ascii_case("capo"),
92 _ => false,
93 };
94 if is_capo {
95 count += 1;
96 if count >= 2 {
97 push_warning(
98 warnings,
99 "Multiple capo settings may yield surprising results.",
100 );
101 return;
102 }
103 }
104 }
105 }
106}
107
108pub fn validate_strict_key(metadata: &Metadata, config: &Config, warnings: &mut Vec<String>) {
117 if config.get_path("settings.strict").as_bool() != Some(true) {
118 return;
119 }
120 if metadata.key.is_none() {
121 push_warning(
122 warnings,
123 "song does not declare a {key} directive (settings.strict)",
124 );
125 }
126}
127
128#[derive(Debug, Clone)]
135#[must_use]
136pub struct RenderResult<T> {
137 pub output: T,
139 pub warnings: Vec<String>,
141}
142
143impl<T> RenderResult<T> {
144 pub fn new(output: T) -> Self {
146 Self {
147 output,
148 warnings: Vec::new(),
149 }
150 }
151
152 pub fn with_warnings(output: T, warnings: Vec<String>) -> Self {
154 Self { output, warnings }
155 }
156
157 #[must_use]
159 pub fn has_warnings(&self) -> bool {
160 !self.warnings.is_empty()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_new_has_no_warnings() {
170 let result = RenderResult::new("hello");
171 assert_eq!(result.output, "hello");
172 assert!(result.warnings.is_empty());
173 assert!(!result.has_warnings());
174 }
175
176 #[test]
177 fn test_with_warnings() {
178 let result = RenderResult::with_warnings("output", vec!["warning 1".to_string()]);
179 assert_eq!(result.output, "output");
180 assert_eq!(result.warnings.len(), 1);
181 assert!(result.has_warnings());
182 }
183
184 #[test]
185 fn test_with_empty_warnings() {
186 let result = RenderResult::with_warnings(42, Vec::new());
187 assert_eq!(result.output, 42);
188 assert!(!result.has_warnings());
189 }
190
191 #[test]
194 fn test_push_warning_under_cap_appends() {
195 let mut v: Vec<String> = Vec::new();
196 push_warning(&mut v, "a");
197 push_warning(&mut v, "b");
198 assert_eq!(v, vec!["a".to_string(), "b".to_string()]);
199 }
200
201 #[test]
202 fn test_push_warning_caps_and_truncates_once() {
203 let mut v: Vec<String> = Vec::with_capacity(MAX_WARNINGS + 5);
204 for i in 0..(MAX_WARNINGS + 50) {
205 push_warning(&mut v, format!("w{i}"));
206 }
207 assert_eq!(v.len(), MAX_WARNINGS + 1);
208 assert!(
209 v.last().unwrap().contains("MAX_WARNINGS"),
210 "last entry must be the truncation marker; got {:?}",
211 v.last()
212 );
213 }
214
215 #[test]
218 fn test_validate_capo_unset_and_valid_emit_nothing() {
219 let mut v = Vec::<String>::new();
220 let md = Metadata::default();
221 validate_capo(&md, &mut v);
222 assert!(v.is_empty());
223
224 let md = Metadata {
225 capo: Some("5".to_string()),
226 ..Metadata::default()
227 };
228 validate_capo(&md, &mut v);
229 assert!(v.is_empty());
230 }
231
232 #[test]
233 fn test_validate_capo_out_of_range_warns_with_value() {
234 let mut v = Vec::<String>::new();
235 let md = Metadata {
236 capo: Some("999".to_string()),
237 ..Metadata::default()
238 };
239 validate_capo(&md, &mut v);
240 assert_eq!(v.len(), 1);
241 assert!(v[0].contains("999") && v[0].contains("out of range"));
242 }
243
244 #[test]
245 fn test_validate_capo_non_integer_warns_with_value() {
246 let mut v = Vec::<String>::new();
247 let md = Metadata {
248 capo: Some("foo".to_string()),
249 ..Metadata::default()
250 };
251 validate_capo(&md, &mut v);
252 assert_eq!(v.len(), 1);
253 assert!(v[0].contains("foo") && v[0].contains("not a valid integer"));
254 }
255
256 fn config_with_strict(strict: bool) -> Config {
259 Config::defaults()
260 .with_define(&format!("settings.strict={strict}"))
261 .expect("defining settings.strict must succeed")
262 }
263
264 #[test]
265 fn test_validate_strict_key_default_off_emits_nothing() {
266 let mut v = Vec::<String>::new();
267 let md = Metadata::default();
268 validate_strict_key(&md, &Config::defaults(), &mut v);
269 assert!(
270 v.is_empty(),
271 "default config has settings.strict=false; no warning expected"
272 );
273 }
274
275 #[test]
276 fn test_validate_strict_key_strict_off_with_missing_key_emits_nothing() {
277 let mut v = Vec::<String>::new();
278 let md = Metadata::default();
279 validate_strict_key(&md, &config_with_strict(false), &mut v);
280 assert!(v.is_empty());
281 }
282
283 #[test]
284 fn test_validate_strict_key_strict_on_with_missing_key_warns() {
285 let mut v = Vec::<String>::new();
286 let md = Metadata::default();
287 validate_strict_key(&md, &config_with_strict(true), &mut v);
288 assert_eq!(v.len(), 1);
289 assert!(v[0].contains("{key}") && v[0].contains("settings.strict"));
290 }
291
292 #[test]
293 fn test_validate_strict_key_strict_on_with_present_key_emits_nothing() {
294 let mut v = Vec::<String>::new();
295 let md = Metadata {
296 key: Some("C".to_string()),
297 ..Metadata::default()
298 };
299 validate_strict_key(&md, &config_with_strict(true), &mut v);
300 assert!(v.is_empty());
301 }
302
303 fn parse_song(input: &str) -> crate::ast::Song {
306 crate::parser::parse(input).expect("parse failed")
307 }
308
309 #[test]
310 fn test_validate_multiple_capo_zero_capos_emits_nothing() {
311 let mut v = Vec::<String>::new();
312 let song = parse_song("{title: T}\n[Am]Hello");
313 validate_multiple_capo(&song, &mut v);
314 assert!(v.is_empty());
315 }
316
317 #[test]
318 fn test_validate_multiple_capo_single_capo_emits_nothing() {
319 let mut v = Vec::<String>::new();
320 let song = parse_song("{capo: 2}\n[Am]Hello");
321 validate_multiple_capo(&song, &mut v);
322 assert!(v.is_empty());
323 }
324
325 #[test]
326 fn test_validate_multiple_capo_two_capos_warns_once() {
327 let mut v = Vec::<String>::new();
328 let song = parse_song("{capo: 2}\n[Am]Hello\n{capo: 4}\n[C]World");
329 validate_multiple_capo(&song, &mut v);
330 assert_eq!(v.len(), 1, "expected exactly one warning");
331 assert!(
332 v[0].contains("Multiple capo settings"),
333 "unexpected message: {:?}",
334 v[0]
335 );
336 }
337
338 #[test]
339 fn test_validate_multiple_capo_three_capos_still_warns_once() {
340 let mut v = Vec::<String>::new();
341 let song = parse_song("{capo: 1}\n{capo: 2}\n{capo: 3}");
342 validate_multiple_capo(&song, &mut v);
343 assert_eq!(v.len(), 1);
344 }
345
346 #[test]
347 fn test_validate_multiple_capo_meta_form_counts() {
348 let mut v = Vec::<String>::new();
350 let song = parse_song("{capo: 2}\n{meta: capo 4}");
351 validate_multiple_capo(&song, &mut v);
352 assert_eq!(v.len(), 1);
353 }
354}