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 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}