ephemeris_forge_github/config/
custom_properties.rs1use std::collections::BTreeSet;
2
3use facet::Facet;
4use facet_value::{VArray, Value};
5
6#[derive(Clone, Debug, Eq, Facet, PartialEq)]
11#[facet(proxy = Value)]
12#[repr(u8)]
13pub enum PropertyValue {
14 Single(String),
16 Multiple(Vec<String>),
18 Unset,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
24pub enum PropertyValueError {
25 #[error("a property value is a string, an array of strings or null")]
27 Shape,
28}
29
30impl PropertyValue {
31 #[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}