spool/
spool_id.rs

1use chrono::{DateTime, Duration, TimeZone, Utc};
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4use uuid::Uuid;
5
6/// Identifies a message within the spool of its host node.
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(into = "String", try_from = "String")]
9#[derive(utoipa::ToSchema)]
10#[schema(value_type=String, example="d7ef132b5d7711eea8c8000c29c33806")]
11pub struct SpoolId(Uuid);
12
13impl std::fmt::Display for SpoolId {
14    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
15        self.0.simple().fmt(fmt)
16    }
17}
18
19impl From<Uuid> for SpoolId {
20    fn from(uuid: Uuid) -> Self {
21        Self(uuid)
22    }
23}
24
25impl From<SpoolId> for String {
26    fn from(id: SpoolId) -> String {
27        id.to_string()
28    }
29}
30
31impl TryFrom<String> for SpoolId {
32    type Error = uuid::Error;
33
34    fn try_from(s: String) -> Result<Self, Self::Error> {
35        let uuid = Uuid::parse_str(&s)?;
36        Ok(Self(uuid))
37    }
38}
39
40impl Default for SpoolId {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl SpoolId {
47    pub fn new() -> Self {
48        // We're using v1, but we should be able to seamlessly upgrade to v7
49        // once that feature stabilizes in the uuid crate
50        Self(uuid_helper::now_v1())
51    }
52
53    pub fn compute_path(&self, in_dir: &Path) -> PathBuf {
54        let (a, b, c, [d, e, f, g, h, i, j, k]) = self.0.as_fields();
55        // Note that in a v1 UUID, a,b,c holds the timestamp components
56        // from least-significant up to most significant.
57        let [a1, a2, a3, a4] = a.to_be_bytes();
58        let name = format!(
59            "{a1:02x}/{a2:02x}/{a3:02x}/{a4:02x}/{b:04x}{c:04x}{d:02x}{e:02x}{f:02x}{g:02x}{h:02x}{i:02x}{j:02x}{k:02x}"
60        );
61        in_dir.join(name)
62    }
63
64    pub fn as_bytes(&self) -> &[u8; 16] {
65        self.0.as_bytes()
66    }
67
68    pub fn from_slice(s: &[u8]) -> Option<Self> {
69        let uuid = Uuid::from_slice(s).ok()?;
70        Some(Self(uuid))
71    }
72
73    pub fn from_ascii_bytes(s: &[u8]) -> Option<Self> {
74        let uuid = Uuid::try_parse_ascii(s).ok()?;
75        Some(Self(uuid))
76    }
77
78    #[allow(clippy::should_implement_trait)]
79    pub fn from_str(s: &str) -> Option<Self> {
80        let uuid = Uuid::parse_str(s).ok()?;
81        Some(Self(uuid))
82    }
83
84    pub fn from_path(mut path: &Path) -> Option<Self> {
85        let mut components = vec![];
86
87        for _ in 0..5 {
88            components.push(path.file_name()?.to_str()?);
89            path = path.parent()?;
90        }
91
92        components.reverse();
93        Some(Self(Uuid::parse_str(&components.join("")).ok()?))
94    }
95
96    /// Returns time elapsed since the id was created,
97    /// given the current timestamp
98    pub fn age(&self, now: DateTime<Utc>) -> Duration {
99        let created = self.created();
100        now - created
101    }
102
103    pub fn created(&self) -> DateTime<Utc> {
104        let (seconds, nanos) = self.0.get_timestamp().unwrap().to_unix();
105        Utc.timestamp_opt(seconds.try_into().unwrap(), nanos)
106            .unwrap()
107    }
108}
109
110#[cfg(test)]
111mod test {
112    use super::*;
113
114    #[test]
115    fn roundtrip_path() {
116        let id = SpoolId::new();
117        eprintln!("{id}");
118        let path = id.compute_path(Path::new("."));
119        let id2 = SpoolId::from_path(&path).unwrap();
120        assert_eq!(id, id2);
121    }
122
123    #[test]
124    fn roundtrip_bytes() {
125        let id = SpoolId::new();
126        eprintln!("{id}");
127        let bytes = id.as_bytes();
128        let id2 = SpoolId::from_slice(bytes.as_slice()).unwrap();
129        assert_eq!(id, id2);
130    }
131}