Skip to main content

solti_model/domain/identity/
slot.rs

1//! # Execution slot
2//!
3//! [`Slot`] is a logical concurrency key.
4//! It accepts `[A-Za-z0-9._-]` and is limited to [`SLOT_MAX_LEN`] bytes.
5
6use super::validate_identity;
7use crate::error::ModelError;
8
9/// Maximum length of a `Slot` identifier.
10pub const SLOT_MAX_LEN: usize = 64;
11
12arc_str_newtype! {
13    #[cfg_attr(feature = "schema", schemars(schema_with = "crate::schema::slot"))]
14    /// Logical identifier for a controller slot.
15    ///
16    /// A slot groups tasks that share a single execution lane.
17    /// Controllers use slots for admission policy and queue behavior.
18    ///
19    /// ```rust
20    /// use solti_model::Slot;
21    ///
22    /// let slot = Slot::new("build-pipeline").unwrap();
23    /// assert_eq!(slot.as_str(), "build-pipeline");
24    ///
25    /// let slot = Slot::new("deploy").unwrap();
26    /// assert_eq!(format!("{slot}"), "deploy");
27    /// ```
28    pub struct Slot;
29}
30
31impl Slot {
32    /// Validates the slot.
33    ///
34    /// # Errors
35    ///
36    /// Returns [`ModelError::Invalid`] when the value is empty, too long, equal to `"."` or `".."`, or contains a byte outside `[A-Za-z0-9._-]`.
37    ///
38    /// ## Example
39    ///
40    /// ```
41    /// use solti_model::Slot;
42    ///
43    /// assert!(Slot::new("build.frontend").is_ok());
44    /// assert!(Slot::new("build/frontend").is_err());
45    /// ```
46    pub fn validate_format(&self) -> Result<(), ModelError> {
47        validate_identity("slot", self.as_str(), SLOT_MAX_LEN)
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::Slot;
54    use std::sync::Arc;
55
56    #[test]
57    fn exposes_string_conversions_and_shares_clones() {
58        let slot = Slot::new("shared").unwrap();
59        let parsed: Slot = "shared".parse().unwrap();
60
61        assert_eq!(slot.as_str(), "shared");
62        assert_eq!(format!("{slot}"), "shared");
63        assert_eq!(slot, *"shared");
64        assert_eq!(slot, parsed);
65
66        let cloned = slot.clone();
67        let a: Arc<str> = slot.into_inner();
68        let b: Arc<str> = cloned.into_inner();
69        assert_eq!(&*a, "shared");
70        assert!(Arc::ptr_eq(&a, &b));
71    }
72
73    #[test]
74    fn serde_is_transparent() {
75        let slot = Slot::new("build").unwrap();
76        let json = serde_json::to_string(&slot).unwrap();
77        assert_eq!(json, "\"build\"");
78        assert_eq!(serde_json::from_str::<Slot>(&json).unwrap(), slot);
79    }
80
81    #[test]
82    fn validation_accepts_safe_values_and_rejects_unsafe_values() {
83        for valid in ["build.frontend", "build", "a"] {
84            Slot::new(valid).unwrap();
85        }
86        for invalid in ["build/frontend", "é", "with space", "a\nb", ".", ""] {
87            assert!(Slot::new(invalid).is_err(), "must reject {invalid:?}");
88        }
89    }
90}