use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Rating(pub i8);
impl Rating {
pub const MAX: i8 = 10;
pub const MIN: i8 = 0;
pub fn new(value: i8) -> Result<Self, RatingError> {
if !(Self::MIN..=Self::MAX).contains(&value) {
return Err(RatingError::OutOfRange { value });
}
Ok(Rating(value))
}
pub const fn from_raw(value: i8) -> Self {
Rating(value)
}
pub fn to_f32(self) -> f32 {
self.0 as f32 / 2.0
}
pub const fn raw(self) -> i8 {
self.0
}
}
#[derive(Debug, thiserror::Error)]
pub enum RatingError {
#[error("rating out of range (0..=10): got {value}")]
OutOfRange { value: i8 },
}
impl Serialize for Rating {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_i8(self.0)
}
}
impl<'de> Deserialize<'de> for Rating {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = i8::deserialize(deserializer)?;
Rating::new(v).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_rejects_out_of_range() {
assert!(Rating::new(-1).is_err());
assert!(Rating::new(0).is_ok());
assert!(Rating::new(10).is_ok());
assert!(Rating::new(11).is_err());
assert!(Rating::new(20).is_err());
}
#[test]
fn to_f32_maps_to_zero_to_five() {
assert_eq!(Rating::new(0).unwrap().to_f32(), 0.0);
assert_eq!(Rating::new(2).unwrap().to_f32(), 1.0);
assert!((Rating::new(10).unwrap().to_f32() - 5.0).abs() < f32::EPSILON);
}
}