use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};
use crate::ids::{ContentId, ContentIdPrefix, GovernanceLogId, PostId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Shown {
pub read: String,
pub bytes: u64,
pub raw: u64,
pub post: Option<PostId>,
}
#[derive(Debug, Default)]
struct Inner {
shown: HashMap<ContentId, Shown>,
pending: Vec<(ContentId, Shown)>,
read: String,
legends: HashSet<&'static str>,
pending_legends: Vec<&'static str>,
summaries: HashMap<GovernanceLogId, Shown>,
pending_summaries: Vec<(GovernanceLogId, Shown)>,
parts: HashMap<String, Shown>,
pending_parts: Vec<(String, Shown)>,
cues: Cues,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Cues {
pub links: bool,
pub full_reads_left: Option<u64>,
}
#[derive(Debug, Clone, Default)]
pub struct ShownOnce(Arc<RwLock<Inner>>);
impl ShownOnce {
pub fn begin(&self, read: impl Into<String>) {
let mut inner = self.0.write().expect("shown-once lock");
inner.pending.clear();
inner.pending_legends.clear();
inner.pending_summaries.clear();
inner.pending_parts.clear();
inner.read = read.into();
}
pub fn earlier_part(&self, link: &str) -> Option<Shown> {
let inner = self.0.read().expect("shown-once lock");
inner.parts.get(link).cloned().or_else(|| {
inner
.pending_parts
.iter()
.find(|(p, _)| p == link)
.map(|(_, s)| s.clone())
})
}
pub fn mark_part(&self, link: String, bytes: usize) {
let mut inner = self.0.write().expect("shown-once lock");
let read = inner.read.clone();
inner.pending_parts.push((
link,
Shown {
read,
bytes: bytes as u64,
raw: bytes as u64,
post: None,
},
));
}
pub fn set_cues(&self, cues: Cues) {
self.0.write().expect("shown-once lock").cues = cues;
}
pub fn cues(&self) -> Cues {
self.0.read().expect("shown-once lock").cues
}
pub fn earlier_summary(&self, id: &GovernanceLogId) -> Option<Shown> {
let inner = self.0.read().expect("shown-once lock");
inner.summaries.get(id).cloned().or_else(|| {
inner
.pending_summaries
.iter()
.find(|(p, _)| p == id)
.map(|(_, s)| s.clone())
})
}
pub fn mark_summary(&self, id: GovernanceLogId, bytes: usize) {
let mut inner = self.0.write().expect("shown-once lock");
let read = inner.read.clone();
inner.pending_summaries.push((
id,
Shown {
read,
bytes: bytes as u64,
raw: bytes as u64,
post: None,
},
));
}
pub fn legend_shown(&self, legend: &'static str) -> bool {
let inner = self.0.read().expect("shown-once lock");
inner.legends.contains(legend)
|| inner.pending_legends.contains(&legend)
}
pub fn legend(&self, legend: &'static str) -> bool {
let mut inner = self.0.write().expect("shown-once lock");
if inner.legends.contains(legend)
|| inner.pending_legends.contains(&legend)
{
return false;
}
inner.pending_legends.push(legend);
true
}
pub fn read_of(id: impl std::fmt::Display) -> String {
format!("your read of {id}")
}
pub fn read_of_content(id: ContentId) -> String {
Self::read_of(ContentIdPrefix::from(id))
}
pub fn earlier(&self, id: impl Into<ContentId>) -> Option<Shown> {
let id = id.into();
let inner = self.0.read().expect("shown-once lock");
inner.shown.get(&id).cloned().or_else(|| {
inner
.pending
.iter()
.find(|(p, _)| *p == id)
.map(|(_, s)| s.clone())
})
}
pub fn contains(&self, id: impl Into<ContentId>) -> bool {
self.earlier(id).is_some()
}
pub fn mark(
&self,
id: impl Into<ContentId>,
bytes: usize,
raw: usize,
post: Option<PostId>,
) {
let mut inner = self.0.write().expect("shown-once lock");
let read = inner.read.clone();
inner.pending.push((
id.into(),
Shown {
read,
bytes: bytes as u64,
raw: raw as u64,
post,
},
));
}
pub fn commit(&self) {
let mut inner = self.0.write().expect("shown-once lock");
let pending = std::mem::take(&mut inner.pending);
for (id, shown) in pending {
inner.shown.entry(id).or_insert(shown);
}
let legends = std::mem::take(&mut inner.pending_legends);
inner.legends.extend(legends);
let summaries = std::mem::take(&mut inner.pending_summaries);
for (id, shown) in summaries {
inner.summaries.entry(id).or_insert(shown);
}
let parts = std::mem::take(&mut inner.pending_parts);
for (link, shown) in parts {
inner.parts.entry(link).or_insert(shown);
}
}
pub fn discard(&self) {
let mut inner = self.0.write().expect("shown-once lock");
inner.pending.clear();
inner.pending_legends.clear();
inner.pending_summaries.clear();
inner.pending_parts.clear();
}
pub fn comments_of(&self, post: PostId) -> (u64, u64) {
let inner = self.0.read().expect("shown-once lock");
inner
.shown
.values()
.filter(|s| s.post == Some(post))
.fold((0, 0), |(n, b), s| (n + 1, b + s.raw))
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
fn id(n: u128) -> ContentId {
ContentId::from(Uuid::from_u128(n))
}
#[test]
fn marked_items_are_shown_once_committed() {
let once = ShownOnce::default();
once.begin("your read of 00000000");
once.mark(id(1), 100, 100, None);
assert!(once.contains(id(1)));
once.commit();
let shown = once.earlier(id(1)).unwrap();
assert_eq!(shown.read, "your read of 00000000");
assert_eq!(shown.bytes, 100);
}
#[test]
fn a_discarded_result_marks_nothing() {
let once = ShownOnce::default();
once.begin("a");
once.mark(id(1), 100, 100, None);
once.discard();
assert!(!once.contains(id(1)));
once.begin("b");
once.mark(id(2), 1, 1, None);
once.begin("c");
once.commit();
assert!(!once.contains(id(2)));
}
#[test]
fn a_legend_is_shown_once() {
let once = ShownOnce::default();
once.begin("a");
assert!(once.legend("x"));
assert!(!once.legend("x"), "not twice in one result");
once.discard();
once.begin("b");
assert!(once.legend("x"));
once.commit();
once.begin("c");
assert!(!once.legend("x"));
}
#[test]
fn the_first_showing_is_kept() {
let once = ShownOnce::default();
once.begin("first");
once.mark(id(1), 10, 10, None);
once.commit();
once.begin("second");
once.mark(id(1), 20, 20, None);
once.commit();
assert_eq!(once.earlier(id(1)).unwrap().read, "first");
}
#[test]
fn a_posts_comments_are_counted() {
let once = ShownOnce::default();
let post = PostId::from(Uuid::from_u128(9));
once.begin("r");
once.mark(id(1), 10, 10, Some(post));
once.mark(id(2), 30, 30, Some(post));
once.mark(id(3), 50, 50, None);
once.commit();
assert_eq!(once.comments_of(post), (2, 40));
}
}