1
//! A module exporting timestamps types that can be encoded as [`Slug`]s.
2

            
3
use crate::slug::{BadSlug, Slug};
4

            
5
use std::fmt;
6
use std::str::FromStr;
7
use std::time::SystemTime;
8

            
9
use derive_more::{From, Into};
10
use thiserror::Error;
11
use time::format_description::FormatItem;
12
use time::macros::format_description;
13
use time::{OffsetDateTime, PrimitiveDateTime};
14
use tor_error::{into_internal, Bug};
15

            
16
/// A UTC timestamp that can be encoded in ISO 8601 format,
17
/// and that can be used as a `Slug`.
18
///
19
/// The encoded timestamp does not have a `-` separator between date values,
20
/// or `:` between time values, or any spaces.
21
/// The encoding format is `[year][month][day][hour][minute][second]`.
22
///
23
/// # Example
24
///
25
/// ```
26
/// # use tor_persist::slug::timestamp::{Iso8601TimeSlug, BadIso8601TimeSlug};
27
/// # fn demo() -> Result<(), BadIso8601TimeSlug> {
28
///
29
/// let slug = "20241023130545".parse::<Iso8601TimeSlug>()?;
30
/// assert_eq!("20241023130545", slug.to_string());
31
///
32
/// # Ok(())
33
/// # }
34
/// ```
35
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] //
36
#[derive(Into, From)]
37
pub struct Iso8601TimeSlug(SystemTime);
38

            
39
/// The format of a [`Iso8601TimeSlug`].
40
const ISO_8601SP_FMT: &[FormatItem] =
41
    format_description!("[year][month][day][hour][minute][second]");
42

            
43
impl FromStr for Iso8601TimeSlug {
44
    type Err = BadIso8601TimeSlug;
45

            
46
63
    fn from_str(s: &str) -> Result<Iso8601TimeSlug, Self::Err> {
47
63
        let d = PrimitiveDateTime::parse(s, &ISO_8601SP_FMT)?;
48

            
49
49
        Ok(Iso8601TimeSlug(d.assume_utc().into()))
50
63
    }
51
}
52

            
53
impl TryInto<Slug> for Iso8601TimeSlug {
54
    type Error = Bug;
55

            
56
90
    fn try_into(self) -> Result<Slug, Self::Error> {
57
90
        Slug::new(self.to_string()).map_err(into_internal!("Iso8601TimeSlug is not a valid slug?!"))
58
90
    }
59
}
60

            
61
/// Error for an invalid `Iso8601TimeSlug`.
62
#[derive(Error, Debug, Clone, Eq, PartialEq)]
63
#[non_exhaustive]
64
pub enum BadIso8601TimeSlug {
65
    /// Invalid timestamp.
66
    #[error("Invalid timestamp")]
67
    Timestamp(#[from] time::error::Parse),
68

            
69
    /// The timestamp is not a valid slug.
70
    #[error("Invalid slug")]
71
    Slug(#[from] BadSlug),
72
}
73

            
74
impl fmt::Display for Iso8601TimeSlug {
75
94
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76
94
        let ts = OffsetDateTime::from(self.0)
77
94
            .format(ISO_8601SP_FMT)
78
94
            .map_err(|_| fmt::Error)?;
79

            
80
94
        write!(f, "{ts}")
81
94
    }
82
}
83

            
84
#[cfg(test)]
85
mod test {
86
    // @@ begin test lint list maintained by maint/add_warning @@
87
    #![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
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
99

            
100
    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!(format!("{VALID_TIMESTAMP}Z")
120
            .parse::<Iso8601TimeSlug>()
121
            .is_err());
122
        assert!("not a timestamp".parse::<Iso8601TimeSlug>().is_err());
123

            
124
        let parsed_timestamp = VALID_TIMESTAMP.parse::<Iso8601TimeSlug>().unwrap();
125
        assert_eq!(VALID_TIMESTAMP, parsed_timestamp.to_string());
126

            
127
        assert_eq!(
128
            VALID_TIMESTAMP,
129
            parsed_timestamp.try_into_slug().unwrap().to_string(),
130
        );
131
    }
132
}