progscrape_application/story/
id.rs

1use serde::{Deserialize, Serialize};
2
3use std::fmt::Display;
4
5use progscrape_scrapers::{StoryDate, StoryUrlNorm};
6
7use crate::Shard;
8
9/// Uniquely identifies a story within the index.
10#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
11pub struct StoryIdentifier {
12    pub norm: StoryUrlNorm,
13    date: (u16, u8, u8),
14}
15
16impl Display for StoryIdentifier {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        f.write_fmt(format_args!(
19            "{}:{}:{}:{}",
20            self.date.0,
21            self.date.1,
22            self.date.2,
23            self.norm.string()
24        ))
25    }
26}
27
28impl StoryIdentifier {
29    const BASE64_CONFIG: base64::engine::GeneralPurpose =
30        base64::engine::general_purpose::URL_SAFE_NO_PAD;
31
32    pub fn new(date: StoryDate, norm: &StoryUrlNorm) -> Self {
33        Self {
34            norm: norm.clone(),
35            date: (date.year() as u16, date.month() as u8, date.day() as u8),
36        }
37    }
38
39    pub fn update_date(&mut self, date: StoryDate) {
40        self.date = (date.year() as u16, date.month() as u8, date.day() as u8);
41    }
42
43    pub fn matches_date(&self, date: StoryDate) -> bool {
44        (self.date.0, self.date.1, self.date.2)
45            == (date.year() as u16, date.month() as u8, date.day() as u8)
46    }
47
48    pub fn to_base64(&self) -> String {
49        use base64::Engine;
50        Self::BASE64_CONFIG.encode(self.to_string().as_bytes())
51    }
52
53    pub fn from_base64<T: AsRef<[u8]>>(s: T) -> Option<Self> {
54        // Use an inner function so we can make use of ? (is there an easier way?)
55        fn from_base64_res<T: AsRef<[u8]>>(s: T) -> Result<StoryIdentifier, ()> {
56            use base64::Engine;
57            let s = StoryIdentifier::BASE64_CONFIG.decode(s).map_err(drop)?;
58            let s = String::from_utf8(s).map_err(drop)?;
59            let mut bits = s.splitn(4, ':');
60            let year = bits.next().ok_or(())?;
61            let month = bits.next().ok_or(())?;
62            let day = bits.next().ok_or(())?;
63            let norm = bits.next().ok_or(())?.to_owned();
64            Ok(StoryIdentifier {
65                norm: StoryUrlNorm::from_string(norm),
66                date: (
67                    year.parse().map_err(drop)?,
68                    month.parse().map_err(drop)?,
69                    day.parse().map_err(drop)?,
70                ),
71            })
72        }
73
74        from_base64_res(s).ok()
75    }
76
77    pub fn shard(&self) -> Shard {
78        Shard::from_year_month(self.year(), self.month())
79    }
80
81    fn year(&self) -> u16 {
82        self.date.0
83    }
84
85    fn month(&self) -> u8 {
86        self.date.1
87    }
88
89    fn day(&self) -> u8 {
90        self.date.2
91    }
92}
93
94#[cfg(test)]
95mod test {
96    use crate::story::{StoryDate, StoryUrl};
97
98    use super::*;
99
100    #[test]
101    fn test_story_identifier() {
102        let url = StoryUrl::parse("https://google.com/?q=foo").expect("Failed to parse URL");
103        let id = StoryIdentifier::new(StoryDate::now(), url.normalization());
104        let base64 = id.to_base64();
105        assert_eq!(
106            id,
107            StoryIdentifier::from_base64(base64).expect("Failed to decode ID")
108        );
109    }
110}