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