use std::fmt;
use std::str::FromStr;
use crate::error::{Error, Result};
pub const DEFAULT_NAMESPACE: &str = "default";
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TableName {
namespace: String,
name: String,
}
impl TableName {
pub fn new(namespace: &str, name: &str) -> Result<Self> {
let valid = |p: &str| !p.is_empty() && p.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-');
if !valid(namespace) || !valid(name) {
return Err(Error::invalid(format!(
"table name {namespace:?}.{name:?} must be [namespace.]table using [A-Za-z0-9_-]"
)));
}
Ok(Self { namespace: namespace.to_string(), name: name.to_string() })
}
pub fn namespace(&self) -> &str {
&self.namespace
}
pub fn name(&self) -> &str {
&self.name
}
}
impl FromStr for TableName {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let (namespace, name) = s.split_once('.').unwrap_or((DEFAULT_NAMESPACE, s));
Self::new(namespace, name)
.map_err(|_| Error::invalid(format!("table name {s:?} must be [namespace.]table using [A-Za-z0-9_-]")))
}
}
impl TryFrom<&str> for TableName {
type Error = Error;
fn try_from(s: &str) -> Result<Self> {
s.parse()
}
}
impl TryFrom<String> for TableName {
type Error = Error;
fn try_from(s: String) -> Result<Self> {
s.parse()
}
}
impl TryFrom<&String> for TableName {
type Error = Error;
fn try_from(s: &String) -> Result<Self> {
s.parse()
}
}
impl From<&TableName> for TableName {
fn from(name: &TableName) -> Self {
name.clone()
}
}
impl fmt::Display for TableName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.namespace, self.name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_qualified_and_default_names() {
let n: TableName = "iot.readings".parse().unwrap();
assert_eq!((n.namespace(), n.name()), ("iot", "readings"));
let n: TableName = "my-table_2".parse().unwrap();
assert_eq!((n.namespace(), n.name()), (DEFAULT_NAMESPACE, "my-table_2"));
assert_eq!(n.to_string().parse::<TableName>().unwrap(), n);
}
#[test]
fn rejects_invalid_names() {
for bad in ["", ".", "iot.", ".readings", "a.b.c", "iot/readings", "../x", "a b", "naïve", "iot.read\"ings"] {
assert!(bad.parse::<TableName>().is_err(), "{bad:?}");
}
assert!(TableName::new("iot", "a.b").is_err());
}
#[test]
fn converts_from_strings_and_names() {
let n = TableName::try_from("iot.r").unwrap();
assert_eq!(TableName::try_from(String::from("iot.r")).unwrap(), n);
assert_eq!(TableName::from(&n), n);
}
}