tor_persist/slug/
timestamp.rs
        
        
        
        1use crate::slug::{BadSlug, Slug};
4
5use std::fmt;
6use std::str::FromStr;
7use std::time::SystemTime;
8
9use derive_more::{From, Into};
10use thiserror::Error;
11use time::format_description::FormatItem;
12use time::macros::format_description;
13use time::{OffsetDateTime, PrimitiveDateTime};
14use tor_error::{Bug, into_internal};
15
16#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Into, From)]
37pub struct Iso8601TimeSlug(SystemTime);
38
39const ISO_8601SP_FMT: &[FormatItem] =
41    format_description!("[year][month][day][hour][minute][second]");
42
43impl FromStr for Iso8601TimeSlug {
44    type Err = BadIso8601TimeSlug;
45
46    fn from_str(s: &str) -> Result<Iso8601TimeSlug, Self::Err> {
47        let d = PrimitiveDateTime::parse(s, &ISO_8601SP_FMT)?;
48
49        Ok(Iso8601TimeSlug(d.assume_utc().into()))
50    }
51}
52
53impl TryInto<Slug> for Iso8601TimeSlug {
54    type Error = Bug;
55
56    fn try_into(self) -> Result<Slug, Self::Error> {
57        Slug::new(self.to_string()).map_err(into_internal!("Iso8601TimeSlug is not a valid slug?!"))
58    }
59}
60
61#[derive(Error, Debug, Clone, Eq, PartialEq)]
63#[non_exhaustive]
64pub enum BadIso8601TimeSlug {
65    #[error("Invalid timestamp")]
67    Timestamp(#[from] time::error::Parse),
68
69    #[error("Invalid slug")]
71    Slug(#[from] BadSlug),
72}
73
74impl fmt::Display for Iso8601TimeSlug {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        let ts = OffsetDateTime::from(self.0)
77            .format(ISO_8601SP_FMT)
78            .map_err(|_| fmt::Error)?;
79
80        write!(f, "{ts}")
81    }
82}
83
84#[cfg(test)]
85mod test {
86    #![allow(clippy::bool_assert_comparison)]
88    #![allow(clippy::clone_on_copy)]
89    #![allow(clippy::dbg_macro)]
90    #![allow(clippy::mixed_attributes_style)]
91    #![allow(clippy::print_stderr)]
92    #![allow(clippy::print_stdout)]
93    #![allow(clippy::single_char_pattern)]
94    #![allow(clippy::unwrap_used)]
95    #![allow(clippy::unchecked_duration_subtraction)]
96    #![allow(clippy::useless_vec)]
97    #![allow(clippy::needless_pass_by_value)]
98    use crate::slug::TryIntoSlug as _;
101
102    use super::*;
103    use humantime::parse_rfc3339;
104
105    #[test]
106    fn timestamp_parsing() {
107        const VALID_TIMESTAMP: &str = "20241023130545";
108        const VALID_TIMESTAMP_RFC3339: &str = "2024-10-23T13:05:45Z";
109
110        let t = VALID_TIMESTAMP.parse::<Iso8601TimeSlug>().unwrap();
111        let t: SystemTime = t.into();
112        assert_eq!(t, parse_rfc3339(VALID_TIMESTAMP_RFC3339).unwrap());
113
114        assert!("2024-10-23 13:05:45".parse::<Iso8601TimeSlug>().is_err());
115        assert!("20241023 13:05:45".parse::<Iso8601TimeSlug>().is_err());
116        assert!("2024-10-23 130545".parse::<Iso8601TimeSlug>().is_err());
117        assert!("20241023".parse::<Iso8601TimeSlug>().is_err());
118        assert!("2024102313054".parse::<Iso8601TimeSlug>().is_err());
119        assert!(
120            format!("{VALID_TIMESTAMP}Z")
121                .parse::<Iso8601TimeSlug>()
122                .is_err()
123        );
124        assert!("not a timestamp".parse::<Iso8601TimeSlug>().is_err());
125
126        let parsed_timestamp = VALID_TIMESTAMP.parse::<Iso8601TimeSlug>().unwrap();
127        assert_eq!(VALID_TIMESTAMP, parsed_timestamp.to_string());
128
129        assert_eq!(
130            VALID_TIMESTAMP,
131            parsed_timestamp.try_into_slug().unwrap().to_string(),
132        );
133    }
134}