Skip to content

Commit 6044ef8

Browse files
authored
refactor: type-safe sub-agent ID (#1618)
* refactor: type-safe sub-agent ID There are (or will be) functions around that are intended to operate with a SubAgentID and should not **ever** receive the AC AgentID or any other reserved AgentID that we add in the future. With this we can be a bit more sure that we are using the correct IDs. * style: remove `new_unchecked` * style: remove `AgentID::is_valid_format`
1 parent 65f8f84 commit 6044ef8

File tree

1 file changed

+85
-34
lines changed

1 file changed

+85
-34
lines changed

agent-control/src/agent_control/agent_id.rs

Lines changed: 85 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,52 +15,23 @@ const AGENT_ID_MAX_LENGTH: usize = 32;
1515
/// following [RFC 1035 Label names](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names).
1616
pub enum AgentID {
1717
AgentControl,
18-
SubAgent(String),
19-
}
20-
21-
#[derive(Error, Debug)]
22-
pub enum AgentIDError {
23-
#[error(
24-
"AgentID must contain 32 characters at most, contain lowercase alphanumeric characters or dashes only, start with alphabetic, and end with alphanumeric"
25-
)]
26-
InvalidFormat,
27-
#[error("AgentID '{0}' is reserved")]
28-
Reserved(String),
18+
SubAgent(SubAgentID),
2919
}
3020

3121
impl AgentID {
3222
pub fn as_str(&self) -> &str {
3323
match self {
3424
Self::AgentControl => AGENT_CONTROL_ID,
35-
Self::SubAgent(id) => id,
25+
Self::SubAgent(id) => id.as_str(),
3626
}
3727
}
38-
39-
/// Checks if a string reference has valid format to build an [AgentID].
40-
/// It follows [RFC 1035 Label names](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names),
41-
/// and sets a shorter maximum length to avoid issues when the agent-id is used to compose names.
42-
fn is_valid_format(s: &str) -> bool {
43-
s.len() <= AGENT_ID_MAX_LENGTH
44-
&& s.starts_with(|c: char| c.is_ascii_alphabetic())
45-
&& s.ends_with(|c: char| c.is_ascii_alphanumeric())
46-
&& s.chars()
47-
.all(|c| c.eq(&'-') || c.is_ascii_digit() || c.is_ascii_lowercase())
48-
}
4928
}
5029

5130
impl TryFrom<String> for AgentID {
5231
type Error = AgentIDError;
5332
fn try_from(input: String) -> Result<Self, Self::Error> {
54-
if RESERVED_AGENT_IDS
55-
.iter()
56-
.any(|id| input.eq_ignore_ascii_case(id))
57-
{
58-
Err(AgentIDError::Reserved(input))
59-
} else if AgentID::is_valid_format(&input) {
60-
Ok(AgentID::SubAgent(input))
61-
} else {
62-
Err(AgentIDError::InvalidFormat)
63-
}
33+
agent_id_not_reserved_and_valid(&input)?;
34+
Ok(Self::SubAgent(SubAgentID(input)))
6435
}
6536
}
6637

@@ -75,7 +46,7 @@ impl From<AgentID> for String {
7546
fn from(val: AgentID) -> Self {
7647
match val {
7748
AgentID::AgentControl => AGENT_CONTROL_ID.to_string(),
78-
AgentID::SubAgent(id) => id,
49+
AgentID::SubAgent(id) => String::from(id),
7950
}
8051
}
8152
}
@@ -93,6 +64,86 @@ impl AsRef<Path> for AgentID {
9364
}
9465
}
9566

67+
/// Type with the same API as [`AgentID`], but used to represent only sub-agent IDs.
68+
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Hash, Eq)]
69+
pub struct SubAgentID(String);
70+
71+
impl SubAgentID {
72+
pub fn as_str(&self) -> &str {
73+
&self.0
74+
}
75+
}
76+
77+
impl TryFrom<String> for SubAgentID {
78+
type Error = AgentIDError;
79+
fn try_from(input: String) -> Result<Self, Self::Error> {
80+
agent_id_not_reserved_and_valid(&input)?;
81+
Ok(Self(input))
82+
}
83+
}
84+
85+
impl TryFrom<&str> for SubAgentID {
86+
type Error = AgentIDError;
87+
fn try_from(input: &str) -> Result<Self, Self::Error> {
88+
Self::try_from(input.to_string())
89+
}
90+
}
91+
92+
impl From<SubAgentID> for String {
93+
fn from(val: SubAgentID) -> Self {
94+
val.0
95+
}
96+
}
97+
98+
impl Display for SubAgentID {
99+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100+
write!(f, "{}", self.as_str())
101+
}
102+
}
103+
104+
impl AsRef<Path> for SubAgentID {
105+
fn as_ref(&self) -> &Path {
106+
// TODO: define how SubAgentID should be converted to a Path here.
107+
Path::new(self.as_str())
108+
}
109+
}
110+
111+
#[derive(Error, Debug)]
112+
pub enum AgentIDError {
113+
#[error(
114+
"AgentID must contain 32 characters at most, contain lowercase alphanumeric characters or dashes only, start with alphabetic, and end with alphanumeric"
115+
)]
116+
InvalidFormat,
117+
#[error("AgentID '{0}' is reserved")]
118+
Reserved(String),
119+
}
120+
121+
/// Checks if a string reference has valid format to build an [`AgentID`] or [`SubAgentID`].
122+
/// It follows [RFC 1035 Label names](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names),
123+
/// and sets a shorter maximum length to avoid issues when the agent-id is used to compose names.
124+
fn agent_id_not_reserved_and_valid(s: impl AsRef<str>) -> Result<(), AgentIDError> {
125+
let s = s.as_ref();
126+
if RESERVED_AGENT_IDS
127+
.iter()
128+
.any(|id| s.eq_ignore_ascii_case(id))
129+
{
130+
Err(AgentIDError::Reserved(s.to_string()))
131+
} else if agent_id_str_validation(s) {
132+
Ok(())
133+
} else {
134+
Err(AgentIDError::InvalidFormat)
135+
}
136+
}
137+
138+
fn agent_id_str_validation(s: impl AsRef<str>) -> bool {
139+
let s = s.as_ref();
140+
s.len() <= AGENT_ID_MAX_LENGTH
141+
&& s.starts_with(|c: char| c.is_ascii_alphabetic())
142+
&& s.ends_with(|c: char| c.is_ascii_alphanumeric())
143+
&& s.chars()
144+
.all(|c| c.eq(&'-') || c.is_ascii_digit() || c.is_ascii_lowercase())
145+
}
146+
96147
#[cfg(test)]
97148
pub(crate) mod tests {
98149
use crate::agent_control::agent_id::AgentID;

0 commit comments

Comments
 (0)