Files
ELY-Browser/crates/ely_domain/src/reading_list.rs
T

167 lines
3.9 KiB
Rust

use std::time::SystemTime;
use crate::{DomainError, ProfileId, ReadingListId, SpaceId, UrlText};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ReadingProgressPercent(u8);
impl ReadingProgressPercent {
pub fn new(value: u8) -> Result<Self, DomainError> {
if !(1..=99).contains(&value) {
return Err(DomainError::InvalidReadingProgressPercent { value: value.to_string() });
}
Ok(Self(value))
}
#[must_use]
pub fn value(self) -> u8 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReadingProgress {
Unread,
InProgress(ReadingProgressPercent),
Finished,
}
impl ReadingProgress {
#[must_use]
pub fn label(self) -> String {
match self {
Self::Unread => "Unread".to_string(),
Self::InProgress(percent) => format!("{}% read", percent.value()),
Self::Finished => "Read".to_string(),
}
}
#[must_use]
pub fn action_label(self) -> &'static str {
match self {
Self::Unread | Self::InProgress(_) => "Mark Read",
Self::Finished => "Mark Unread",
}
}
#[must_use]
pub fn toggled(self) -> Self {
match self {
Self::Unread | Self::InProgress(_) => Self::Finished,
Self::Finished => Self::Unread,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReadingListEntry {
id: ReadingListId,
profile_id: ProfileId,
space_id: SpaceId,
title: String,
source_url: UrlText,
progress: ReadingProgress,
added_at: SystemTime,
}
impl ReadingListEntry {
pub fn new(
profile_id: ProfileId,
space_id: SpaceId,
title: impl Into<String>,
source_url: UrlText,
added_at: SystemTime,
) -> Result<Self, DomainError> {
let title = non_empty_text("reading list title", title.into())?;
Ok(Self {
id: ReadingListId::new(),
profile_id,
space_id,
title,
source_url,
progress: ReadingProgress::Unread,
added_at,
})
}
#[must_use]
pub fn id(&self) -> &ReadingListId {
&self.id
}
#[must_use]
pub fn profile_id(&self) -> &ProfileId {
&self.profile_id
}
#[must_use]
pub fn space_id(&self) -> &SpaceId {
&self.space_id
}
#[must_use]
pub fn title(&self) -> &str {
&self.title
}
#[must_use]
pub fn source_url(&self) -> &UrlText {
&self.source_url
}
#[must_use]
pub fn display_url(&self) -> String {
self.source_url.display_url()
}
#[must_use]
pub fn progress(&self) -> &ReadingProgress {
&self.progress
}
pub fn set_progress(&mut self, progress: ReadingProgress) {
self.progress = progress;
}
#[must_use]
pub fn added_at(&self) -> SystemTime {
self.added_at
}
}
fn non_empty_text(field: &'static str, value: String) -> Result<String, DomainError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(DomainError::EmptyField { field });
}
Ok(trimmed.to_string())
}
#[cfg(test)]
mod tests {
use super::{ReadingProgress, ReadingProgressPercent};
use crate::DomainError;
#[test]
fn reading_progress_percent_accepts_partial_progress() -> Result<(), DomainError> {
let percent = ReadingProgressPercent::new(42)?;
assert_eq!(percent.value(), 42);
assert_eq!(ReadingProgress::InProgress(percent).label(), "42% read");
Ok(())
}
#[test]
fn reading_progress_percent_rejects_terminal_values() {
assert_eq!(
ReadingProgressPercent::new(0),
Err(DomainError::InvalidReadingProgressPercent { value: "0".to_string() })
);
assert_eq!(
ReadingProgressPercent::new(100),
Err(DomainError::InvalidReadingProgressPercent { value: "100".to_string() })
);
}
}