1use std::collections::BTreeMap;
4use std::fmt;
5
6use facet::Facet;
7
8use crate::Mode;
9
10#[derive(Clone, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
13#[facet(proxy = String)]
14pub struct LabelColor(String);
15
16#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
18#[error("label color {value:?} is not six hexadecimal digits")]
19pub struct InvalidLabelColor {
20 pub value: String,
22}
23
24impl LabelColor {
25 #[must_use]
27 pub fn as_str(&self) -> &str {
28 &self.0
29 }
30
31 #[must_use]
35 pub fn matches(&self, reported: &str) -> bool {
36 reported
37 .trim_start_matches('#')
38 .eq_ignore_ascii_case(&self.0)
39 }
40}
41
42impl TryFrom<String> for LabelColor {
43 type Error = InvalidLabelColor;
44
45 fn try_from(value: String) -> Result<Self, Self::Error> {
46 if value.len() == 6 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
47 Ok(Self(value.to_ascii_lowercase()))
48 } else {
49 Err(InvalidLabelColor { value })
50 }
51 }
52}
53
54impl TryFrom<&LabelColor> for String {
55 type Error = InvalidLabelColor;
56
57 fn try_from(value: &LabelColor) -> Result<Self, Self::Error> {
58 Ok(value.0.clone())
59 }
60}
61
62impl fmt::Display for LabelColor {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 self.0.fmt(f)
65 }
66}
67
68#[derive(Clone, Debug, Eq, Facet, PartialEq)]
70#[facet(deny_unknown_fields)]
71pub struct Label {
72 pub color: LabelColor,
74 pub description: Option<String>,
76}
77
78#[derive(Clone, Debug, Eq, Facet, PartialEq)]
82#[facet(deny_unknown_fields)]
83pub struct Labels<L: 'static> {
84 pub mode: Mode,
86 pub labels: BTreeMap<String, L>,
88}
89
90#[cfg(test)]
91mod tests {
92 use super::{Label, LabelColor, Labels};
93
94 #[test]
95 fn colors_are_six_hex_digits_in_lowercase() {
96 let color = LabelColor::try_from("D73A4A".to_owned()).expect("valid");
97 assert_eq!(color.as_str(), "d73a4a");
98 assert!(color.matches("#D73A4A"));
99 for bad in ["", "fff", "#d73a4a", "d73a4g", "d73a4a0"] {
100 LabelColor::try_from(bad.to_owned()).expect_err(bad);
101 }
102 }
103
104 #[test]
105 fn labels_decode_and_reject_unknown_keys() {
106 let labels: Labels<Label> = facet_json::from_str(
107 r#"{"mode":"owned","labels":{"bug":{"color":"d73a4a","description":null}}}"#,
108 )
109 .expect("decodes");
110 assert_eq!(labels.labels.len(), 1);
111 facet_json::from_str::<Labels<Label>>(
112 r#"{"mode":"owned","labels":{"bug":{"color":"d73a4a","description":null,"x":1}}}"#,
113 )
114 .expect_err("unknown key");
115 }
116}