use core::fmt;
use uuid::Uuid;
pub const ROOT_NS: Uuid = Uuid::from_u128(0xa11c_e115_dead_beef_cafe_c0ff_ee1d_ecaf);
pub const SEP: char = '/';
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NameError {
EmptySegment,
BadStart,
InvalidChar,
}
impl fmt::Display for NameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let msg = match self {
NameError::EmptySegment => "empty SRN segment (check for leading/trailing/doubled '/')",
NameError::BadStart => "SRN segment must start with a lowercase letter or digit",
NameError::InvalidChar => "SRN segment may only contain [a-z0-9._-]",
};
f.write_str(msg)
}
}
pub fn validate_segment(seg: &str) -> Result<(), NameError> {
let mut chars = seg.chars();
match chars.next() {
None => return Err(NameError::EmptySegment),
Some(c) if c.is_ascii_lowercase() || c.is_ascii_digit() => {}
Some(_) => return Err(NameError::BadStart),
}
for c in chars {
if !(c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '.' | '_' | '-')) {
return Err(NameError::InvalidChar);
}
}
Ok(())
}
pub fn child_sri(parent: Uuid, local_name: &str) -> Result<Uuid, NameError> {
validate_segment(local_name)?;
Ok(Uuid::new_v5(&parent, local_name.as_bytes()))
}
pub fn sri_of_path(path: &str) -> Result<Uuid, NameError> {
let mut cur = ROOT_NS;
for seg in path.split(SEP) {
cur = child_sri(cur, seg)?;
}
Ok(cur)
}
pub fn resolve_target(s: &str) -> Result<Uuid, NameError> {
match Uuid::try_parse(s) {
Ok(uuid) => Ok(uuid),
Err(_) => sri_of_path(s),
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn root_ns_reads_as_intended() {
assert_eq!(ROOT_NS.to_string(), "a11ce115-dead-beef-cafe-c0ffee1decaf");
}
#[test]
fn known_answer_vectors() {
assert_eq!(
sri_of_path("myapp").unwrap().to_string(),
"2dc85832-4752-5fcc-87fb-50df0d636319",
);
}
#[test]
fn path_fold_equals_manual_v5_fold() {
let app = Uuid::new_v5(&ROOT_NS, b"myapp");
let gw = Uuid::new_v5(&app, b"gateway");
let worker = Uuid::new_v5(&gw, b"worker-1");
assert_eq!(sri_of_path("myapp").unwrap(), app);
assert_eq!(sri_of_path("myapp/gateway").unwrap(), gw);
assert_eq!(sri_of_path("myapp/gateway/worker-1").unwrap(), worker);
}
#[test]
fn child_of_parent_equals_full_path() {
let gw = sri_of_path("myapp/gateway").unwrap();
assert_eq!(
child_sri(gw, "worker-1").unwrap(),
sri_of_path("myapp/gateway/worker-1").unwrap(),
);
}
#[test]
fn grammar_rejects_bad_paths() {
assert_eq!(sri_of_path(""), Err(NameError::EmptySegment));
assert_eq!(sri_of_path("a//b"), Err(NameError::EmptySegment));
assert_eq!(sri_of_path("a/"), Err(NameError::EmptySegment));
assert_eq!(sri_of_path("/a"), Err(NameError::EmptySegment));
assert_eq!(sri_of_path("App"), Err(NameError::BadStart));
assert_eq!(child_sri(ROOT_NS, "-bad"), Err(NameError::BadStart));
assert_eq!(sri_of_path("a b"), Err(NameError::InvalidChar));
}
#[test]
fn resolve_target_accepts_uuid_or_path() {
let sri = sri_of_path("myapp/gateway").unwrap();
assert_eq!(resolve_target(&sri.to_string()).unwrap(), sri);
assert_eq!(resolve_target("myapp/gateway").unwrap(), sri);
}
}