1use crate::error::{Error, ErrorKind};
2use std::{convert::TryFrom, str::FromStr};
3
4use hex::FromHex;
5use serde::Serialize;
6
7#[derive(Serialize, Debug, Clone, PartialEq)]
14pub struct HexColor(String);
15impl HexColor {
16 fn new<S: Into<String>>(s: S) -> HexColor {
17 HexColor(s.into())
18 }
19}
20
21impl Default for HexColor {
22 fn default() -> HexColor {
23 HexColor::new("#000")
24 }
25}
26
27impl ::std::fmt::Display for HexColor {
28 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
29 write!(f, "{}", self.0)
30 }
31}
32
33impl TryFrom<&str> for HexColor {
34 type Error = Error;
35
36 fn try_from(s: &str) -> Result<Self, Self::Error> {
37 s.parse()
38 }
39}
40
41#[derive(Copy, Clone, Debug, PartialEq, Eq)]
44pub enum SlackColor {
45 Good,
47 Warning,
49 Danger,
51}
52
53const SLACK_COLORS: [&str; 3] = ["good", "warning", "danger"];
54
55impl ::std::fmt::Display for SlackColor {
56 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
57 write!(f, "{}", self.as_ref())
58 }
59}
60
61impl AsRef<str> for SlackColor {
62 fn as_ref(&self) -> &str {
63 match *self {
64 SlackColor::Good => "good",
65 SlackColor::Warning => "warning",
66 SlackColor::Danger => "danger",
67 }
68 }
69}
70
71impl From<SlackColor> for HexColor {
72 fn from(color: SlackColor) -> HexColor {
73 HexColor::new(color.to_string())
74 }
75}
76
77impl FromStr for HexColor {
78 type Err = Error;
79
80 fn from_str(s: &str) -> Result<Self, Self::Err> {
81 let s: String = s.into();
82 if SLACK_COLORS.contains(&&s[..]) {
83 return Ok(HexColor(s));
84 }
85
86 let num_chars = s.chars().count();
87 if num_chars != 7 && num_chars != 4 {
88 return Err(ErrorKind::HexColor(format!(
89 "Must be 4 or 7 characters long (including #): \
90 found `{}`",
91 s
92 ))
93 .into());
94 }
95 if !s.starts_with('#') {
96 return Err(ErrorKind::HexColor(format!("No leading #: found `{}`", s)).into());
97 }
98
99 let hex = if num_chars == 4 {
101 s.chars().skip(1).fold(String::from("#"), |mut s, c| {
102 s.push(c);
103 s.push(c);
104 s
105 })
106 } else {
107 s.clone()
108 };
109
110 match Vec::from_hex(&hex[1..]) {
112 Ok(_) => Ok(HexColor::new(s)),
113 Err(e) => Err(e.into()),
114 }
115 }
116}
117
118#[cfg(test)]
119mod test {
120 use super::*;
121 use crate::HexColor;
122 use std::convert::TryFrom;
123
124 #[test]
125 fn test_hex_color_too_short() {
126 let err = HexColor::try_from("abc").unwrap_err();
127 assert_eq!(
128 err.to_string(),
129 "hex color parsing error: Must be 4 or 7 characters long (including #): found \
130 `abc`"
131 );
132 }
133
134 #[test]
135 fn test_hex_color_missing_hash() {
136 let err = HexColor::try_from("1234567").unwrap_err();
137 assert_eq!(
138 err.to_string(),
139 "hex color parsing error: No leading #: found `1234567`"
140 );
141 }
142
143 #[test]
144 fn test_hex_color_invalid_hex_fmt() {
145 let err = HexColor::try_from("#abc12z").unwrap_err();
146 assert!(err
147 .to_string()
148 .contains("Invalid character 'z' at position 5"));
149 }
150
151 #[test]
152 fn test_hex_color_good() {
153 let h: HexColor = HexColor::try_from(SlackColor::Good).unwrap();
154 assert_eq!(h.to_string(), "good");
155 }
156
157 #[test]
158 fn test_hex_color_danger_str() {
159 let ok = HexColor::try_from("danger").unwrap();
160 assert_eq!(ok.to_string(), "danger");
161 }
162
163 #[test]
164 fn test_hex_color_3_char_hex() {
165 let ok = HexColor::try_from("#d18").unwrap();
166 assert_eq!(ok.to_string(), "#d18");
167 }
168
169 #[test]
170 fn test_hex_color_valid_upper_hex() {
171 let ok = HexColor::try_from("#103D18").unwrap();
172 assert_eq!(ok.to_string(), "#103D18");
173 }
174
175 #[test]
176 fn test_hex_color_valid_lower_hex() {
177 let ok = HexColor::try_from("#103d18").unwrap();
178 assert_eq!(ok.to_string(), "#103d18");
179 }
180}