use crate::error::IdentifierError;
use spacetimedb_data_structures::map::{Equivalent, HashSet};
use spacetimedb_sats::raw_identifier::RawIdentifier;
use spacetimedb_sats::{impl_deserialize, impl_serialize, impl_st};
use std::fmt::{self, Debug, Display};
use std::ops::Deref;
use unicode_ident::{is_xid_continue, is_xid_start};
use unicode_normalization::UnicodeNormalization;
lazy_static::lazy_static! {
static ref RESERVED_IDENTIFIERS: HashSet<&'static str> = include_str!("reserved_identifiers.txt").lines().collect();
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Identifier {
id: RawIdentifier,
}
impl_st!([] Identifier, ts => RawIdentifier::make_type(ts));
impl_serialize!([] Identifier, (self, ser) => ser.serialize_str(&self.id));
impl_deserialize!([] Identifier, de => RawIdentifier::deserialize(de).map(Self::new_assume_valid));
impl Identifier {
pub fn new_assume_valid(name: RawIdentifier) -> Self {
Self { id: name }
}
pub fn new(name: RawIdentifier) -> Result<Self, IdentifierError> {
if name.is_empty() {
return Err(IdentifierError::Empty {});
}
if name.nfc().zip(name.chars()).any(|(a, b)| a != b) {
return Err(IdentifierError::NotCanonicalized { name });
}
let mut chars = name.chars();
let start = chars.next().ok_or(IdentifierError::Empty {})?;
if !is_xid_start(start) && start != '_' {
return Err(IdentifierError::InvalidStart {
name,
invalid_start: start,
});
}
for char_ in chars {
if !is_xid_continue(char_) {
return Err(IdentifierError::InvalidContinue {
name,
invalid_continue: char_,
});
}
}
if Identifier::is_reserved(&name) {
return Err(IdentifierError::Reserved { name });
}
Ok(Identifier { id: name })
}
pub fn for_test(name: impl AsRef<str>) -> Self {
Identifier::new(RawIdentifier::new(name.as_ref())).unwrap()
}
pub fn as_raw(&self) -> &RawIdentifier {
&self.id
}
pub fn is_reserved(name: &str) -> bool {
RESERVED_IDENTIFIERS.contains(&*name.to_uppercase())
}
}
impl Debug for Identifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.id, f)
}
}
impl Display for Identifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.id, f)
}
}
impl Deref for Identifier {
type Target = str;
fn deref(&self) -> &str {
&self.id
}
}
impl Equivalent<Identifier> for str {
fn equivalent(&self, other: &Identifier) -> bool {
self == &other.id[..]
}
}
impl From<Identifier> for RawIdentifier {
fn from(id: Identifier) -> Self {
id.id
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
fn new(s: &str) -> Result<Identifier, IdentifierError> {
Identifier::new(RawIdentifier::new(s))
}
#[test]
fn test_a_bunch_of_identifiers() {
assert!(new("friends").is_ok());
assert!(new("Oysters").is_ok());
assert!(new("_hello").is_ok());
assert!(new("bananas_there_").is_ok());
assert!(new("Москва").is_ok());
assert!(new("東京").is_ok());
assert!(new("bees123").is_ok());
assert!(new("").is_err());
assert!(new("123bees").is_err());
assert!(new("\u{200B}hello").is_err()); assert!(new(" hello").is_err());
assert!(new("hello ").is_err());
assert!(new("🍌").is_err()); assert!(new("").is_err());
}
#[test]
fn test_canonicalization() {
assert!(new("_\u{0041}\u{030A}").is_err());
assert!(new("_\u{00C5}").is_ok());
}
proptest! {
#[test]
fn test_standard_ascii_identifiers(s in "[a-zA-Z_][a-zA-Z0-9_]*") {
prop_assume!(!Identifier::is_reserved(&s));
prop_assert!(new(&s).is_ok());
}
}
}