twilight_model/util/
hex_color.rs

1use std::fmt::Formatter;
2use std::fmt::{Display, Result as FmtResult};
3use std::num::ParseIntError;
4use std::str::FromStr;
5
6use serde::de::Visitor;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9/// Represents a color in the RGB format using hexadecimal notation.
10#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
11pub struct HexColor(
12    /// Red component of the color.
13    pub u8,
14    /// Green component of the color.
15    pub u8,
16    /// Blue component of the color.
17    pub u8,
18);
19
20impl Display for HexColor {
21    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
22        f.write_fmt(format_args!("#{:02X}{:02X}{:02X}", self.0, self.1, self.2))
23    }
24}
25
26impl Serialize for HexColor {
27    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
28        serializer.serialize_str(&self.to_string())
29    }
30}
31
32#[derive(Debug)]
33pub enum HexColorParseError {
34    InvalidLength,
35    InvalidFormat,
36    InvalidCharacter(ParseIntError),
37}
38
39impl From<ParseIntError> for HexColorParseError {
40    fn from(err: ParseIntError) -> Self {
41        Self::InvalidCharacter(err)
42    }
43}
44
45impl FromStr for HexColor {
46    type Err = HexColorParseError;
47
48    fn from_str(s: &str) -> Result<Self, Self::Err> {
49        if !s.starts_with('#') {
50            return Err(HexColorParseError::InvalidFormat);
51        }
52
53        let s = s.trim_start_matches('#');
54
55        let (r, g, b) = match s.len() {
56            3 => (
57                u8::from_str_radix(&s[0..1], 16)?,
58                u8::from_str_radix(&s[1..2], 16)?,
59                u8::from_str_radix(&s[2..3], 16)?,
60            ),
61            6 => (
62                u8::from_str_radix(&s[0..2], 16)?,
63                u8::from_str_radix(&s[2..4], 16)?,
64                u8::from_str_radix(&s[4..6], 16)?,
65            ),
66            _ => return Err(HexColorParseError::InvalidLength),
67        };
68
69        Ok(Self(r, g, b))
70    }
71}
72
73struct HexColorVisitor;
74
75impl Visitor<'_> for HexColorVisitor {
76    type Value = HexColor;
77
78    fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
79        formatter.write_str("a hex color string")
80    }
81
82    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
83    where
84        E: serde::de::Error,
85    {
86        HexColor::from_str(v).map_err(|_| E::custom("invalid hex color"))
87    }
88}
89
90impl<'de> Deserialize<'de> for HexColor {
91    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
92        deserializer.deserialize_str(HexColorVisitor)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::HexColor;
99
100    #[test]
101    fn hex_color_display() {
102        let hex_color = HexColor(255, 255, 255);
103        assert_eq!(hex_color.to_string(), "#FFFFFF");
104    }
105
106    #[test]
107    fn serialize() {
108        let hex_color = HexColor(252, 177, 3);
109        let serialized = serde_json::to_string(&hex_color).unwrap();
110        assert_eq!(serialized, "\"#FCB103\"");
111    }
112
113    #[test]
114    fn serialize_2() {
115        let hex_color = HexColor(255, 255, 255);
116        let serialized = serde_json::to_string(&hex_color).unwrap();
117        assert_eq!(serialized, "\"#FFFFFF\"");
118    }
119
120    #[test]
121    fn deserialize() {
122        let deserialized: HexColor = serde_json::from_str("\"#FFFFFF\"").unwrap();
123        assert_eq!(deserialized, HexColor(255, 255, 255));
124    }
125
126    #[test]
127    fn deserialize_invalid() {
128        let deserialized: Result<HexColor, _> = serde_json::from_str("\"#GGGGGG\"");
129        assert!(deserialized.is_err());
130    }
131}