1use crate::error::Error;
2use std::{convert::TryFrom, fmt, str::FromStr};
3
4use hex::FromHex;
5use serde::Serialize;
6
7#[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
34impl 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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
46pub enum SlackColor {
47 Good,
49 Warning,
51 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 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 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}