Skip to main content

solti_model/domain/identity/
agent.rs

1//! # Agent identity
2//!
3//! [`AgentId`] identifies one agent.
4//! It accepts `[A-Za-z0-9._-]` and is limited to [`AGENT_ID_MAX_LEN`] bytes.
5
6use super::validate_identity;
7use crate::error::ModelError;
8
9/// Maximum length of an `AgentId`.
10pub const AGENT_ID_MAX_LEN: usize = 128;
11
12arc_str_newtype! {
13    #[cfg_attr(feature = "schema", schemars(schema_with = "crate::schema::agent_id"))]
14    /// Caller-provided identifier for a Solti agent.
15    ///
16    /// The model validates its format.
17    /// The caller owns assignment and uniqueness.
18    ///
19    /// ```rust
20    /// use solti_model::AgentId;
21    ///
22    /// // From a UUID
23    /// let id = AgentId::new("550e8400-e29b-41d4-a716-446655440000").unwrap();
24    /// assert_eq!(id.as_str(), "550e8400-e29b-41d4-a716-446655440000");
25    ///
26    /// // From a Kubernetes pod name
27    /// let id = AgentId::new("worker-pod-7b9f4").unwrap();
28    /// assert_eq!(format!("{id}"), "worker-pod-7b9f4");
29    /// ```
30    pub struct AgentId;
31}
32
33impl AgentId {
34    /// Validates the agent id.
35    ///
36    /// # Errors
37    ///
38    /// Returns [`ModelError::Invalid`] when the value is empty, too long, equal to `"."` or `".."`, or contains a byte outside `[A-Za-z0-9._-]`.
39    ///
40    /// ## Example
41    ///
42    /// ```
43    /// use solti_model::AgentId;
44    ///
45    /// assert!(AgentId::new("worker-pod-7b9f4").is_ok());
46    /// assert!(AgentId::new("worker/pod").is_err());
47    /// ```
48    pub fn validate_format(&self) -> Result<(), ModelError> {
49        validate_identity("agent_id", self.as_str(), AGENT_ID_MAX_LEN)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use std::sync::Arc;
57
58    #[test]
59    fn exposes_string_identity_hashing_and_shared_clones() {
60        use std::collections::HashSet;
61
62        let id = AgentId::new("agent-a").unwrap();
63        assert_eq!(id.as_str(), "agent-a");
64        assert_eq!(format!("{id}"), "agent-a");
65        assert_eq!(id, *"agent-a");
66
67        let mut set = HashSet::new();
68        set.insert(id.clone());
69        set.insert(AgentId::new("agent-b").unwrap());
70        set.insert(AgentId::new("agent-a").unwrap());
71        assert_eq!(set.len(), 2);
72
73        let cloned = id.clone();
74        let a: Arc<str> = id.into_inner();
75        let b: Arc<str> = cloned.into_inner();
76        assert!(Arc::ptr_eq(&a, &b));
77    }
78
79    #[test]
80    fn serde_is_transparent() {
81        let id = AgentId::new("550e8400-e29b-41d4-a716-446655440000").unwrap();
82        let json = serde_json::to_string(&id).unwrap();
83        assert_eq!(json, r#""550e8400-e29b-41d4-a716-446655440000""#);
84        assert_eq!(serde_json::from_str::<AgentId>(&json).unwrap(), id);
85    }
86
87    #[test]
88    fn validation_accepts_safe_values_and_rejects_unsafe_values() {
89        for valid in [
90            "550e8400-e29b-41d4-a716-446655440000",
91            "worker-pod-7b9f4",
92            "agent.eu-west-1.01",
93        ] {
94            AgentId::new(valid).unwrap();
95        }
96        for invalid in ["", "agent with space", "agent/path"] {
97            assert!(AgentId::new(invalid).is_err(), "must reject {invalid:?}");
98        }
99    }
100}