1use std::ops::RangeInclusive;
4
5use crate::config::Config;
6use crate::error::{Error, Result};
7use crate::style::Style;
8
9pub const TIMES_RANGE: RangeInclusive<u32> = 1..=10;
10pub const PITCH_RANGE: RangeInclusive<f32> = 0.5..=2.0;
11pub const VOLUME_RANGE: RangeInclusive<f32> = 0.0..=1.0;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Mood {
16 #[default]
17 Neutral,
18 Happy,
19 Sad,
20}
21
22impl Mood {
23 pub fn from_exit_code(code: i32) -> Mood {
25 if code == 0 { Mood::Happy } else { Mood::Sad }
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct HonkSpec {
32 pub style: Style,
33 pub times: u32,
34 pub long: bool,
35 pub pitch: f32,
36 pub volume: f32,
37 pub mood: Mood,
38}
39
40impl Default for HonkSpec {
41 fn default() -> Self {
42 HonkSpec {
43 style: Style::Car,
44 times: 2,
45 long: false,
46 pitch: 1.0,
47 volume: 0.8,
48 mood: Mood::Neutral,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, Default)]
55pub struct Overrides {
56 pub style: Option<Style>,
57 pub times: Option<u32>,
58 pub long: bool,
59 pub pitch: Option<f32>,
60 pub volume: Option<f32>,
61 pub mood: Mood,
62}
63
64impl HonkSpec {
65 pub fn resolve(config: &Config, o: &Overrides) -> Result<(HonkSpec, Vec<String>)> {
68 let mut spec = HonkSpec::default();
69 let mut warnings = Vec::new();
70
71 if let Some(style) = config.style {
72 spec.style = style;
73 }
74 match config.volume {
75 Some(v) if VOLUME_RANGE.contains(&v) => spec.volume = v,
76 Some(v) => warnings.push(format!(
77 "ignoring config volume {v}: must be between 0.0 and 1.0"
78 )),
79 None => {}
80 }
81 match config.times {
82 Some(t) if TIMES_RANGE.contains(&t) => spec.times = t,
83 Some(t) => warnings.push(format!(
84 "ignoring config times {t}: must be between 1 and 10"
85 )),
86 None => {}
87 }
88
89 if let Some(style) = o.style {
90 spec.style = style;
91 }
92 if let Some(t) = o.times {
93 check(
94 TIMES_RANGE.contains(&t),
95 format!("--times {t} must be between 1 and 10"),
96 )?;
97 spec.times = t;
98 }
99 if let Some(p) = o.pitch {
100 check(
101 PITCH_RANGE.contains(&p),
102 format!("--pitch {p} must be between 0.5 and 2.0"),
103 )?;
104 spec.pitch = p;
105 }
106 if let Some(v) = o.volume {
107 check(
108 VOLUME_RANGE.contains(&v),
109 format!("--volume {v} must be between 0.0 and 1.0"),
110 )?;
111 spec.volume = v;
112 }
113 spec.long = o.long;
114 spec.mood = o.mood;
115 Ok((spec, warnings))
116 }
117}
118
119fn check(ok: bool, msg: String) -> Result<()> {
120 if ok {
121 Ok(())
122 } else {
123 Err(Error::InvalidSpec(msg))
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::config::Config;
131
132 #[test]
133 fn defaults_without_config_or_flags() {
134 let (spec, warnings) =
135 HonkSpec::resolve(&Config::default(), &Overrides::default()).unwrap();
136 assert_eq!(spec, HonkSpec::default());
137 assert_eq!(spec.style, Style::Car);
138 assert!(warnings.is_empty());
139 }
140
141 #[test]
142 fn config_beats_defaults_and_flags_beat_config() {
143 let cfg = Config {
144 style: Some(Style::Truck),
145 volume: Some(0.5),
146 times: Some(3),
147 };
148 let (spec, _) = HonkSpec::resolve(&cfg, &Overrides::default()).unwrap();
149 assert_eq!(
150 (spec.style, spec.volume, spec.times),
151 (Style::Truck, 0.5, 3)
152 );
153
154 let o = Overrides {
155 style: Some(Style::Clown),
156 times: Some(1),
157 ..Overrides::default()
158 };
159 let (spec, _) = HonkSpec::resolve(&cfg, &o).unwrap();
160 assert_eq!(
161 (spec.style, spec.volume, spec.times),
162 (Style::Clown, 0.5, 1)
163 );
164 }
165
166 #[test]
167 fn out_of_range_config_values_are_skipped_with_a_warning() {
168 let cfg = Config {
169 style: None,
170 volume: Some(7.0),
171 times: Some(0),
172 };
173 let (spec, warnings) = HonkSpec::resolve(&cfg, &Overrides::default()).unwrap();
174 assert_eq!(spec.volume, HonkSpec::default().volume);
175 assert_eq!(spec.times, HonkSpec::default().times);
176 assert_eq!(warnings.len(), 2, "{warnings:?}");
177 assert!(warnings[0].contains("volume"));
178 }
179
180 #[test]
181 fn out_of_range_overrides_are_errors() {
182 for o in [
183 Overrides {
184 pitch: Some(9.0),
185 ..Overrides::default()
186 },
187 Overrides {
188 volume: Some(-0.1),
189 ..Overrides::default()
190 },
191 Overrides {
192 times: Some(11),
193 ..Overrides::default()
194 },
195 ] {
196 assert!(HonkSpec::resolve(&Config::default(), &o).is_err());
197 }
198 }
199
200 #[test]
201 fn mood_from_exit_code() {
202 assert_eq!(Mood::from_exit_code(0), Mood::Happy);
203 assert_eq!(Mood::from_exit_code(1), Mood::Sad);
204 assert_eq!(Mood::from_exit_code(-1), Mood::Sad);
205 }
206}