use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum IdError {
Empty,
BadChar { at: usize },
BadDot,
}
impl fmt::Display for IdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IdError::Empty => write!(f, "capability id is empty"),
IdError::BadChar { at } => {
write!(
f,
"capability id has a character outside a-z 0-9 _ . : at byte {at}"
)
}
IdError::BadDot => write!(f, "capability id has a leading, trailing or doubled '.'"),
}
}
}
impl std::error::Error for IdError {}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CapabilityId(String);
impl CapabilityId {
pub fn new(s: impl Into<String>) -> Result<Self, IdError> {
let s = s.into();
if s.is_empty() {
return Err(IdError::Empty);
}
let ok =
|b: u8| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'.' | b':');
if let Some(at) = s.bytes().position(|b| !ok(b)) {
return Err(IdError::BadChar { at });
}
if s.starts_with('.') || s.ends_with('.') || s.contains("..") {
return Err(IdError::BadDot);
}
Ok(CapabilityId(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_under(&self, other: &CapabilityId) -> bool {
self.0 == other.0
|| (self.0.starts_with(&other.0) && self.0.as_bytes().get(other.0.len()) == Some(&b'.'))
}
}
impl fmt::Display for CapabilityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::str::FromStr for CapabilityId {
type Err = IdError;
fn from_str(s: &str) -> Result<Self, IdError> {
CapabilityId::new(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_the_ids_in_the_design_and_in_unisql() {
for s in [
"kv.prefix_list.exactly_once",
"cast.to_uuid",
"fn:max",
"fn:string_agg.order_by",
"blob.max_value_size",
"rel.source_dialect",
] {
assert_eq!(CapabilityId::new(s).unwrap().as_str(), s);
}
}
#[test]
fn rejects_what_would_not_diff_or_parse() {
assert_eq!(CapabilityId::new(""), Err(IdError::Empty));
assert_eq!(CapabilityId::new("Kv.get"), Err(IdError::BadChar { at: 0 }));
assert_eq!(CapabilityId::new("kv get"), Err(IdError::BadChar { at: 2 }));
assert_eq!(CapabilityId::new("kv;get"), Err(IdError::BadChar { at: 2 }));
assert_eq!(CapabilityId::new("kv..get"), Err(IdError::BadDot));
assert_eq!(CapabilityId::new(".kv"), Err(IdError::BadDot));
assert_eq!(CapabilityId::new("kv."), Err(IdError::BadDot));
}
#[test]
fn is_under_matches_a_dot_boundary_only() {
let a = CapabilityId::new("kv.prefix_list").unwrap();
let b = CapabilityId::new("kv.prefix_list.exactly_once").unwrap();
let c = CapabilityId::new("kv.prefix_listing").unwrap();
assert!(b.is_under(&a) && a.is_under(&a));
assert!(
!c.is_under(&a),
"control: a shared string prefix is not a namespace parent"
);
assert!(!a.is_under(&b));
}
}