Skip to main content

sofie_core/
variants.rs

1use std::mem::discriminant;
2
3use serde::{Deserializer, Serializer};
4
5use crate::{Cast, ColumnReader, ColumnSpec, Error, Result, SqlValue};
6
7pub trait Variants: Copy + 'static {
8    const TYPE: &'static str;
9    /// Module-qualified, so two enums of one name keep their recorded moves apart.
10    const PATH: &'static str = Self::TYPE;
11    const VARIANTS: &'static [(&'static str, Self)];
12    const TRANSITIONS: &'static [(Self, Self)] = &[];
13
14    #[must_use]
15    fn name(self) -> &'static str {
16        Self::VARIANTS
17            .iter()
18            .find(|(_, variant)| same(*variant, self))
19            .map(|(name, _)| *name)
20            .expect("`#[derive(Enum)]` lists every variant")
21    }
22
23    /// A refused move is an error on `field`, the one holding the value.
24    fn to(self, next: Self, field: &str) -> Result<Self> {
25        let allowed = Self::TRANSITIONS
26            .iter()
27            .any(|(source, target)| same(*source, self) && same(*target, next));
28        if allowed {
29            crate::moves::record(Self::PATH, self.name(), next.name());
30            return Ok(next);
31        }
32        Err(crate::moves::refused(field, self.name(), next.name()))
33    }
34}
35
36pub const COLUMNS: &[ColumnSpec] = String::COLUMNS;
37
38fn same<E: Variants>(left: E, right: E) -> bool {
39    discriminant(&left) == discriminant(&right)
40}
41
42#[must_use]
43pub fn parse<E: Variants>(name: &str) -> Option<E> {
44    E::VARIANTS
45        .iter()
46        .find(|(candidate, _)| *candidate == name)
47        .map(|(_, variant)| *variant)
48}
49
50pub fn encode<E: Variants>(value: E, out: &mut Vec<SqlValue>) {
51    out.push(SqlValue::Text(value.name().to_owned()));
52}
53
54pub fn decode<E: Variants>(row: &mut dyn ColumnReader) -> Result<E> {
55    let stored = row.text()?;
56    parse(stored)
57        .ok_or_else(|| Error::internal_message(format!("`{stored}` is not a stored {}", E::TYPE)))
58}
59
60pub fn serialize<E: Variants, S: Serializer>(value: E, serializer: S) -> Result<S::Ok, S::Error> {
61    serializer.serialize_str(value.name())
62}
63
64pub fn deserialize<'de, E: Variants, D: Deserializer<'de>>(deserializer: D) -> Result<E, D::Error> {
65    crate::deserialize_parsed(deserializer, "a variant name", parse_given)
66}
67
68fn parse_given<E: Variants>(given: &str) -> Result<E, String> {
69    parse(given).ok_or_else(|| {
70        let accepted: Vec<&str> = E::VARIANTS.iter().map(|(name, _)| *name).collect();
71        format!(
72            "`{given}` is not a {}, expected one of {}",
73            E::TYPE,
74            accepted.join(", ")
75        )
76    })
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[derive(Clone, Copy, Debug, PartialEq)]
84    enum Light {
85        Green,
86        Red,
87    }
88
89    impl Variants for Light {
90        const TYPE: &'static str = "Light";
91        const VARIANTS: &'static [(&'static str, Self)] =
92            &[("GREEN", Self::Green), ("RED", Self::Red)];
93        const TRANSITIONS: &'static [(Self, Self)] = &[(Self::Green, Self::Red)];
94    }
95
96    #[test]
97    fn names_and_parses_variants() {
98        assert_eq!(Light::Red.name(), "RED");
99        assert_eq!(parse::<Light>("GREEN"), Some(Light::Green));
100        assert_eq!(parse::<Light>("BLUE"), None);
101    }
102
103    #[test]
104    fn decodes_a_stored_name_and_refuses_an_unknown_one() {
105        let values = [
106            SqlValue::Text("RED".to_owned()),
107            SqlValue::Text("BLUE".to_owned()),
108        ];
109        let mut row = crate::RowReader::new(&values);
110
111        assert_eq!(decode::<Light>(&mut row).unwrap(), Light::Red);
112        assert!(matches!(
113            decode::<Light>(&mut row),
114            Err(Error::Internal { .. })
115        ));
116    }
117
118    #[test]
119    fn deserializes_a_borrowed_or_escaped_name_and_names_the_accepted_ones() {
120        let read = |json: &str| {
121            deserialize::<Light, _>(&mut serde_json::Deserializer::from_str(json))
122                .map_err(|error| error.to_string())
123        };
124
125        assert_eq!(read("\"RED\""), Ok(Light::Red));
126        assert_eq!(read("\"R\\u0045D\""), Ok(Light::Red));
127        let refusal = read("\"BLUE\"").unwrap_err();
128        assert!(
129            refusal.starts_with("`BLUE` is not a Light, expected one of GREEN, RED"),
130            "{refusal}"
131        );
132    }
133
134    #[test]
135    fn allows_only_listed_transitions() {
136        assert_eq!(Light::Green.to(Light::Red, "light").unwrap(), Light::Red);
137        let Err(Error::Invalid(refused)) = Light::Red.to(Light::Green, "signal") else {
138            panic!("an unlisted move is a field error");
139        };
140        assert_eq!(
141            refused,
142            [
143                crate::FieldError::new(
144                    "signal",
145                    "validation.move",
146                    "cannot move from RED to GREEN"
147                )
148                .with("from", "RED")
149                .with("to", "GREEN")
150            ]
151        );
152    }
153}