Skip to main content

honk_core/
spec.rs

1//! What to honk: the fully resolved request handed to the synthesiser.
2
3use 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/// How the honk should feel: neutral, or the outcome of a command.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Mood {
16    #[default]
17    Neutral,
18    Happy,
19    Sad,
20}
21
22impl Mood {
23    /// 0 is success (happy); anything else is failure (sad).
24    pub fn from_exit_code(code: i32) -> Mood {
25        if code == 0 { Mood::Happy } else { Mood::Sad }
26    }
27}
28
29/// A fully resolved honk.
30#[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/// Values given on the command line; `None` means "not given".
54#[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    /// Resolve defaults, then config, then overrides. Invalid config values are
66    /// skipped and reported as warnings; invalid overrides are errors.
67    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}