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