slack_hook/
hex.rs

1use crate::error::Error;
2use std::{convert::TryFrom, fmt, str::FromStr};
3
4use hex::FromHex;
5use serde::Serialize;
6
7/// A `HexColor` `String` can be one of:
8///
9/// 1. `String`s: `good`, `warning`, `danger`
10/// 2. Any valid hex color code: e.g. `#b13d41` or `#000`.
11///
12/// hex color codes will be checked to ensure a valid hex number is provided
13#[derive(Serialize, Debug, Clone, PartialEq)]
14pub struct HexColor(String);
15
16impl HexColor {
17    fn new<S: Into<String>>(s: S) -> HexColor {
18        HexColor(s.into())
19    }
20}
21
22impl Default for HexColor {
23    fn default() -> HexColor {
24        HexColor::new("#000")
25    }
26}
27
28impl fmt::Display for HexColor {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        self.0.fmt(f)
31    }
32}
33
34// FIXME(cosmic): Why provide this when there's already `FromStr`? Some generic bound maybe?
35impl TryFrom<&str> for HexColor {
36    type Error = Error;
37
38    fn try_from(s: &str) -> Result<Self, Self::Error> {
39        s.parse()
40    }
41}
42
43/// Default slack colors built-in to the API
44/// See: <https://api.slack.com/docs/attachments>
45#[derive(Copy, Clone, Debug, PartialEq, Eq)]
46pub enum SlackColor {
47    /// green
48    Good,
49    /// orange
50    Warning,
51    /// red
52    Danger,
53}
54
55const SLACK_COLORS: [&str; 3] = ["good", "warning", "danger"];
56
57impl fmt::Display for SlackColor {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        self.as_ref().fmt(f)
60    }
61}
62
63impl AsRef<str> for SlackColor {
64    fn as_ref(&self) -> &str {
65        match *self {
66            SlackColor::Good => "good",
67            SlackColor::Warning => "warning",
68            SlackColor::Danger => "danger",
69        }
70    }
71}
72
73impl From<SlackColor> for HexColor {
74    fn from(color: SlackColor) -> HexColor {
75        HexColor::new(color.to_string())
76    }
77}
78
79impl FromStr for HexColor {
80    type Err = Error;
81
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        let s: String = s.into();
84        if SLACK_COLORS.contains(&&s[..]) {
85            return Ok(HexColor(s));
86        }
87
88        let num_chars = s.chars().count();
89        if num_chars != 7 && num_chars != 4 {
90            return Err(Error::HexColor(format!(
91                "Must be 4 or 7 characters long (including #): \
92                 found `{}`",
93                s
94            )));
95        }
96        if !s.starts_with('#') {
97            return Err(Error::HexColor(format!("No leading #: found `{}`", s)));
98        }
99
100        // #d18 -> #dd1188
101        let hex = if num_chars == 4 {
102            s.chars().skip(1).fold(String::from("#"), |mut s, c| {
103                s.push(c);
104                s.push(c);
105                s
106            })
107        } else {
108            s.clone()
109        };
110
111        // see if the remaining part of the string is actually hex
112        match Vec::from_hex(&hex[1..]) {
113            Ok(_) => Ok(HexColor::new(s)),
114            Err(e) => Err(e.into()),
115        }
116    }
117}
118
119#[cfg(test)]
120mod test {
121    use std::convert::TryFrom;
122
123    use super::*;
124    use crate::HexColor;
125
126    use insta::assert_snapshot;
127
128    mod err {
129        use super::*;
130
131        #[test]
132        fn too_short() {
133            let err = HexColor::try_from("abc").unwrap_err();
134            assert_snapshot!(
135                err,
136                @"hex color parsing error: Must be 4 or 7 characters long (including #): found `abc`"
137            );
138        }
139
140        #[test]
141        fn missing_hash() {
142            let err = HexColor::try_from("1234567").unwrap_err();
143            assert_snapshot!(
144                err,
145                @"hex color parsing error: No leading #: found `1234567`"
146            );
147        }
148
149        #[test]
150        fn invalid_hex_char() {
151            let err = HexColor::try_from("#abc12z").unwrap_err();
152            assert_snapshot!(err, @"Invalid character 'z' at position 5");
153        }
154    }
155
156    mod ok {
157        use super::*;
158
159        fn assert_hexcolor_roundtrip(color: &str) {
160            let ok: HexColor = color.parse().expect("color should be valid");
161            assert_eq!(ok.to_string(), color, "Color should roundtrip");
162        }
163
164        #[test]
165        fn good_variant() {
166            let h: HexColor = SlackColor::Good.into();
167            assert_snapshot!(h, @"good");
168        }
169
170        #[test]
171        fn danger_str() {
172            assert_hexcolor_roundtrip("danger");
173        }
174
175        #[test]
176        fn short_hex() {
177            assert_hexcolor_roundtrip("#d18");
178        }
179
180        #[test]
181        fn upper_hex() {
182            assert_hexcolor_roundtrip("#103D18");
183        }
184
185        #[test]
186        fn lower_hex() {
187            assert_hexcolor_roundtrip("#103d18");
188        }
189    }
190}