All checks were successful
CI / Check / Test (push) Successful in 1h5m56s
130 lines
3.6 KiB
Rust
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
|
|
}
|
|
}
|