//! Resource identifiers. use std::borrow::{Borrow, Cow}; use std::cmp::Ordering; use std::fmt; use std::fmt::Formatter; use std::io::Write; use std::str::FromStr; use serde::de::Error as _; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; use crate::protocol::{Decode, Encode}; #[doc(hidden)] pub mod __private { pub use valence_core_macros::parse_ident_str; } /// A wrapper around a string type `S` which guarantees the wrapped string is a /// valid resource identifier. /// /// A resource identifier is a string divided into a "namespace" part and a /// "path" part. For instance `minecraft:apple` and `valence:frobnicator` are /// both valid identifiers. A string must match the regex /// `^([a-z0-9_.-]+:)?[a-z0-9_.-\/]+$` to be successfully parsed. /// /// While parsing, if the namespace part is left off (the part before and /// including the colon) then "minecraft:" is inserted at the beginning of the /// string. #[derive(Copy, Clone, Eq, Ord, Hash)] pub struct Ident { string: S, } /// Creates a new [`Ident`] at compile time from a string literal. A compile /// error is raised if the string is not a valid resource identifier. /// /// The type of the expression returned by this macro is `Ident<&'static str>`. /// /// # Examples /// /// ``` /// # use valence_core::{ident, ident::Ident}; /// let my_ident: Ident<&'static str> = ident!("apple"); /// /// println!("{my_ident}"); /// ``` #[macro_export] macro_rules! ident { ($string:literal) => { $crate::ident::Ident::<&'static str>::new_unchecked( $crate::ident::__private::parse_ident_str!($string), ) }; } /// The error type created when an [`Ident`] cannot be parsed from a /// string. Contains the string that failed to parse. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Error)] #[error("invalid resource identifier \"{0}\"")] pub struct IdentError(pub String); impl<'a> Ident> { pub fn new(string: impl Into>) -> Result { parse(string.into()) } } impl Ident { /// Internal API. Do not use. #[doc(hidden)] pub const fn new_unchecked(string: S) -> Self { Self { string } } pub fn as_str(&self) -> &str where S: AsRef, { self.string.as_ref() } pub fn as_str_ident(&self) -> Ident<&str> where S: AsRef, { Ident { string: self.as_str(), } } pub fn to_string_ident(&self) -> Ident where S: AsRef, { Ident { string: self.as_str().to_owned(), } } pub fn into_inner(self) -> S { self.string } /// Returns the namespace part of this resource identifier (the part before /// the colon). pub fn namespace(&self) -> &str where S: AsRef, { self.namespace_and_path().0 } /// Returns the path part of this resource identifier (the part after the /// colon). pub fn path(&self) -> &str where S: AsRef, { self.namespace_and_path().1 } pub fn namespace_and_path(&self) -> (&str, &str) where S: AsRef, { self.as_str() .split_once(':') .expect("invalid resource identifier") } } impl<'a> Ident> { pub fn borrowed(&self) -> Ident> { Ident::new_unchecked(Cow::Borrowed(self.as_str())) } } fn parse(string: Cow) -> Result>, IdentError> { let check_namespace = |s: &str| { !s.is_empty() && s.chars() .all(|c| matches!(c, 'a'..='z' | '0'..='9' | '_' | '.' | '-')) }; let check_path = |s: &str| { !s.is_empty() && s.chars() .all(|c| matches!(c, 'a'..='z' | '0'..='9' | '_' | '.' | '-' | '/')) }; match string.split_once(':') { Some((namespace, path)) if check_namespace(namespace) && check_path(path) => { Ok(Ident { string }) } None if check_path(&string) => Ok(Ident { string: format!("minecraft:{string}").into(), }), _ => Err(IdentError(string.into())), } } impl> AsRef for Ident { fn as_ref(&self) -> &str { self.string.as_ref() } } impl AsRef for Ident { fn as_ref(&self) -> &S { &self.string } } impl> Borrow for Ident { fn borrow(&self) -> &str { self.string.borrow() } } impl From> for String { fn from(value: Ident<&str>) -> Self { value.as_str().to_owned() } } impl From> for String { fn from(value: Ident) -> Self { value.into_inner() } } impl<'a> From>> for Cow<'a, str> { fn from(value: Ident>) -> Self { value.into_inner() } } impl<'a> From>> for Ident { fn from(value: Ident>) -> Self { Self { string: value.string.into(), } } } impl<'a> From> for Ident> { fn from(value: Ident) -> Self { Self { string: value.string.into(), } } } impl<'a> From> for Ident> { fn from(value: Ident<&'a str>) -> Self { Ident { string: value.string.into(), } } } impl<'a> From> for Ident { fn from(value: Ident<&'a str>) -> Self { Ident { string: value.string.into(), } } } impl FromStr for Ident { type Err = IdentError; fn from_str(s: &str) -> Result { Ok(Ident::new(s)?.into()) } } impl FromStr for Ident> { type Err = IdentError; fn from_str(s: &str) -> Result { Ident::::try_from(s).map(From::from) } } impl<'a> TryFrom<&'a str> for Ident { type Error = IdentError; fn try_from(value: &'a str) -> Result { Ok(Ident::new(value)?.into()) } } impl TryFrom for Ident { type Error = IdentError; fn try_from(value: String) -> Result { Ok(Ident::new(value)?.into()) } } impl<'a> TryFrom> for Ident { type Error = IdentError; fn try_from(value: Cow<'a, str>) -> Result { Ok(Ident::new(value)?.into()) } } impl<'a> TryFrom<&'a str> for Ident> { type Error = IdentError; fn try_from(value: &'a str) -> Result { Self::new(value) } } impl<'a> TryFrom for Ident> { type Error = IdentError; fn try_from(value: String) -> Result { Self::new(value) } } impl<'a> TryFrom> for Ident> { type Error = IdentError; fn try_from(value: Cow<'a, str>) -> Result { Self::new(value) } } impl fmt::Debug for Ident { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.string.fmt(f) } } impl fmt::Display for Ident { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.string.fmt(f) } } impl PartialEq> for Ident where S: PartialEq, { fn eq(&self, other: &Ident) -> bool { self.string == other.string } } impl PartialOrd> for Ident where S: PartialOrd, { fn partial_cmp(&self, other: &Ident) -> Option { self.string.partial_cmp(&other.string) } } impl Encode for Ident { fn encode(&self, w: impl Write) -> anyhow::Result<()> { self.as_ref().encode(w) } } impl<'a, S> Decode<'a> for Ident where S: Decode<'a>, Ident: TryFrom, { fn decode(r: &mut &'a [u8]) -> anyhow::Result { Ok(Ident::try_from(S::decode(r)?)?) } } impl From> for valence_nbt::Value where S: Into, { fn from(value: Ident) -> Self { value.into_inner().into() } } impl Serialize for Ident { fn serialize(&self, serializer: S) -> Result where S: Serializer, { self.string.serialize(serializer) } } impl<'de, S> Deserialize<'de> for Ident where S: Deserialize<'de>, Ident: TryFrom, { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { Ident::try_from(S::deserialize(deserializer)?).map_err(D::Error::custom) } } #[cfg(test)] mod tests { use super::*; #[test] fn check_namespace_and_path() { let id = ident!("namespace:path"); assert_eq!(id.namespace(), "namespace"); assert_eq!(id.path(), "path"); } #[test] fn parse_valid() { ident!("minecraft:whatever"); ident!("_what-ever55_:.whatever/whatever123456789_"); ident!("valence:frobnicator"); } #[test] #[should_panic] fn parse_invalid_0() { Ident::new("").unwrap(); } #[test] #[should_panic] fn parse_invalid_1() { Ident::new(":").unwrap(); } #[test] #[should_panic] fn parse_invalid_2() { Ident::new("foo:bar:baz").unwrap(); } #[test] fn equality() { assert_eq!(ident!("minecraft:my.identifier"), ident!("my.identifier")); } }