Skip to main content

solti_model/domain/identity/
slot.rs

1//! # Execution slot.
2//!
3//! [`Slot`] is the logical execution lane name (newtype over `Arc<str>`).
4
5use super::validate_identity;
6use crate::error::ModelError;
7
8/// Maximum length of a `Slot` identifier.
9pub const SLOT_MAX_LEN: usize = 64;
10
11arc_str_newtype! {
12    /// Logical identifier for a controller slot.
13    ///
14    /// A slot groups tasks that share a single execution lane.
15    ///
16    /// ```text
17    ///  Slot: "build-pipeline"         Slot: "deploy"
18    ///  ┌───────────────────────┐      ┌────────────────────────┐
19    ///  │  TaskId: sub-build-1  │      │  TaskId: sub-deploy-1  │
20    ///  │  TaskId: sub-build-2  │      │  TaskId: sub-deploy-2  │
21    ///  │  TaskId: sub-build-3  │      │  TaskId: sub-deploy-3  │
22    ///  │        ...            │      │        ...             │
23    ///  └───────────────────────┘      └────────────────────────┘
24    ///         one lane                        one lane
25    ///     (one at a time)                  (one at a time)
26    /// ```
27    ///
28    /// ```rust
29    /// use solti_model::Slot;
30    ///
31    /// let slot = Slot::new("build-pipeline");
32    /// assert_eq!(slot.as_str(), "build-pipeline");
33    ///
34    /// let slot: Slot = "deploy".into();
35    /// assert_eq!(format!("{slot}"), "deploy");
36    /// ```
37    pub struct Slot;
38}
39
40impl Slot {
41    /// Validate that the slot name is safe to use across the SDK.
42    ///
43    /// See [`validate_identity`] for the exact rules.
44    pub fn validate_format(&self) -> Result<(), ModelError> {
45        validate_identity("slot", self.as_str(), SLOT_MAX_LEN)
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::Slot;
52    use std::sync::Arc;
53
54    #[test]
55    fn new_and_as_str() {
56        let slot = Slot::new("my-slot");
57        assert_eq!(slot.as_str(), "my-slot");
58    }
59
60    #[test]
61    fn from_str_and_string() {
62        let a: Slot = "abc".into();
63        let b: Slot = String::from("abc").into();
64        assert_eq!(a, b);
65    }
66
67    #[test]
68    fn display() {
69        let slot = Slot::new("demo");
70        assert_eq!(format!("{slot}"), "demo");
71    }
72
73    #[test]
74    fn partial_eq_with_str() {
75        let slot = Slot::new("test");
76        assert_eq!(slot, *"test");
77    }
78
79    #[test]
80    fn serde_transparent() {
81        let slot = Slot::new("build");
82        let json = serde_json::to_string(&slot).unwrap();
83        assert_eq!(json, "\"build\"");
84
85        let back: Slot = serde_json::from_str(&json).unwrap();
86        assert_eq!(back, slot);
87    }
88
89    #[test]
90    fn into_inner() {
91        let slot = Slot::new("owned");
92        let s: Arc<str> = slot.into_inner();
93        assert_eq!(&*s, "owned");
94    }
95
96    #[test]
97    fn clone_is_cheap() {
98        let slot = Slot::new("shared");
99        let cloned = slot.clone();
100        let a: Arc<str> = slot.into_inner();
101        let b: Arc<str> = cloned.into_inner();
102        assert!(Arc::ptr_eq(&a, &b));
103    }
104
105    #[test]
106    fn validate_format_accepts_valid() {
107        Slot::new("build.frontend").validate_format().unwrap();
108        Slot::new("build").validate_format().unwrap();
109        Slot::new("a").validate_format().unwrap();
110    }
111
112    #[test]
113    fn validate_format_rejects_invalid() {
114        assert!(Slot::new("build/frontend").validate_format().is_err());
115        assert!(Slot::new("с кириллицей").validate_format().is_err());
116        assert!(Slot::new("with space").validate_format().is_err());
117        assert!(Slot::new("a\nb").validate_format().is_err());
118        assert!(Slot::new(".").validate_format().is_err());
119        assert!(Slot::new("").validate_format().is_err());
120    }
121}