1use std::{fmt, time::SystemTime};
7
8use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
9use serde::{Deserialize, Deserializer, Serialize};
10
11use crate::{Annotations, Labels, ModelError, ModelResult, TaskId};
12
13const UID_ENTROPY_BYTES: usize = 16;
14
15#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20#[cfg_attr(feature = "schema", schemars(schema_with = "crate::schema::uid"))]
21#[serde(transparent)]
22pub struct Uid(String);
23
24impl Uid {
25 pub fn new(value: impl Into<String>) -> ModelResult<Self> {
31 let value = value.into();
32 if value.trim().is_empty() {
33 return Err(ModelError::Invalid("uid must not be empty".into()));
34 }
35 Ok(Self(value))
36 }
37
38 pub fn generate() -> ModelResult<Self> {
44 let mut bytes = [0_u8; UID_ENTROPY_BYTES];
45 getrandom::fill(&mut bytes).map_err(|error| {
46 ModelError::Invalid(format!("OS entropy source unavailable: {error}").into())
47 })?;
48 Ok(Self(URL_SAFE_NO_PAD.encode(bytes)))
49 }
50
51 #[inline]
53 pub fn as_str(&self) -> &str {
54 &self.0
55 }
56}
57
58impl fmt::Display for Uid {
59 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60 formatter.write_str(&self.0)
61 }
62}
63
64impl<'de> Deserialize<'de> for Uid {
65 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66 where
67 D: Deserializer<'de>,
68 {
69 let value = String::deserialize(deserializer)?;
70 Self::new(value).map_err(serde::de::Error::custom)
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
81#[serde(rename_all = "camelCase", deny_unknown_fields)]
82pub struct ObjectMeta {
83 name: TaskId,
84 uid: Uid,
85 resource_version: String,
86 #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
87 generation: u64,
88 #[serde(with = "rfc3339_time_serde")]
89 #[cfg_attr(
90 feature = "schema",
91 schemars(schema_with = "crate::schema::rfc3339_time")
92 )]
93 creation_timestamp: SystemTime,
94 #[serde(default, skip_serializing_if = "Labels::is_empty")]
95 labels: Labels,
96 #[serde(default, skip_serializing_if = "Annotations::is_empty")]
97 annotations: Annotations,
98}
99
100impl ObjectMeta {
101 pub fn new(name: TaskId) -> ModelResult<Self> {
110 Self::with_uid(name, Uid::generate()?)
111 }
112
113 pub fn with_uid(name: TaskId, uid: Uid) -> ModelResult<Self> {
119 name.validate_format()?;
120 Ok(Self {
121 name,
122 uid,
123 resource_version: String::new(),
124 generation: 1,
125 creation_timestamp: time_serde::now(),
126 labels: Labels::new(),
127 annotations: Annotations::new(),
128 })
129 }
130
131 #[inline]
133 pub fn name(&self) -> &TaskId {
134 &self.name
135 }
136
137 #[inline]
139 pub fn uid(&self) -> &Uid {
140 &self.uid
141 }
142
143 #[inline]
145 pub fn resource_version(&self) -> &str {
146 &self.resource_version
147 }
148
149 #[inline]
151 pub fn generation(&self) -> u64 {
152 self.generation
153 }
154
155 #[inline]
157 pub fn creation_timestamp(&self) -> SystemTime {
158 self.creation_timestamp
159 }
160
161 #[inline]
163 pub fn labels(&self) -> &Labels {
164 &self.labels
165 }
166
167 #[inline]
169 pub fn annotations(&self) -> &Annotations {
170 &self.annotations
171 }
172
173 pub fn set_resource_version(&mut self, resource_version: impl Into<String>) -> ModelResult<()> {
181 let resource_version = resource_version.into();
182 if resource_version.trim().is_empty() {
183 return Err(ModelError::Invalid(
184 "resourceVersion must not be empty when assigned".into(),
185 ));
186 }
187 self.resource_version = resource_version;
188 Ok(())
189 }
190
191 pub(crate) fn apply_metadata(&mut self, labels: Labels, annotations: Annotations) -> bool {
192 if self.labels == labels && self.annotations == annotations {
193 return false;
194 }
195 self.labels = labels;
196 self.annotations = annotations;
197 true
198 }
199
200 pub(crate) fn bump_generation(&mut self) {
201 self.generation = self.generation.saturating_add(1);
202 }
203}
204
205pub(crate) mod time_serde {
206 use serde::{Deserialize, Deserializer, Serialize, Serializer};
207 use std::time::{SystemTime, UNIX_EPOCH};
208
209 pub(crate) fn now() -> SystemTime {
210 let duration = SystemTime::now()
211 .duration_since(UNIX_EPOCH)
212 .unwrap_or_default();
213 UNIX_EPOCH + std::time::Duration::from_millis(duration.as_millis() as u64)
214 }
215
216 pub(crate) fn serialize<S>(time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
217 where
218 S: Serializer,
219 {
220 let since_epoch = time
221 .duration_since(UNIX_EPOCH)
222 .map_err(serde::ser::Error::custom)?;
223 let ms = since_epoch.as_secs() * 1_000 + u64::from(since_epoch.subsec_millis());
224 ms.serialize(serializer)
225 }
226
227 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
228 where
229 D: Deserializer<'de>,
230 {
231 let ms = u64::deserialize(deserializer)?;
232 UNIX_EPOCH
233 .checked_add(std::time::Duration::from_millis(ms))
234 .ok_or_else(|| <D::Error as serde::de::Error>::custom("timestamp out of range"))
235 }
236}
237
238pub(crate) mod rfc3339_time_serde {
239 use serde::{Deserialize, Deserializer, Serializer};
240 use std::time::SystemTime;
241 use time::{OffsetDateTime, format_description::well_known::Rfc3339};
242
243 pub(crate) fn serialize<S>(timestamp: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
244 where
245 S: Serializer,
246 {
247 OffsetDateTime::from(*timestamp)
248 .format(&Rfc3339)
249 .map_err(serde::ser::Error::custom)
250 .and_then(|value| serializer.serialize_str(&value))
251 }
252
253 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
254 where
255 D: Deserializer<'de>,
256 {
257 let value = String::deserialize(deserializer)?;
258 let timestamp =
259 OffsetDateTime::parse(&value, &Rfc3339).map_err(serde::de::Error::custom)?;
260 Ok(SystemTime::from(timestamp))
261 }
262
263 pub(crate) mod option {
264 use serde::{Deserialize, Deserializer, Serializer};
265 use std::time::SystemTime;
266
267 pub(crate) fn serialize<S>(
268 timestamp: &Option<SystemTime>,
269 serializer: S,
270 ) -> Result<S::Ok, S::Error>
271 where
272 S: Serializer,
273 {
274 match timestamp {
275 Some(timestamp) => super::serialize(timestamp, serializer),
276 None => serializer.serialize_none(),
277 }
278 }
279
280 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<SystemTime>, D::Error>
281 where
282 D: Deserializer<'de>,
283 {
284 Option::<String>::deserialize(deserializer)?
285 .map(|value| {
286 time::OffsetDateTime::parse(
287 &value,
288 &time::format_description::well_known::Rfc3339,
289 )
290 .map(SystemTime::from)
291 .map_err(serde::de::Error::custom)
292 })
293 .transpose()
294 }
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn new_sets_server_identity_and_generation() {
304 let meta = ObjectMeta::new(TaskId::new("task-a").unwrap()).unwrap();
305
306 assert_eq!(meta.name(), "task-a");
307 assert!(!meta.uid().as_str().is_empty());
308 assert_eq!(meta.generation(), 1);
309 assert_eq!(meta.resource_version(), "");
310 }
311
312 #[test]
313 fn server_metadata_uses_kubernetes_fields_and_opaque_resource_version() {
314 let mut meta = ObjectMeta::new(TaskId::new("task-a").unwrap()).unwrap();
315 meta.set_resource_version("store/revision:0021").unwrap();
316
317 assert_eq!(meta.resource_version(), "store/revision:0021");
318 assert!(serde_json::from_str::<Uid>(r#"""#).is_err());
319
320 let json = serde_json::to_value(&meta).unwrap();
321 assert_eq!(json["name"], "task-a");
322 assert_eq!(json["resourceVersion"], "store/revision:0021");
323 assert_eq!(json["generation"], 1);
324 assert!(json["creationTimestamp"].is_string());
325 assert!(json.get("uid").is_some());
326 }
327
328 #[test]
329 fn creation_timestamp_uses_rfc3339_milliseconds_and_rejects_unix_numbers() {
330 let mut meta = ObjectMeta::new(TaskId::new("task-a").unwrap()).unwrap();
331 meta.set_resource_version("1").unwrap();
332 let mut json = serde_json::to_value(meta).unwrap();
333 json["creationTimestamp"] = serde_json::json!("2025-01-02T03:04:05.678Z");
334
335 let back: ObjectMeta = serde_json::from_value(json).unwrap();
336 let serialized = serde_json::to_value(back).unwrap();
337
338 assert_eq!(serialized["creationTimestamp"], "2025-01-02T03:04:05.678Z");
339
340 let meta = ObjectMeta::new(TaskId::new("task-a").unwrap()).unwrap();
341 let mut json = serde_json::to_value(meta).unwrap();
342 json["creationTimestamp"] = serde_json::json!(1_735_786_800_000_u64);
343
344 assert!(serde_json::from_value::<ObjectMeta>(json).is_err());
345 }
346}