use crate::bos::{Bos, DefaultStr};
use crate::types::recordkey::RecordKeyType;
use crate::types::string::{AtStrError, StrParseKind};
use crate::{CowStr, IntoStatic};
use alloc::string::{String, ToString};
use core::fmt;
use core::ops::Deref;
use core::str::FromStr;
#[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
use regex::Regex;
#[cfg(all(not(target_arch = "wasm32"), not(feature = "std")))]
use regex_automata::meta::Regex;
#[cfg(target_arch = "wasm32")]
use regex_lite::Regex;
use serde::{Deserialize, Deserializer, Serialize};
use smol_str::{SmolStr, ToSmolStr};
use super::Lazy;
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
#[serde(transparent)]
#[repr(transparent)]
pub struct Nsid<S: Bos<str> = DefaultStr>(pub(crate) S);
pub static NSID_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z][a-zA-Z0-9]{0,62})$").unwrap()
});
pub fn validate_nsid(nsid: &str) -> Result<(), AtStrError> {
if nsid.len() > 317 {
Err(AtStrError::too_long("nsid", nsid, 317, nsid.len()))
} else if !NSID_REGEX.is_match(nsid) {
Err(AtStrError::regex(
"nsid",
nsid,
SmolStr::new_static("invalid"),
))
} else {
Ok(())
}
}
impl<S: Bos<str> + AsRef<str>> Nsid<S> {
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
pub fn domain_authority(&self) -> &str {
let s = self.as_str();
let split = s.rfind('.').expect("enforced by constructor");
&s[..split]
}
pub fn name(&self) -> &str {
let s = self.as_str();
let split = s.rfind('.').expect("enforced by constructor");
&s[split + 1..]
}
}
impl<S: Bos<str>> Nsid<S> {
pub unsafe fn unchecked(nsid: S) -> Self {
Nsid(nsid)
}
pub fn borrow(&self) -> Nsid<&str>
where
S: AsRef<str>,
{
unsafe { Nsid::unchecked(self.0.as_ref()) }
}
}
impl<S: Bos<str> + AsRef<str>> Nsid<S> {
pub fn new(s: S) -> Result<Self, AtStrError> {
validate_nsid(s.as_ref())?;
Ok(Self(s))
}
pub fn raw(s: S) -> Self {
Self::new(s).expect("invalid NSID")
}
}
impl<S: Bos<str> + FromStr> Nsid<S> {
pub fn new_owned(nsid: impl AsRef<str>) -> Result<Self, AtStrError> {
let nsid = nsid.as_ref();
validate_nsid(nsid)?;
let s = S::from_str(nsid)
.map_err(|_| AtStrError::new("nsid", nsid.to_string(), StrParseKind::Conversion))?;
Ok(Self(s))
}
pub fn new_static(nsid: &'static str) -> Result<Self, AtStrError> {
validate_nsid(nsid)?;
let s = S::from_str(nsid)
.map_err(|_| AtStrError::new("nsid", nsid.to_string(), StrParseKind::Conversion))?;
Ok(Self(s))
}
}
impl<'de, S> Deserialize<'de> for Nsid<S>
where
S: Bos<str> + AsRef<str> + Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = S::deserialize(deserializer)?;
validate_nsid(s.as_ref()).map_err(serde::de::Error::custom)?;
Ok(Nsid(s))
}
}
impl<S: Bos<str> + IntoStatic> IntoStatic for Nsid<S>
where
S::Output: Bos<str>,
{
type Output = Nsid<S::Output>;
fn into_static(self) -> Self::Output {
Nsid(self.0.into_static())
}
}
impl<S: Bos<str>> Nsid<S> {
pub fn convert<B: Bos<str> + From<S>>(self) -> Nsid<B> {
Nsid(B::from(self.0))
}
}
impl<S: Bos<str> + FromStr> FromStr for Nsid<S> {
type Err = AtStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new_owned(s)
}
}
impl<S: Bos<str> + AsRef<str>> fmt::Display for Nsid<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl<S: Bos<str> + AsRef<str>> fmt::Debug for Nsid<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "at://{}", self.as_str())
}
}
impl<S: Bos<str> + AsRef<str>> From<Nsid<S>> for String {
fn from(value: Nsid<S>) -> Self {
value.as_str().to_string()
}
}
impl<S: Bos<str> + AsRef<str>> From<Nsid<S>> for SmolStr {
fn from(value: Nsid<S>) -> Self {
value.as_str().to_smolstr()
}
}
impl From<String> for Nsid {
fn from(value: String) -> Self {
Self::new_owned(value).unwrap()
}
}
impl<'n> From<CowStr<'n>> for Nsid<CowStr<'n>> {
fn from(value: CowStr<'n>) -> Self {
Self::new(value).unwrap()
}
}
impl From<SmolStr> for Nsid {
fn from(value: SmolStr) -> Self {
Self::new_owned(value).unwrap()
}
}
impl<S: Bos<str> + AsRef<str>> AsRef<str> for Nsid<S> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<S: Bos<str> + AsRef<str>> Deref for Nsid<S> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
unsafe impl<S: Bos<str> + AsRef<str> + Clone + Serialize> RecordKeyType for Nsid<S> {
fn as_str(&self) -> &str {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_nsids() {
assert!(Nsid::<&str>::new("com.example.foo").is_ok());
assert!(Nsid::<&str>::new("com.example.fooBar").is_ok());
assert!(Nsid::<&str>::new("com.long-domain.foo").is_ok());
assert!(Nsid::<&str>::new("a.b.c").is_ok());
assert!(Nsid::<&str>::new("a1.b2.c3").is_ok());
}
#[test]
fn minimum_segments() {
assert!(Nsid::<&str>::new("a.b.c").is_ok());
assert!(Nsid::<&str>::new("a.b").is_err());
assert!(Nsid::<&str>::new("a").is_err());
}
#[test]
fn domain_and_name_parsing() {
let nsid = Nsid::<&str>::new("com.example.fooBar").unwrap();
assert_eq!(nsid.domain_authority(), "com.example");
assert_eq!(nsid.name(), "fooBar");
}
#[test]
fn max_length() {
let s1 = format!("a{}a", "b".repeat(61));
let s2 = format!("c{}c", "d".repeat(61));
let s3 = format!("e{}e", "f".repeat(61));
let s4 = format!("g{}g", "h".repeat(61));
let s5 = format!("i{}i", "j".repeat(59));
let valid_317 = format!("{s1}.{s2}.{s3}.{s4}.{s5}");
assert_eq!(valid_317.len(), 317);
assert!(Nsid::<&str>::new(&valid_317).is_ok());
let s5_long = format!("i{}i", "j".repeat(60));
let too_long_318 = format!("{s1}.{s2}.{s3}.{s4}.{s5_long}");
assert_eq!(too_long_318.len(), 318);
assert!(Nsid::<&str>::new(&too_long_318).is_err());
}
#[test]
fn segment_length() {
let valid_63 = format!("{}.{}.foo", "a".repeat(63), "b".repeat(63));
assert!(Nsid::<&str>::new(&valid_63).is_ok());
let too_long_64 = format!("{}.b.foo", "a".repeat(64));
assert!(Nsid::<&str>::new(&too_long_64).is_err());
}
#[test]
fn first_segment_cannot_start_with_digit() {
assert!(Nsid::<&str>::new("com.example.foo").is_ok());
assert!(Nsid::<&str>::new("9com.example.foo").is_err());
}
#[test]
fn name_segment_rules() {
assert!(Nsid::<&str>::new("com.example.foo").is_ok());
assert!(Nsid::<&str>::new("com.example.fooBar123").is_ok());
assert!(Nsid::<&str>::new("com.example.9foo").is_err());
assert!(Nsid::<&str>::new("com.example.foo-bar").is_err());
}
#[test]
fn domain_segment_rules() {
assert!(Nsid::<&str>::new("foo-bar.example.baz").is_ok());
assert!(Nsid::<&str>::new("foo.bar-baz.qux").is_ok());
assert!(Nsid::<&str>::new("-foo.bar.baz").is_err());
assert!(Nsid::<&str>::new("foo-.bar.baz").is_err());
}
#[test]
fn case_sensitivity() {
assert!(Nsid::<&str>::new("com.example.fooBar").is_ok());
assert!(Nsid::<&str>::new("com.example.FooBar").is_ok());
}
#[test]
fn into_static() {
let n = Nsid::<&str>::new("com.example.foo").unwrap();
let owned: Nsid<SmolStr> = n.into_static();
assert_eq!(owned.as_str(), "com.example.foo");
}
}