Skip to main content

sofie_core/
variants.rs

1use std::mem::discriminant;
2
3use serde::{Deserializer, Serializer};
4
5use crate::case::snake_case;
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
40pub const COLUMNS: &[ColumnSpec] = String::COLUMNS;
41
42fn same<E: Variants>(left: E, right: E) -> bool {
43    discriminant(&left) == discriminant(&right)
44}
45
46#[must_use]
47pub fn parse<E: Variants>(name: &str) -> Option<E> {
48    E::VARIANTS
49        .iter()
50        .find(|(candidate, _)| *candidate == name)
51        .map(|(_, variant)| *variant)
52}
53
54pub fn encode<E: Variants>(value: E, out: &mut Vec<SqlValue>) {
55    out.push(SqlValue::Text(value.name().to_owned()));
56}
57
58pub fn decode<E: Variants>(row: &mut dyn ColumnReader) -> Result<E> {
59    let stored = row.text()?;
60    parse(stored)
61        .ok_or_else(|| Error::internal_message(format!("`{stored}` is not a stored {}", E::TYPE)))
62}
63
64pub fn serialize<E: Variants, S: Serializer>(value: E, serializer: S) -> Result<S::Ok, S::Error> {
65    serializer.serialize_str(value.name())
66}
67
68pub fn deserialize<'de, E: Variants, D: Deserializer<'de>>(deserializer: D) -> Result<E, D::Error> {
69    crate::deserialize_parsed(deserializer, "a variant name", parse_given)
70}
71
72fn parse_given<E: Variants>(given: &str) -> Result<E, String> {
73    parse(given).ok_or_else(|| {
74        let accepted: Vec<&str> = E::VARIANTS.iter().map(|(name, _)| *name).collect();
75        format!(
76            "`{given}` is not a {}, expected one of {}",
77            E::TYPE,
78            accepted.join(", ")
79        )
80    })
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[derive(Clone, Copy, Debug, PartialEq)]
88    enum Light {
89        Green,
90        Red,
91    }
92
93    impl Variants for Light {
94        const TYPE: &'static str = "Light";
95        const VARIANTS: &'static [(&'static str, Self)] =
96            &[("GREEN", Self::Green), ("RED", Self::Red)];
97        const TRANSITIONS: &'static [(Self, Self)] = &[(Self::Green, Self::Red)];
98    }
99
100    #[test]
101    fn names_and_parses_variants() {
102        assert_eq!(Light::Red.name(), "RED");
103        assert_eq!(parse::<Light>("GREEN"), Some(Light::Green));
104        assert_eq!(parse::<Light>("BLUE"), None);
105    }
106
107    #[test]
108    fn decodes_a_stored_name_and_refuses_an_unknown_one() {
109        let values = [
110            SqlValue::Text("RED".to_owned()),
111            SqlValue::Text("BLUE".to_owned()),
112        ];
113        let mut row = crate::RowReader::new(&values);
114
115        assert_eq!(decode::<Light>(&mut row).unwrap(), Light::Red);
116        assert!(matches!(
117            decode::<Light>(&mut row),
118            Err(Error::Internal { .. })
119        ));
120    }
121
122    #[test]
123    fn deserializes_a_borrowed_or_escaped_name_and_names_the_accepted_ones() {
124        let read = |json: &str| {
125            deserialize::<Light, _>(&mut serde_json::Deserializer::from_str(json))
126                .map_err(|error| error.to_string())
127        };
128
129        assert_eq!(read("\"RED\""), Ok(Light::Red));
130        assert_eq!(read("\"R\\u0045D\""), Ok(Light::Red));
131        let refusal = read("\"BLUE\"").unwrap_err();
132        assert!(
133            refusal.starts_with("`BLUE` is not a Light, expected one of GREEN, RED"),
134            "{refusal}"
135        );
136    }
137
138    #[test]
139    fn allows_only_listed_transitions() {
140        assert_eq!(Light::Green.to(Light::Red).unwrap(), Light::Red);
141        let Err(Error::Invalid(refused)) = Light::Red.to(Light::Green) else {
142            panic!("an unlisted move is a field error");
143        };
144        assert_eq!(
145            refused,
146            [
147                crate::FieldError::new("light", "validation.move", "cannot move from RED to GREEN")
148                    .with("from", "RED")
149                    .with("to", "GREEN")
150            ]
151        );
152    }
153}