Skip to main content

solti_model/resource/
metadata.rs

1//! # Object metadata.
2//!
3//! [`ObjectMeta`] tracks identity, versioning (`resource_version`), and timestamps.
4
5use std::time::SystemTime;
6
7use serde::{Deserialize, Serialize};
8
9use crate::TaskId;
10
11/// Resource metadata.
12///
13/// Identity (`id`), optimistic-concurrency counter (`resource_version`), and lifecycle timestamps.
14/// Slot and labels live in [`crate::TaskSpec`] as the single source of truth.
15///
16/// ## Also
17///
18/// - [`Task`](crate::Task) aggregate that embeds `ObjectMeta`.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct ObjectMeta {
22    /// Unique task identifier.
23    pub id: TaskId,
24    /// Incremented on any change to the resource (spec or status).
25    pub resource_version: u64,
26    /// When the resource was created.
27    #[serde(with = "time_serde")]
28    pub created_at: SystemTime,
29    /// When the resource was last updated.
30    #[serde(with = "time_serde")]
31    pub updated_at: SystemTime,
32}
33
34impl ObjectMeta {
35    /// Create metadata for a new resource.
36    pub fn new(id: TaskId) -> Self {
37        let now = SystemTime::now();
38
39        Self {
40            id,
41            resource_version: 1,
42            created_at: now,
43            updated_at: now,
44        }
45    }
46
47    /// Increment `resource_version` and stamp `updated_at`.
48    pub fn bump_resource_version(&mut self) {
49        self.updated_at = SystemTime::now();
50        self.resource_version += 1;
51    }
52}
53
54pub(crate) mod time_serde {
55    use serde::{Deserialize, Deserializer, Serialize, Serializer};
56    use std::time::{SystemTime, UNIX_EPOCH};
57
58    pub fn serialize<S>(time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
59    where
60        S: Serializer,
61    {
62        let since_epoch = time.duration_since(UNIX_EPOCH).unwrap_or_default();
63        let ms = since_epoch.as_secs() * 1_000 + u64::from(since_epoch.subsec_millis());
64        ms.serialize(serializer)
65    }
66
67    pub fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
68    where
69        D: Deserializer<'de>,
70    {
71        let ms = u64::deserialize(deserializer)?;
72        Ok(UNIX_EPOCH + std::time::Duration::from_millis(ms))
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn new_sets_defaults() {
82        let meta = ObjectMeta::new("test-id".into());
83        assert_eq!(meta.id, "test-id");
84        assert_eq!(meta.resource_version, 1);
85    }
86
87    #[test]
88    fn bump_resource_version_increments() {
89        let mut meta = ObjectMeta::new("t".into());
90        meta.bump_resource_version();
91        assert_eq!(meta.resource_version, 2);
92    }
93
94    #[test]
95    fn serde_roundtrip() {
96        let meta = ObjectMeta::new("id-1".into());
97        let json = serde_json::to_string(&meta).unwrap();
98        let back: ObjectMeta = serde_json::from_str(&json).unwrap();
99        assert_eq!(back.id, meta.id);
100        assert_eq!(back.resource_version, 1);
101    }
102
103    #[test]
104    fn serialize_handles_pre_epoch_timestamp() {
105        use std::time::{Duration, UNIX_EPOCH};
106        let mut meta = ObjectMeta::new("pre-epoch".into());
107        meta.created_at = UNIX_EPOCH - Duration::from_secs(10);
108        meta.updated_at = UNIX_EPOCH - Duration::from_secs(5);
109
110        let json = serde_json::to_string(&meta).expect("must not fail");
111        assert!(json.contains(r#""createdAt":0"#));
112        assert!(json.contains(r#""updatedAt":0"#));
113    }
114}