homie_device/
values.rs

1use std::fmt::{self, Debug, Display, Formatter};
2use std::num::ParseIntError;
3use std::str::FromStr;
4use thiserror::Error;
5
6/// The format of a [colour](https://homieiot.github.io/specification/#color) property, either RGB
7/// or HSV.
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub enum ColorFormat {
10    /// The colour is in red-green-blue format.
11    Rgb,
12    /// The colour is in hue-saturation-value format.
13    Hsv,
14}
15
16impl ColorFormat {
17    fn as_str(&self) -> &'static str {
18        match self {
19            Self::Rgb => "rgb",
20            Self::Hsv => "hsv",
21        }
22    }
23}
24
25impl Display for ColorFormat {
26    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
27        f.write_str(self.as_str())
28    }
29}
30
31pub trait Color {
32    fn format() -> ColorFormat;
33}
34
35/// An error while attempting to parse a `Color` from a string.
36#[derive(Clone, Debug, Error, Eq, PartialEq)]
37#[error("Failed to parse color.")]
38pub struct ParseColorError();
39
40impl From<ParseIntError> for ParseColorError {
41    fn from(_: ParseIntError) -> Self {
42        ParseColorError()
43    }
44}
45
46/// A [colour](https://homieiot.github.io/specification/#color) in red-green-blue format.
47#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct ColorRgb {
49    /// The red channel of the colour, between 0 and 255.
50    pub r: u8,
51    /// The green channel of the colour, between 0 and 255.
52    pub g: u8,
53    /// The blue channel of the colour, between 0 and 255.
54    pub b: u8,
55}
56
57impl ColorRgb {
58    /// Construct a new RGB colour.
59    pub fn new(r: u8, g: u8, b: u8) -> Self {
60        ColorRgb { r, g, b }
61    }
62}
63
64impl Display for ColorRgb {
65    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
66        write!(f, "{},{},{}", self.r, self.g, self.b)
67    }
68}
69
70impl FromStr for ColorRgb {
71    type Err = ParseColorError;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        let parts: Vec<_> = s.split(',').collect();
75        if let [r, g, b] = parts.as_slice() {
76            Ok(ColorRgb {
77                r: r.parse()?,
78                g: g.parse()?,
79                b: b.parse()?,
80            })
81        } else {
82            Err(ParseColorError())
83        }
84    }
85}
86
87impl Color for ColorRgb {
88    fn format() -> ColorFormat {
89        ColorFormat::Rgb
90    }
91}
92
93/// A [colour](https://homieiot.github.io/specification/#color) in hue-saturation-value format.
94#[derive(Clone, Debug, Eq, PartialEq)]
95pub struct ColorHsv {
96    /// The hue of the colour, between 0 and 360.
97    pub h: u16,
98    /// The saturation of the colour, between 0 and 100.
99    pub s: u8,
100    /// The value of the colour, between 0 and 100.
101    pub v: u8,
102}
103
104impl ColorHsv {
105    /// Construct a new HSV colour, or panic if the values given are out of range.
106    pub fn new(h: u16, s: u8, v: u8) -> Self {
107        assert!(h <= 360);
108        assert!(s <= 100);
109        assert!(v <= 100);
110        ColorHsv { h, s, v }
111    }
112}
113
114impl Display for ColorHsv {
115    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
116        write!(f, "{},{},{}", self.h, self.s, self.v)
117    }
118}
119
120impl FromStr for ColorHsv {
121    type Err = ParseColorError;
122
123    fn from_str(s: &str) -> Result<Self, Self::Err> {
124        let parts: Vec<_> = s.split(',').collect();
125        if let [h, s, v] = parts.as_slice() {
126            let h = h.parse()?;
127            let s = s.parse()?;
128            let v = v.parse()?;
129            if h <= 360 && s <= 100 && v <= 100 {
130                return Ok(ColorHsv { h, s, v });
131            }
132        }
133        Err(ParseColorError())
134    }
135}
136
137impl Color for ColorHsv {
138    fn format() -> ColorFormat {
139        ColorFormat::Hsv
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn color_rgb_to_from_string() {
149        let color = ColorRgb::new(111, 222, 42);
150        assert_eq!(color.to_string().parse(), Ok(color));
151    }
152
153    #[test]
154    fn color_hsv_to_from_string() {
155        let color = ColorHsv::new(231, 88, 77);
156        assert_eq!(color.to_string().parse(), Ok(color));
157    }
158
159    #[test]
160    fn color_rgb_parse_invalid() {
161        assert_eq!("".parse::<ColorRgb>(), Err(ParseColorError()));
162        assert_eq!("1,2".parse::<ColorRgb>(), Err(ParseColorError()));
163        assert_eq!("1,2,3,4".parse::<ColorRgb>(), Err(ParseColorError()));
164        assert_eq!("1,2,256".parse::<ColorRgb>(), Err(ParseColorError()));
165        assert_eq!("1,256,3".parse::<ColorRgb>(), Err(ParseColorError()));
166        assert_eq!("256,2,3".parse::<ColorRgb>(), Err(ParseColorError()));
167        assert_eq!("1,-2,3".parse::<ColorRgb>(), Err(ParseColorError()));
168    }
169
170    #[test]
171    fn color_hsv_parse_invalid() {
172        assert_eq!("".parse::<ColorHsv>(), Err(ParseColorError()));
173        assert_eq!("1,2".parse::<ColorHsv>(), Err(ParseColorError()));
174        assert_eq!("1,2,3,4".parse::<ColorHsv>(), Err(ParseColorError()));
175        assert_eq!("1,2,101".parse::<ColorHsv>(), Err(ParseColorError()));
176        assert_eq!("1,101,3".parse::<ColorHsv>(), Err(ParseColorError()));
177        assert_eq!("361,2,3".parse::<ColorHsv>(), Err(ParseColorError()));
178        assert_eq!("1,-2,3".parse::<ColorHsv>(), Err(ParseColorError()));
179    }
180}