Skip to main content

solti_model/resource/
preconditions.rs

1//! # Write preconditions
2//!
3//! [`WritePreconditions`] protects an apply or delete from a stale resource snapshot.
4
5use crate::{ModelError, ModelResult, Task, Uid};
6
7/// Optional identity and version checks for a resource write.
8///
9/// When both values are present, both must match the current resource.
10#[derive(Clone, Debug, Default, PartialEq, Eq)]
11pub struct WritePreconditions {
12    uid: Option<Uid>,
13    resource_version: Option<String>,
14}
15
16impl WritePreconditions {
17    /// Creates empty preconditions.
18    #[inline]
19    pub const fn new() -> Self {
20        Self {
21            uid: None,
22            resource_version: None,
23        }
24    }
25
26    /// Captures resource identity and version.
27    ///
28    /// # Errors
29    ///
30    /// Returns [`ModelError::Invalid`] for a resource not yet stored.
31    pub fn from_task(task: &Task) -> ModelResult<Self> {
32        Self::new()
33            .with_uid(task.uid().clone())
34            .with_resource_version(task.metadata().resource_version())
35    }
36
37    /// Requires this resource UID.
38    #[inline]
39    pub fn with_uid(mut self, uid: Uid) -> Self {
40        self.uid = Some(uid);
41        self
42    }
43
44    /// Requires this resource version.
45    ///
46    /// # Errors
47    ///
48    /// Returns [`ModelError::Invalid`] when the value is empty.
49    pub fn with_resource_version(
50        mut self,
51        resource_version: impl Into<String>,
52    ) -> ModelResult<Self> {
53        let resource_version = resource_version.into();
54        if resource_version.trim().is_empty() {
55            return Err(ModelError::Invalid(
56                "resourceVersion precondition must not be empty".into(),
57            ));
58        }
59        self.resource_version = Some(resource_version);
60        Ok(self)
61    }
62
63    /// Expected resource UID, when present.
64    #[inline]
65    pub fn uid(&self) -> Option<&Uid> {
66        self.uid.as_ref()
67    }
68
69    /// Expected resource version, when present.
70    #[inline]
71    pub fn resource_version(&self) -> Option<&str> {
72        self.resource_version.as_deref()
73    }
74
75    /// Returns whether no checks are set.
76    #[inline]
77    pub fn is_empty(&self) -> bool {
78        self.uid.is_none() && self.resource_version.is_none()
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn empty_is_unconditional() {
88        assert!(WritePreconditions::new().is_empty());
89    }
90
91    #[test]
92    fn values_are_retained() {
93        let uid = Uid::new("resource-uid").unwrap();
94        let preconditions = WritePreconditions::new()
95            .with_uid(uid.clone())
96            .with_resource_version("42")
97            .unwrap();
98
99        assert_eq!(preconditions.uid(), Some(&uid));
100        assert_eq!(preconditions.resource_version(), Some("42"));
101        assert!(!preconditions.is_empty());
102    }
103
104    #[test]
105    fn empty_resource_version_is_rejected() {
106        let error = WritePreconditions::new()
107            .with_resource_version(" ")
108            .unwrap_err();
109        assert_eq!(
110            error.to_string(),
111            "invalid model: resourceVersion precondition must not be empty"
112        );
113    }
114
115    #[test]
116    fn unstored_task_cannot_be_captured() {
117        let task = Task::from_manifest(
118            crate::TaskManifest::new(
119                "unstored",
120                crate::TaskSpec::builder(
121                    "slot",
122                    crate::TaskWorkload::Embedded(crate::EmbeddedSpec::new("test").unwrap()),
123                    1_000_u64,
124                )
125                .build()
126                .unwrap(),
127            )
128            .unwrap(),
129        )
130        .unwrap();
131
132        assert!(WritePreconditions::from_task(&task).is_err());
133    }
134}