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::Bulb,
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!(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}