progscrape_application/story/
id.rs1use serde::{Deserialize, Serialize};
2
3use std::fmt::Display;
4
5use progscrape_scrapers::{StoryDate, StoryUrlNorm};
6
7use crate::Shard;
8
9#[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 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}