use std::mem::discriminant;
use serde::{Deserializer, Serializer};
use crate::{Cast, ColumnReader, ColumnSpec, Error, Result, SqlValue};
pub trait Variants: Copy + 'static {
const TYPE: &'static str;
const PATH: &'static str = Self::TYPE;
const VARIANTS: &'static [(&'static str, Self)];
const TRANSITIONS: &'static [(Self, Self)] = &[];
#[must_use]
fn name(self) -> &'static str {
Self::VARIANTS
.iter()
.find(|(_, variant)| same(*variant, self))
.map(|(name, _)| *name)
.expect("`#[derive(Enum)]` lists every variant")
}
fn to(self, next: Self, field: &str) -> Result<Self> {
let allowed = Self::TRANSITIONS
.iter()
.any(|(source, target)| same(*source, self) && same(*target, next));
if allowed {
crate::moves::record(Self::PATH, self.name(), next.name());
return Ok(next);
}
Err(crate::moves::refused(field, self.name(), next.name()))
}
}
pub const COLUMNS: &[ColumnSpec] = String::COLUMNS;
fn same<E: Variants>(left: E, right: E) -> bool {
discriminant(&left) == discriminant(&right)
}
#[must_use]
pub fn parse<E: Variants>(name: &str) -> Option<E> {
E::VARIANTS
.iter()
.find(|(candidate, _)| *candidate == name)
.map(|(_, variant)| *variant)
}
pub fn encode<E: Variants>(value: E, out: &mut Vec<SqlValue>) {
out.push(SqlValue::Text(value.name().to_owned()));
}
pub fn decode<E: Variants>(row: &mut dyn ColumnReader) -> Result<E> {
let stored = row.text()?;
parse(stored)
.ok_or_else(|| Error::internal_message(format!("`{stored}` is not a stored {}", E::TYPE)))
}
pub fn serialize<E: Variants, S: Serializer>(value: E, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(value.name())
}
pub fn deserialize<'de, E: Variants, D: Deserializer<'de>>(deserializer: D) -> Result<E, D::Error> {
crate::deserialize_parsed(deserializer, "a variant name", parse_given)
}
fn parse_given<E: Variants>(given: &str) -> Result<E, String> {
parse(given).ok_or_else(|| {
let accepted: Vec<&str> = E::VARIANTS.iter().map(|(name, _)| *name).collect();
format!(
"`{given}` is not a {}, expected one of {}",
E::TYPE,
accepted.join(", ")
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Copy, Debug, PartialEq)]
enum Light {
Green,
Red,
}
impl Variants for Light {
const TYPE: &'static str = "Light";
const VARIANTS: &'static [(&'static str, Self)] =
&[("GREEN", Self::Green), ("RED", Self::Red)];
const TRANSITIONS: &'static [(Self, Self)] = &[(Self::Green, Self::Red)];
}
#[test]
fn names_and_parses_variants() {
assert_eq!(Light::Red.name(), "RED");
assert_eq!(parse::<Light>("GREEN"), Some(Light::Green));
assert_eq!(parse::<Light>("BLUE"), None);
}
#[test]
fn decodes_a_stored_name_and_refuses_an_unknown_one() {
let values = [
SqlValue::Text("RED".to_owned()),
SqlValue::Text("BLUE".to_owned()),
];
let mut row = crate::RowReader::new(&values);
assert_eq!(decode::<Light>(&mut row).unwrap(), Light::Red);
assert!(matches!(
decode::<Light>(&mut row),
Err(Error::Internal { .. })
));
}
#[test]
fn deserializes_a_borrowed_or_escaped_name_and_names_the_accepted_ones() {
let read = |json: &str| {
deserialize::<Light, _>(&mut serde_json::Deserializer::from_str(json))
.map_err(|error| error.to_string())
};
assert_eq!(read("\"RED\""), Ok(Light::Red));
assert_eq!(read("\"R\\u0045D\""), Ok(Light::Red));
let refusal = read("\"BLUE\"").unwrap_err();
assert!(
refusal.starts_with("`BLUE` is not a Light, expected one of GREEN, RED"),
"{refusal}"
);
}
#[test]
fn allows_only_listed_transitions() {
assert_eq!(Light::Green.to(Light::Red, "light").unwrap(), Light::Red);
let Err(Error::Invalid(refused)) = Light::Red.to(Light::Green, "signal") else {
panic!("an unlisted move is a field error");
};
assert_eq!(
refused,
[
crate::FieldError::new(
"signal",
"validation.move",
"cannot move from RED to GREEN"
)
.with("from", "RED")
.with("to", "GREEN")
]
);
}
}