use crate::bos::{Bos, DefaultStr};
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use core::str::FromStr;
#[cfg(feature = "std")]
use miette::{Diagnostic, SourceSpan};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use smol_str::SmolStr;
#[cfg(not(feature = "std"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceSpan(pub usize, pub usize);
#[cfg(not(feature = "std"))]
impl SourceSpan {
pub fn new(offset: usize, len: usize) -> Self {
Self(offset, len)
}
}
#[cfg(not(feature = "std"))]
impl From<(usize, usize)> for SourceSpan {
fn from((offset, len): (usize, usize)) -> Self {
Self(offset, len)
}
}
pub use crate::{
CowStr,
types::{
aturi::AtUri,
cid::{Cid, CidLink},
datetime::Datetime,
did::Did,
did_service::DidService,
handle::Handle,
ident::AtIdentifier,
language::Language,
nsid::Nsid,
recordkey::{RecordKey, Rkey},
tid::Tid,
uri::UriValue,
},
};
use crate::{
IntoStatic,
types::{LexiconStringType, UriType},
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AtprotoStr<S: Bos<str> + AsRef<str> = DefaultStr> {
Datetime(Datetime),
Language(Language),
Tid(Tid),
Nsid(Nsid<S>),
Did(Did<S>),
Handle(Handle<S>),
AtIdentifier(AtIdentifier<S>),
AtUri(AtUri<S>),
Uri(UriValue<S>),
Cid(Cid<S>),
RecordKey(RecordKey<Rkey<S>>),
String(S),
}
use crate::types::cid::IpldCid;
use crate::types::did::validate_did;
use crate::types::handle::validate_handle;
use crate::types::nsid::validate_nsid;
impl<S: Bos<str> + AsRef<str>> AtprotoStr<S> {
pub fn new(string: S) -> Self {
let s: &str = string.as_ref();
if let Ok(datetime) = Datetime::from_str(s) {
return Self::Datetime(datetime);
}
if let Ok(lang) = Language::new(s) {
return Self::Language(lang);
}
if let Ok(tid) = Tid::from_str(s) {
return Self::Tid(tid);
}
if validate_did(s).is_ok() {
return Self::Did(unsafe { Did::unchecked(string) });
}
if validate_handle(s).is_ok() {
return Self::Handle(unsafe { Handle::unchecked(string) });
}
if validate_nsid(s).is_ok() {
return Self::Nsid(unsafe { Nsid::unchecked(string) });
}
if crate::types::aturi::validate_and_index(s).is_ok() {
return Self::AtUri(unsafe { AtUri::unchecked(string) });
}
if s.starts_with("https://") || s.starts_with("wss://") || s.starts_with("ipld://") {
if let Ok(uri) = UriValue::new(s) {
match uri {
UriValue::Any(_) => {}
_ => {
return Self::Uri(UriValue::new(string).expect("already checked"));
}
}
}
}
let s: &str = string.as_ref();
if IpldCid::try_from(s).is_ok() || s.starts_with("bafy") {
return Self::Cid(unsafe { Cid::unchecked_str(string) });
}
Self::String(string)
}
pub fn as_str(&self) -> &str {
match self {
Self::Datetime(datetime) => datetime.as_str(),
Self::Language(lang) => lang.as_str(),
Self::Handle(handle) => handle.as_str(),
Self::AtIdentifier(atid) => atid.as_str(),
Self::Nsid(nsid) => nsid.as_str(),
Self::AtUri(aturi) => aturi.as_str(),
Self::Uri(uri) => uri.as_str(),
Self::Cid(cid) => cid.as_str(),
Self::Tid(tid) => tid.as_str(),
Self::Did(did) => did.as_str(),
Self::RecordKey(rkey) => rkey.as_ref(),
Self::String(string) => string.as_ref(),
}
}
pub fn string_type(&self) -> LexiconStringType {
match self {
Self::Datetime(_) => LexiconStringType::Datetime,
Self::Language(_) => LexiconStringType::Language,
Self::Handle(_) => LexiconStringType::Handle,
Self::AtIdentifier(_) => LexiconStringType::AtIdentifier,
Self::Nsid(_) => LexiconStringType::Nsid,
Self::AtUri(_) => LexiconStringType::AtUri,
Self::Uri(uri) => LexiconStringType::Uri(match uri {
UriValue::Did(_) => UriType::Did,
UriValue::At(_) => UriType::At,
UriValue::Https(_) => UriType::Https,
UriValue::Wss(_) => UriType::Wss,
UriValue::Cid(_) => UriType::Cid,
UriValue::Any(_) => UriType::Any,
}),
Self::Cid(_) => LexiconStringType::Cid,
Self::Tid(_) => LexiconStringType::Tid,
Self::Did(_) => LexiconStringType::Did,
Self::RecordKey(_) => LexiconStringType::RecordKey,
Self::String(_) => LexiconStringType::String,
}
}
}
impl<S: Bos<str> + AsRef<str>> AsRef<str> for AtprotoStr<S> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<S: Bos<str> + AsRef<str> + Serialize> Serialize for AtprotoStr<S> {
fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
where
Ser: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de, S> Deserialize<'de> for AtprotoStr<S>
where
S: Bos<str> + AsRef<str> + Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = S::deserialize(deserializer)?;
Ok(Self::new(value))
}
}
impl<S: Bos<str> + AsRef<str>> AtprotoStr<S> {
pub fn convert<B: Bos<str> + AsRef<str> + From<S>>(self) -> AtprotoStr<B> {
match self {
AtprotoStr::Datetime(dt) => AtprotoStr::Datetime(dt),
AtprotoStr::Language(lang) => AtprotoStr::Language(lang),
AtprotoStr::Tid(tid) => AtprotoStr::Tid(tid),
AtprotoStr::Nsid(nsid) => AtprotoStr::Nsid(nsid.convert()),
AtprotoStr::Did(did) => AtprotoStr::Did(did.convert()),
AtprotoStr::Handle(handle) => AtprotoStr::Handle(handle.convert()),
AtprotoStr::AtIdentifier(ident) => AtprotoStr::AtIdentifier(ident.convert()),
AtprotoStr::AtUri(at_uri) => AtprotoStr::AtUri(at_uri.convert()),
AtprotoStr::Uri(uri) => AtprotoStr::Uri(uri.convert()),
AtprotoStr::Cid(cid) => AtprotoStr::Cid(cid.convert()),
AtprotoStr::RecordKey(rkey) => AtprotoStr::RecordKey(RecordKey(rkey.0.convert())),
AtprotoStr::String(s) => AtprotoStr::String(B::from(s)),
}
}
}
impl<S: Bos<str> + AsRef<str> + IntoStatic> IntoStatic for AtprotoStr<S>
where
S::Output: Bos<str> + AsRef<str>,
{
type Output = AtprotoStr<S::Output>;
fn into_static(self) -> Self::Output {
match self {
AtprotoStr::Datetime(datetime) => AtprotoStr::Datetime(datetime),
AtprotoStr::Language(language) => AtprotoStr::Language(language),
AtprotoStr::Tid(tid) => AtprotoStr::Tid(tid),
AtprotoStr::Nsid(nsid) => AtprotoStr::Nsid(nsid.into_static()),
AtprotoStr::Did(did) => AtprotoStr::Did(did.into_static()),
AtprotoStr::Handle(handle) => AtprotoStr::Handle(handle.into_static()),
AtprotoStr::AtIdentifier(ident) => AtprotoStr::AtIdentifier(ident.into_static()),
AtprotoStr::AtUri(at_uri) => AtprotoStr::AtUri(at_uri.into_static()),
AtprotoStr::Uri(uri) => AtprotoStr::Uri(uri.into_static()),
AtprotoStr::Cid(cid) => AtprotoStr::Cid(cid.into_static()),
AtprotoStr::RecordKey(record_key) => AtprotoStr::RecordKey(record_key.into_static()),
AtprotoStr::String(s) => AtprotoStr::String(s.into_static()),
}
}
}
impl<S: Bos<str> + AsRef<str> + Clone + Serialize> From<AtprotoStr<S>> for String {
fn from(value: AtprotoStr<S>) -> Self {
value.as_str().to_string()
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "std", derive(Diagnostic))]
#[error("error in `{source}`: {kind}")]
#[cfg_attr(
feature = "std",
diagnostic(
url("https://atproto.com/specs/{spec}"),
help("if something doesn't match the spec, contact the crate author")
)
)]
pub struct AtStrError {
pub spec: SmolStr,
#[cfg_attr(feature = "std", source_code)]
pub source: String,
#[source]
#[cfg_attr(feature = "std", diagnostic_source)]
pub kind: StrParseKind,
}
impl AtStrError {
pub fn new(spec: &'static str, source: String, kind: StrParseKind) -> Self {
Self {
spec: SmolStr::new_static(spec),
source,
kind,
}
}
pub fn wrap(spec: &'static str, source: String, error: AtStrError) -> Self {
if let Some(span) = match &error.kind {
StrParseKind::Disallowed { problem, .. } => problem,
StrParseKind::MissingComponent { span, .. } => span,
_ => &None,
} {
Self {
spec: SmolStr::new_static(spec),
source,
kind: StrParseKind::Wrap {
span: Some(*span),
err: Arc::new(error),
},
}
} else {
let span = source
.find(&error.source)
.map(|start| (start, error.source.len()).into());
Self {
spec: SmolStr::new_static(spec),
source,
kind: StrParseKind::Wrap {
span,
err: Arc::new(error),
},
}
}
}
pub fn disallowed(spec: &'static str, source: &str, disallowed: &[&str]) -> Self {
for item in disallowed {
if let Some(loc) = source.find(item) {
return Self {
spec: SmolStr::new_static(spec),
source: source.to_string(),
kind: StrParseKind::Disallowed {
problem: Some(SourceSpan::new(loc.into(), item.len())),
message: smol_str::format_smolstr!("`{item}`"),
},
};
}
}
Self {
spec: SmolStr::new_static(spec),
source: source.to_string(),
kind: StrParseKind::Disallowed {
problem: None,
message: SmolStr::new_static(""),
},
}
}
pub fn too_long(spec: &'static str, source: &str, max: usize, actual: usize) -> Self {
Self {
spec: SmolStr::new_static(spec),
source: source.to_string(),
kind: StrParseKind::TooLong { max, actual },
}
}
pub fn too_short(spec: &'static str, source: &str, min: usize, actual: usize) -> Self {
Self {
spec: SmolStr::new_static(spec),
source: source.to_string(),
kind: StrParseKind::TooShort { min, actual },
}
}
pub fn missing(spec: &'static str, source: &str, expected: &str) -> Self {
if let Some(loc) = source.find(expected) {
return Self {
spec: SmolStr::new_static(spec),
source: source.to_string(),
kind: StrParseKind::MissingComponent {
span: Some(SourceSpan::new(loc.into(), expected.len())),
message: smol_str::format_smolstr!("`{expected}` found incorrectly here"),
},
};
}
Self {
spec: SmolStr::new_static(spec),
source: source.to_string(),
kind: StrParseKind::MissingComponent {
span: None,
message: SmolStr::new(expected),
},
}
}
pub fn missing_from(
spec: &'static str,
source: &str,
expected: &str,
span: (usize, usize),
) -> Self {
Self {
spec: SmolStr::new_static(spec),
source: source.to_string(),
kind: StrParseKind::MissingComponent {
span: Some(span.into()),
message: SmolStr::new(expected),
},
}
}
pub fn regex(spec: &'static str, source: &str, message: SmolStr) -> Self {
Self {
spec: SmolStr::new_static(spec),
source: source.to_string(),
kind: StrParseKind::RegexFail {
span: None,
message,
},
}
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "std", derive(Diagnostic))]
pub enum StrParseKind {
#[error("regex failure - {message}")]
#[cfg_attr(feature = "std", diagnostic(code(jacquard::types::string::regex_fail)))]
RegexFail {
#[cfg_attr(feature = "std", label)]
span: Option<SourceSpan>,
#[cfg_attr(feature = "std", help)]
message: SmolStr,
},
#[error("string too long (allowed: {max}, actual: {actual})")]
#[cfg_attr(
feature = "std",
diagnostic(code(jacquard::types::string::wrong_length))
)]
TooLong {
max: usize,
actual: usize,
},
#[error("string too short (allowed: {min}, actual: {actual})")]
#[cfg_attr(
feature = "std",
diagnostic(code(jacquard::types::string::wrong_length))
)]
TooShort {
min: usize,
actual: usize,
},
#[error("disallowed - {message}")]
#[cfg_attr(feature = "std", diagnostic(code(jacquard::types::string::disallowed)))]
Disallowed {
#[cfg_attr(feature = "std", label)]
problem: Option<SourceSpan>,
#[cfg_attr(feature = "std", help)]
message: SmolStr,
},
#[error("missing - {message}")]
#[cfg_attr(feature = "std", diagnostic(code(jacquard::atstr::missing_component)))]
MissingComponent {
#[cfg_attr(feature = "std", label)]
span: Option<SourceSpan>,
#[cfg_attr(feature = "std", help)]
message: SmolStr,
},
#[error("{err:?}")]
#[cfg_attr(feature = "std", diagnostic(code(jacquard::atstr::inner)))]
Wrap {
#[cfg_attr(feature = "std", label)]
span: Option<SourceSpan>,
#[source]
err: Arc<AtStrError>,
},
#[error("converting from a string slice")]
#[cfg_attr(feature = "std", diagnostic(code(jacquard::atstr::conversion)))]
Conversion,
}