Skip to main content

sofie_core/
variants.rs

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