use crate::ast::{CapoValidation, DirectiveKind, Line, Metadata, Song};
use crate::config::Config;
pub const MAX_WARNINGS: usize = 1000;
pub fn push_warning(warnings: &mut Vec<String>, message: impl Into<String>) {
if warnings.len() < MAX_WARNINGS {
warnings.push(message.into());
} else if warnings.len() == MAX_WARNINGS {
warnings.push(format!(
"additional warnings suppressed; MAX_WARNINGS ({MAX_WARNINGS}) reached"
));
}
}
pub fn validate_capo(metadata: &Metadata, warnings: &mut Vec<String>) {
match metadata.capo_validated() {
CapoValidation::Unset | CapoValidation::Valid(_) => {}
CapoValidation::OutOfRange(n) => {
push_warning(
warnings,
format!("{{capo}} value {n} out of range (expected 1..=24); ignored"),
);
}
CapoValidation::NotInteger(raw) => {
push_warning(
warnings,
format!("{{capo}} value {raw:?} is not a valid integer; ignored"),
);
}
}
}
pub fn validate_multiple_capo(song: &Song, warnings: &mut Vec<String>) {
let mut count = 0usize;
for line in &song.lines {
if let Line::Directive(directive) = line {
if directive.selector.is_some() {
continue;
}
if directive.value.is_none() {
continue;
}
let is_capo = match directive.kind {
DirectiveKind::Capo => true,
DirectiveKind::Meta(ref key) => key.eq_ignore_ascii_case("capo"),
_ => false,
};
if is_capo {
count += 1;
if count >= 2 {
push_warning(
warnings,
"Multiple capo settings may yield surprising results.",
);
return;
}
}
}
}
}
pub fn validate_strict_key(metadata: &Metadata, config: &Config, warnings: &mut Vec<String>) {
if config.get_path("settings.strict").as_bool() != Some(true) {
return;
}
if metadata.key.is_none() {
push_warning(
warnings,
"song does not declare a {key} directive (settings.strict)",
);
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct RenderResult<T> {
pub output: T,
pub warnings: Vec<String>,
}
impl<T> RenderResult<T> {
pub fn new(output: T) -> Self {
Self {
output,
warnings: Vec::new(),
}
}
pub fn with_warnings(output: T, warnings: Vec<String>) -> Self {
Self { output, warnings }
}
#[must_use]
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_has_no_warnings() {
let result = RenderResult::new("hello");
assert_eq!(result.output, "hello");
assert!(result.warnings.is_empty());
assert!(!result.has_warnings());
}
#[test]
fn test_with_warnings() {
let result = RenderResult::with_warnings("output", vec!["warning 1".to_string()]);
assert_eq!(result.output, "output");
assert_eq!(result.warnings.len(), 1);
assert!(result.has_warnings());
}
#[test]
fn test_with_empty_warnings() {
let result = RenderResult::with_warnings(42, Vec::new());
assert_eq!(result.output, 42);
assert!(!result.has_warnings());
}
#[test]
fn test_push_warning_under_cap_appends() {
let mut v: Vec<String> = Vec::new();
push_warning(&mut v, "a");
push_warning(&mut v, "b");
assert_eq!(v, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn test_push_warning_caps_and_truncates_once() {
let mut v: Vec<String> = Vec::with_capacity(MAX_WARNINGS + 5);
for i in 0..(MAX_WARNINGS + 50) {
push_warning(&mut v, format!("w{i}"));
}
assert_eq!(v.len(), MAX_WARNINGS + 1);
assert!(
v.last().unwrap().contains("MAX_WARNINGS"),
"last entry must be the truncation marker; got {:?}",
v.last()
);
}
#[test]
fn test_validate_capo_unset_and_valid_emit_nothing() {
let mut v = Vec::<String>::new();
let md = Metadata::default();
validate_capo(&md, &mut v);
assert!(v.is_empty());
let md = Metadata {
capo: Some("5".to_string()),
..Metadata::default()
};
validate_capo(&md, &mut v);
assert!(v.is_empty());
}
#[test]
fn test_validate_capo_out_of_range_warns_with_value() {
let mut v = Vec::<String>::new();
let md = Metadata {
capo: Some("999".to_string()),
..Metadata::default()
};
validate_capo(&md, &mut v);
assert_eq!(v.len(), 1);
assert!(v[0].contains("999") && v[0].contains("out of range"));
}
#[test]
fn test_validate_capo_non_integer_warns_with_value() {
let mut v = Vec::<String>::new();
let md = Metadata {
capo: Some("foo".to_string()),
..Metadata::default()
};
validate_capo(&md, &mut v);
assert_eq!(v.len(), 1);
assert!(v[0].contains("foo") && v[0].contains("not a valid integer"));
}
fn config_with_strict(strict: bool) -> Config {
Config::defaults()
.with_define(&format!("settings.strict={strict}"))
.expect("defining settings.strict must succeed")
}
#[test]
fn test_validate_strict_key_default_off_emits_nothing() {
let mut v = Vec::<String>::new();
let md = Metadata::default();
validate_strict_key(&md, &Config::defaults(), &mut v);
assert!(
v.is_empty(),
"default config has settings.strict=false; no warning expected"
);
}
#[test]
fn test_validate_strict_key_strict_off_with_missing_key_emits_nothing() {
let mut v = Vec::<String>::new();
let md = Metadata::default();
validate_strict_key(&md, &config_with_strict(false), &mut v);
assert!(v.is_empty());
}
#[test]
fn test_validate_strict_key_strict_on_with_missing_key_warns() {
let mut v = Vec::<String>::new();
let md = Metadata::default();
validate_strict_key(&md, &config_with_strict(true), &mut v);
assert_eq!(v.len(), 1);
assert!(v[0].contains("{key}") && v[0].contains("settings.strict"));
}
#[test]
fn test_validate_strict_key_strict_on_with_present_key_emits_nothing() {
let mut v = Vec::<String>::new();
let md = Metadata {
key: Some("C".to_string()),
..Metadata::default()
};
validate_strict_key(&md, &config_with_strict(true), &mut v);
assert!(v.is_empty());
}
fn parse_song(input: &str) -> crate::ast::Song {
crate::parser::parse(input).expect("parse failed")
}
#[test]
fn test_validate_multiple_capo_zero_capos_emits_nothing() {
let mut v = Vec::<String>::new();
let song = parse_song("{title: T}\n[Am]Hello");
validate_multiple_capo(&song, &mut v);
assert!(v.is_empty());
}
#[test]
fn test_validate_multiple_capo_single_capo_emits_nothing() {
let mut v = Vec::<String>::new();
let song = parse_song("{capo: 2}\n[Am]Hello");
validate_multiple_capo(&song, &mut v);
assert!(v.is_empty());
}
#[test]
fn test_validate_multiple_capo_two_capos_warns_once() {
let mut v = Vec::<String>::new();
let song = parse_song("{capo: 2}\n[Am]Hello\n{capo: 4}\n[C]World");
validate_multiple_capo(&song, &mut v);
assert_eq!(v.len(), 1, "expected exactly one warning");
assert!(
v[0].contains("Multiple capo settings"),
"unexpected message: {:?}",
v[0]
);
}
#[test]
fn test_validate_multiple_capo_three_capos_still_warns_once() {
let mut v = Vec::<String>::new();
let song = parse_song("{capo: 1}\n{capo: 2}\n{capo: 3}");
validate_multiple_capo(&song, &mut v);
assert_eq!(v.len(), 1);
}
#[test]
fn test_validate_multiple_capo_meta_form_counts() {
let mut v = Vec::<String>::new();
let song = parse_song("{capo: 2}\n{meta: capo 4}");
validate_multiple_capo(&song, &mut v);
assert_eq!(v.len(), 1);
}
}