Files
movies-diary/crates/domain/src/value_objects/review.rs
Gabriel Kaszewski 6bf4ffc4ab
All checks were successful
CI / Check / Test (push) Successful in 1h5m56s
refactor: update rating validation logic — enforce minimum rating value
2026-07-10 13:22:21 +02:00

130 lines
3.6 KiB
Rust

use std::fmt;
use std::str::FromStr;
use crate::errors::DomainError;
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WatchMedium {
Cinema,
Streaming,
#[serde(rename = "tv")]
TV,
PhysicalMedia,
Download,
MediaServer,
Other,
}
impl fmt::Display for WatchMedium {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cinema => write!(f, "cinema"),
Self::Streaming => write!(f, "streaming"),
Self::TV => write!(f, "tv"),
Self::PhysicalMedia => write!(f, "physical_media"),
Self::Download => write!(f, "download"),
Self::MediaServer => write!(f, "media_server"),
Self::Other => write!(f, "other"),
}
}
}
impl WatchMedium {
pub fn label(&self) -> &'static str {
match self {
Self::Cinema => "Cinema",
Self::Streaming => "Streaming",
Self::TV => "TV",
Self::PhysicalMedia => "Physical Media",
Self::Download => "Download",
Self::MediaServer => "Media Server",
Self::Other => "Other",
}
}
}
impl FromStr for WatchMedium {
type Err = DomainError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"cinema" => Ok(Self::Cinema),
"streaming" => Ok(Self::Streaming),
"tv" => Ok(Self::TV),
"physical_media" => Ok(Self::PhysicalMedia),
"download" => Ok(Self::Download),
"media_server" => Ok(Self::MediaServer),
"other" => Ok(Self::Other),
_ => Err(DomainError::ValidationError(format!(
"unknown watch medium: {s}"
))),
}
}
}
pub fn parse_watched_at(s: &str) -> Result<chrono::NaiveDateTime, DomainError> {
chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")
.or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S"))
.or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M"))
.or_else(|_| {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
.map(|d| d.and_hms_opt(0, 0, 0).expect("midnight always valid"))
})
.map_err(|_| {
DomainError::ValidationError(format!(
"invalid date '{s}'; expected YYYY-MM-DD or YYYY-MM-DDTHH:MM[:SS]"
))
})
}
pub fn format_watched_at(dt: &chrono::NaiveDateTime) -> String {
dt.format("%Y-%m-%dT%H:%M:%S").to_string()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Rating(u8);
impl Rating {
const MIN: u8 = 1;
const MAX: u8 = 5;
pub fn new(value: u8) -> Result<Self, DomainError> {
if (Self::MIN..=Self::MAX).contains(&value) {
Ok(Self(value))
} else {
Err(DomainError::InvalidRating {
max: Self::MAX,
given: value,
})
}
}
pub fn value(&self) -> u8 {
self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Comment(String);
impl Comment {
const MAX_LENGTH: usize = 10_000;
pub fn new(comment: String) -> Result<Self, DomainError> {
let trimmed = comment.trim();
if trimmed.len() > Self::MAX_LENGTH {
Err(DomainError::ValidationError(format!(
"Comment exceeds {} characters",
Self::MAX_LENGTH
)))
} else {
Ok(Self(trimmed.to_string()))
}
}
pub fn value(&self) -> &str {
&self.0
}
}