Skip to main content

solti_model/domain/identity/
task.rs

1//! # Task identity
2//!
3//! [`TaskId`] is the stable name of a task resource.
4//! It follows the Kubernetes DNS-1123 subdomain format.
5
6use crate::error::ModelError;
7
8/// Maximum length of a `TaskId`.
9pub const TASK_ID_MAX_LEN: usize = crate::validation::DNS1123_SUBDOMAIN_MAX_LEN;
10
11arc_str_newtype! {
12    #[cfg_attr(feature = "schema", schemars(schema_with = "crate::schema::task_id"))]
13    /// Stable name used to address a task resource.
14    ///
15    /// Apply, get and delete operations address a task through this name.
16    /// [`Uid`](crate::Uid) separately identifies a particular incarnation when a name is deleted and recreated.
17    ///
18    /// ```
19    /// use solti_model::TaskId;
20    ///
21    /// let id = TaskId::new("subprocess-build-1").unwrap();
22    /// assert_eq!(id.as_str(), "subprocess-build-1");
23    /// ```
24    pub struct TaskId;
25}
26
27impl TaskId {
28    /// Validates the task id.
29    ///
30    /// # Errors
31    ///
32    /// Returns [`ModelError::Invalid`].
33    ///
34    /// ## Example
35    ///
36    /// ```
37    /// use solti_model::TaskId;
38    ///
39    /// assert!(TaskId::new("subprocess-build-1").is_ok());
40    /// assert!(TaskId::new("with/slash").is_err());
41    /// ```
42    pub fn validate_format(&self) -> Result<(), ModelError> {
43        crate::validation::validate_dns1123_subdomain("task_id", self.as_str())
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use std::sync::Arc;
51
52    #[test]
53    fn exposes_string_identity_hashing_and_shared_clones() {
54        use std::collections::HashSet;
55
56        let id = TaskId::new("id-1").unwrap();
57        assert_eq!(id.as_str(), "id-1");
58        assert_eq!(format!("{id}"), "id-1");
59
60        let mut set = HashSet::new();
61        set.insert(id.clone());
62        set.insert(TaskId::new("id-2").unwrap());
63        set.insert(TaskId::new("id-1").unwrap());
64        assert_eq!(set.len(), 2);
65
66        let cloned = id.clone();
67        let a: Arc<str> = id.into_inner();
68        let b: Arc<str> = cloned.into_inner();
69        assert!(Arc::ptr_eq(&a, &b));
70    }
71
72    #[test]
73    fn serde_is_transparent_and_validated() {
74        let id = TaskId::new("runner-slot-ff").unwrap();
75        let json = serde_json::to_string(&id).unwrap();
76        assert_eq!(json, r#""runner-slot-ff""#);
77        assert_eq!(serde_json::from_str::<TaskId>(&json).unwrap(), id);
78
79        for invalid in [
80            r#""a/b""#,
81            r#""""#,
82            r#"".""#,
83            r#""a b""#,
84            r#""UPPER""#,
85            r#""under_score""#,
86        ] {
87            assert!(
88                serde_json::from_str::<TaskId>(invalid).is_err(),
89                "must reject {invalid}"
90            );
91        }
92    }
93
94    #[test]
95    fn validation_matches_kubernetes_resource_names_and_length_limit() {
96        for valid in ["subprocess-build-1", "subprocess-build.frontend-ff"] {
97            TaskId::new(valid).unwrap();
98        }
99        for invalid in [
100            "",
101            "with/slash",
102            "with space",
103            "UPPER",
104            "with_underscore",
105            "-leading",
106            "trailing-",
107            "empty..label",
108        ] {
109            assert!(TaskId::new(invalid).is_err(), "must reject {invalid:?}");
110        }
111        assert!(TaskId::new("x".repeat(64)).is_ok());
112        let max = "a".repeat(TASK_ID_MAX_LEN);
113        assert!(TaskId::new(&max).is_ok());
114        assert!(TaskId::new(format!("{max}e")).is_err());
115    }
116}