const TENANT_LEN: std::ops::RangeInclusive<usize> = 1..=32;
const AGENT_LEN: std::ops::RangeInclusive<usize> = 1..=64;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum IdError {
#[error(
"invalid tenant_id {0:?}: ids are [A-Za-z0-9._], tenant 1..=32, agent 1..=64, no \"..\""
)]
Tenant(String),
#[error(
"invalid agent_id {0:?}: ids are [A-Za-z0-9._], tenant 1..=32, agent 1..=64, no \"..\""
)]
Agent(String),
}
const fn is_id_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '.' || c == '_'
}
fn id_ok(id: &str, len: std::ops::RangeInclusive<usize>) -> bool {
len.contains(&id.len()) && id.chars().all(is_id_char) && !id.contains("..")
}
pub fn validate_tenant_id(id: &str) -> Result<(), IdError> {
if id_ok(id, TENANT_LEN) {
Ok(())
} else {
Err(IdError::Tenant(id.to_owned()))
}
}
pub fn validate_agent_id(id: &str) -> Result<(), IdError> {
if id_ok(id, AGENT_LEN) {
Ok(())
} else {
Err(IdError::Agent(id.to_owned()))
}
}
pub fn sandbox_name(tenant_id: &str, agent_id: &str) -> Result<String, IdError> {
validate_tenant_id(tenant_id)?;
validate_agent_id(agent_id)?;
Ok(format!("a-{tenant_id}-{agent_id}"))
}
pub fn workspace_volume_name(tenant_id: &str, agent_id: &str) -> Result<String, IdError> {
validate_tenant_id(tenant_id)?;
validate_agent_id(agent_id)?;
Ok(format!("ws-{tenant_id}-{agent_id}"))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "tests")]
mod tests {
use super::*;
#[test]
fn sandbox_name_is_injective_across_hyphen_boundary() {
assert_ne!(
sandbox_name("x", "yz").unwrap(),
sandbox_name("xy", "z").unwrap(),
"a-x-yz vs a-xy-z"
);
assert_ne!(
workspace_volume_name("x", "yz").unwrap(),
workspace_volume_name("xy", "z").unwrap(),
"ws-x-yz vs ws-xy-z"
);
assert!(sandbox_name("x-y", "z").is_err(), "hyphen in tenant");
assert!(sandbox_name("x", "y-z").is_err(), "hyphen in agent");
assert!(sandbox_name("x", "y:z").is_err(), "colon in agent");
assert!(
workspace_volume_name("x-y", "z").is_err(),
"hyphen in tenant volume"
);
assert!(
workspace_volume_name("x", "y-z").is_err(),
"hyphen in agent volume"
);
assert!(
workspace_volume_name("x", "y:z").is_err(),
"colon in agent volume"
);
}
#[test]
fn consecutive_dots_rejected() {
assert!(
workspace_volume_name("a..b", "x").is_err(),
"dot-dot tenant"
);
assert!(sandbox_name("t", "a..b").is_err(), "dot-dot agent");
assert!(validate_tenant_id("..").is_err(), "dot-dot only");
assert!(sandbox_name("a.b", "x").is_ok(), "single dots allowed");
}
#[test]
fn formatted_names() {
assert_eq!(sandbox_name("t", "a").unwrap(), "a-t-a", "sandbox");
assert_eq!(workspace_volume_name("t", "a").unwrap(), "ws-t-a", "volume");
}
#[test]
fn tenant_length_bounds() {
let ok32 = "a".repeat(32);
let bad33 = "a".repeat(33);
assert!(validate_tenant_id(&ok32).is_ok(), "32");
assert!(validate_tenant_id(&bad33).is_err(), "33");
assert!(validate_tenant_id("").is_err(), "empty");
}
#[test]
fn agent_length_bounds() {
let ok64 = "a".repeat(64);
let bad65 = "a".repeat(65);
assert!(validate_agent_id(&ok64).is_ok(), "64");
assert!(validate_agent_id(&bad65).is_err(), "65");
}
#[test]
fn dot_and_underscore_allowed() {
assert!(
sandbox_name("t.id_1", "ag.nt_2").is_ok(),
"dot and underscore"
);
}
}