//! Resource identifiers. use std::cmp::Ordering; use std::error::Error; use std::fmt; use std::fmt::Formatter; use std::hash::{Hash, Hasher}; use std::io::Write; use std::str::FromStr; use serde::de::Error as _; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::nbt; use crate::protocol::{Decode, Encode}; /// 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 considered valid. /// /// If the namespace part is left off (the part before and including the colon) /// the namespace is considered to be "minecraft" for the purposes of equality, /// ordering, and hashing. /// /// # Contract /// /// The type `S` must meet the following criteria: /// - All calls to [`AsRef::as_ref`] and [`Borrow::borrow`][borrow] while the /// string is wrapped in `Ident` must return the same value. /// /// [borrow]: std::borrow::Borrow::borrow #[derive(Copy, Clone, Debug)] pub struct Ident { string: S, path_start: usize, } impl> Ident { pub fn new(string: S) -> Result> { 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' | '_' | '.' | '-' | '/')) }; let str = string.as_ref(); match str.split_once(':') { Some((namespace, path)) if check_namespace(namespace) && check_path(path) => { let path_start = namespace.len() + 1; Ok(Self { string, path_start }) } None if check_path(str) => Ok(Self { string, path_start: 0, }), _ => Err(IdentError(string)), } } /// Returns the namespace part of this resource identifier. /// /// If the underlying string does not contain a namespace followed by a /// ':' character, `"minecraft"` is returned. pub fn namespace(&self) -> &str { if self.path_start == 0 { "minecraft" } else { &self.string.as_ref()[..self.path_start - 1] } } pub fn path(&self) -> &str { &self.string.as_ref()[self.path_start..] } /// Returns the underlying string as a `str`. pub fn as_str(&self) -> &str { self.string.as_ref() } /// Borrows the underlying string and returns it as an `Ident`. This /// operation is infallible and no checks need to be performed. pub fn as_str_ident(&self) -> Ident<&str> { Ident { string: self.string.as_ref(), path_start: self.path_start, } } /// Converts the underlying string to its owned representation and returns /// it as an `Ident`. This operation is infallible and no checks need to be /// performed. pub fn to_owned_ident(&self) -> Ident where S: ToOwned, S::Owned: AsRef, { Ident { string: self.string.to_owned(), path_start: self.path_start, } } /// Consumes the identifier and returns the underlying string. pub fn into_inner(self) -> S { self.string } /// Consumes the identifier and returns the underlying string. pub fn get(self) -> S { self.string } } impl FromStr for Ident { type Err = IdentError; fn from_str(s: &str) -> Result { Ident::new(s.to_owned()) } } impl TryFrom for Ident { type Error = IdentError; fn try_from(value: String) -> Result { Ident::new(value) } } impl From> for String where S: Into + AsRef, { fn from(id: Ident) -> Self { if id.path_start == 0 { format!("minecraft:{}", id.string.as_ref()) } else { id.string.into() } } } impl> fmt::Display for Ident { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}:{}", self.namespace(), self.path()) } } impl PartialEq> for Ident where S: AsRef, T: AsRef, { fn eq(&self, other: &Ident) -> bool { self.namespace() == other.namespace() && self.path() == other.path() } } impl Eq for Ident where S: AsRef {} impl PartialOrd> for Ident where S: AsRef, T: AsRef, { fn partial_cmp(&self, other: &Ident) -> Option { (self.namespace(), self.path()).partial_cmp(&(other.namespace(), other.path())) } } impl Ord for Ident where S: AsRef, { fn cmp(&self, other: &Self) -> Ordering { (self.namespace(), self.path()).cmp(&(other.namespace(), other.path())) } } impl Hash for Ident where S: AsRef, { fn hash(&self, state: &mut H) { (self.namespace(), self.path()).hash(state); } } impl Serialize for Ident where T: Serialize, { fn serialize(&self, serializer: S) -> Result where S: Serializer, { self.string.serialize(serializer) } } impl<'de, T> Deserialize<'de> for Ident where T: Deserialize<'de> + AsRef, { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { Ident::new(T::deserialize(deserializer)?).map_err(D::Error::custom) } } impl Encode for Ident { fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { self.string.encode(w) } } impl Decode for Ident where S: Decode + AsRef + Send + Sync + 'static, { fn decode(r: &mut &[u8]) -> anyhow::Result { Ok(Ident::new(S::decode(r)?)?) } } impl From> for nbt::Value where S: Into, { fn from(id: Ident) -> Self { id.string.into() } } /// The error type created when an [`Ident`] cannot be parsed from a /// string. Contains the offending string. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct IdentError(pub S); impl fmt::Debug for IdentError where S: AsRef, { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.debug_tuple("IdentError").field(&self.0.as_ref()).finish() } } impl fmt::Display for IdentError where S: AsRef, { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "invalid resource identifier \"{}\"", self.0.as_ref()) } } impl Error for IdentError where S: AsRef {} /// Convenience macro for constructing an [`Ident`] from a format /// string. /// /// The arguments to this macro are forwarded to [`std::format`]. /// /// # Panics /// /// The macro will cause a panic if the formatted string is not a valid resource /// identifier. See [`Ident`] for more information. /// /// [`Ident`]: [Ident] /// /// # Examples /// /// ``` /// use valence::ident; /// /// let namespace = "my_namespace"; /// let path = "my_path"; /// /// let id = ident!("{namespace}:{path}"); /// /// assert_eq!(id.namespace(), "my_namespace"); /// assert_eq!(id.path(), "my_path"); /// ``` #[macro_export] macro_rules! ident { ($($arg:tt)*) => {{ $crate::ident::Ident::new(::std::format!($($arg)*)).unwrap() }} } #[cfg(test)] mod tests { use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; 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!(""); } #[test] #[should_panic] fn parse_invalid_1() { ident!(":"); } #[test] #[should_panic] fn parse_invalid_2() { ident!("foo:bar:baz"); } #[test] fn equality() { assert_eq!(ident!("minecraft:my.identifier"), ident!("my.identifier")); } #[test] fn equal_hash() { let mut h1 = DefaultHasher::new(); ident!("minecraft:my.identifier").hash(&mut h1); let mut h2 = DefaultHasher::new(); ident!("my.identifier").hash(&mut h2); assert_eq!(h1.finish(), h2.finish()); } }