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::Bulb,
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!(warnings.is_empty());
138 }
139
140 #[test]
141 fn config_beats_defaults_and_flags_beat_config() {
142 let cfg = Config {
143 style: Some(Style::Truck),
144 volume: Some(0.5),
145 times: Some(3),
146 };
147 let (spec, _) = HonkSpec::resolve(&cfg, &Overrides::default()).unwrap();
148 assert_eq!(
149 (spec.style, spec.volume, spec.times),
150 (Style::Truck, 0.5, 3)
151 );
152
153 let o = Overrides {
154 style: Some(Style::Clown),
155 times: Some(1),
156 ..Overrides::default()
157 };
158 let (spec, _) = HonkSpec::resolve(&cfg, &o).unwrap();
159 assert_eq!(
160 (spec.style, spec.volume, spec.times),
161 (Style::Clown, 0.5, 1)
162 );
163 }
164
165 #[test]
166 fn out_of_range_config_values_are_skipped_with_a_warning() {
167 let cfg = Config {
168 style: None,
169 volume: Some(7.0),
170 times: Some(0),
171 };
172 let (spec, warnings) = HonkSpec::resolve(&cfg, &Overrides::default()).unwrap();
173 assert_eq!(spec.volume, HonkSpec::default().volume);
174 assert_eq!(spec.times, HonkSpec::default().times);
175 assert_eq!(warnings.len(), 2, "{warnings:?}");
176 assert!(warnings[0].contains("volume"));
177 }
178
179 #[test]
180 fn out_of_range_overrides_are_errors() {
181 for o in [
182 Overrides {
183 pitch: Some(9.0),
184 ..Overrides::default()
185 },
186 Overrides {
187 volume: Some(-0.1),
188 ..Overrides::default()
189 },
190 Overrides {
191 times: Some(11),
192 ..Overrides::default()
193 },
194 ] {
195 assert!(HonkSpec::resolve(&Config::default(), &o).is_err());
196 }
197 }
198
199 #[test]
200 fn mood_from_exit_code() {
201 assert_eq!(Mood::from_exit_code(0), Mood::Happy);
202 assert_eq!(Mood::from_exit_code(1), Mood::Sad);
203 assert_eq!(Mood::from_exit_code(-1), Mood::Sad);
204 }
205}