Skip to main content

ephemeris_forge_github/config/
custom_properties.rs

1use std::collections::BTreeSet;
2
3use facet::Facet;
4use facet_value::{VArray, Value};
5
6/// The value of one custom property on a repository.
7///
8/// On the wire a single value is a string, a multi-select value is an
9/// array of strings, and no value is null.
10#[derive(Clone, Debug, Eq, Facet, PartialEq)]
11#[facet(proxy = Value)]
12#[repr(u8)]
13pub enum PropertyValue {
14    /// A single value.
15    Single(String),
16    /// The values of a multi-select property.
17    Multiple(Vec<String>),
18    /// No value.
19    Unset,
20}
21
22/// A property value that [`PropertyValue`] cannot represent.
23#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
24pub enum PropertyValueError {
25    /// The value is not a string, an array of strings or null.
26    #[error("a property value is a string, an array of strings or null")]
27    Shape,
28}
29
30impl PropertyValue {
31    /// Whether both values mean the same, ignoring the order and repeats of
32    /// multi-select values.
33    #[must_use]
34    pub fn same_as(&self, other: &Self) -> bool {
35        match (self, other) {
36            (Self::Single(left), Self::Single(right)) => left == right,
37            (Self::Multiple(left), Self::Multiple(right)) => {
38                left.iter().collect::<BTreeSet<_>>() == right.iter().collect::<BTreeSet<_>>()
39            }
40            (Self::Unset, Self::Unset) => true,
41            (Self::Single(_) | Self::Multiple(_) | Self::Unset, _) => false,
42        }
43    }
44}
45
46impl TryFrom<Value> for PropertyValue {
47    type Error = PropertyValueError;
48
49    fn try_from(value: Value) -> Result<Self, Self::Error> {
50        if value.is_null() {
51            return Ok(Self::Unset);
52        }
53        if let Some(text) = value.as_string() {
54            return Ok(Self::Single(text.as_str().to_owned()));
55        }
56        let items = value.as_array().ok_or(PropertyValueError::Shape)?;
57        items
58            .iter()
59            .map(|item| {
60                item.as_string()
61                    .map(|text| text.as_str().to_owned())
62                    .ok_or(PropertyValueError::Shape)
63            })
64            .collect::<Result<_, _>>()
65            .map(Self::Multiple)
66    }
67}
68
69impl TryFrom<&PropertyValue> for Value {
70    type Error = PropertyValueError;
71
72    fn try_from(value: &PropertyValue) -> Result<Self, Self::Error> {
73        Ok(match value {
74            PropertyValue::Single(text) => Self::from(text.as_str()),
75            PropertyValue::Multiple(items) => items
76                .iter()
77                .map(|item| Self::from(item.as_str()))
78                .collect::<VArray>()
79                .into(),
80            PropertyValue::Unset => Self::NULL,
81        })
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::PropertyValue;
88
89    #[test]
90    fn the_three_shapes_decode_and_encode_back() {
91        for (input, expected) in [
92            (r#""team-a""#, PropertyValue::Single("team-a".to_owned())),
93            (
94                r#"["a","b"]"#,
95                PropertyValue::Multiple(vec!["a".to_owned(), "b".to_owned()]),
96            ),
97            ("null", PropertyValue::Unset),
98        ] {
99            let value: PropertyValue = facet_json::from_str(input).expect(input);
100            assert_eq!(value, expected);
101            assert_eq!(facet_json::to_string(&value).expect("encodes"), input);
102        }
103    }
104
105    #[test]
106    fn other_shapes_are_rejected() {
107        for input in ["3", "true", r#"["a",1]"#, r#"{"a":"b"}"#] {
108            facet_json::from_str::<PropertyValue>(input).expect_err(input);
109        }
110    }
111
112    #[test]
113    fn multiple_values_compare_as_sets() {
114        let left = PropertyValue::Multiple(vec!["a".to_owned(), "b".to_owned()]);
115        let right = PropertyValue::Multiple(vec!["b".to_owned(), "a".to_owned()]);
116        assert!(left.same_as(&right));
117        assert!(!left.same_as(&PropertyValue::Single("a".to_owned())));
118    }
119}