2022-10-11 19:10:49 +11:00
|
|
|
//! Resource identifiers.
|
2022-07-15 16:18:20 +10:00
|
|
|
|
2022-04-15 07:55:45 +10:00
|
|
|
use std::borrow::Cow;
|
2022-10-11 19:10:49 +11:00
|
|
|
use std::cmp::Ordering;
|
2022-09-09 14:39:08 +10:00
|
|
|
use std::io::Write;
|
2022-04-15 07:55:45 +10:00
|
|
|
use std::str::FromStr;
|
2022-10-11 19:10:49 +11:00
|
|
|
use std::{fmt, hash};
|
2022-04-15 07:55:45 +10:00
|
|
|
|
|
|
|
use ascii::{AsAsciiStr, AsciiChar, AsciiStr, IntoAsciiString};
|
2022-10-11 19:10:49 +11:00
|
|
|
use hash::Hash;
|
2022-04-15 07:55:45 +10:00
|
|
|
use serde::de::Visitor;
|
2022-10-11 19:10:49 +11:00
|
|
|
use serde::{de, Deserialize, Deserializer, Serialize};
|
2022-04-15 07:55:45 +10:00
|
|
|
use thiserror::Error;
|
|
|
|
|
2022-09-23 21:03:21 +10:00
|
|
|
use crate::nbt;
|
2022-10-11 19:10:49 +11:00
|
|
|
use crate::protocol::{Decode, Encode};
|
2022-04-15 07:55:45 +10:00
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
/// 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.
|
2022-04-15 07:55:45 +10:00
|
|
|
///
|
|
|
|
/// If the namespace part is left off (the part before and including the colon)
|
2022-10-11 19:10:49 +11:00
|
|
|
/// the namespace is considered to be "minecraft" for the purposes of equality,
|
|
|
|
/// ordering, and hashing.
|
2022-04-15 07:55:45 +10:00
|
|
|
///
|
2022-09-02 17:06:45 +10:00
|
|
|
/// A string must match the regex `^([a-z0-9_-]+:)?[a-z0-9_\/.-]+$` to be a
|
|
|
|
/// valid identifier.
|
2022-04-15 07:55:45 +10:00
|
|
|
#[derive(Clone, Eq)]
|
2022-10-11 19:10:49 +11:00
|
|
|
pub struct Ident<'a> {
|
|
|
|
string: Cow<'a, AsciiStr>,
|
2022-04-15 07:55:45 +10:00
|
|
|
/// The index of the ':' character in the string.
|
|
|
|
/// If there is no namespace then it is `usize::MAX`.
|
|
|
|
///
|
|
|
|
/// Since the string only contains ASCII characters, we can slice it
|
|
|
|
/// in O(1) time.
|
|
|
|
colon_idx: usize,
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
/// The error type created when an [`Ident`] cannot be parsed from a
|
|
|
|
/// string. Contains the offending string.
|
2022-07-15 16:18:20 +10:00
|
|
|
#[derive(Clone, Debug, Error)]
|
2022-10-11 19:10:49 +11:00
|
|
|
#[error("invalid resource identifier \"{0}\"")]
|
|
|
|
pub struct IdentParseError<'a>(pub Cow<'a, str>);
|
2022-04-15 07:55:45 +10:00
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> Ident<'a> {
|
2022-04-15 07:55:45 +10:00
|
|
|
/// Parses a new identifier from a string.
|
|
|
|
///
|
2022-10-11 19:10:49 +11:00
|
|
|
/// An error is returned containing the input string if it is not a valid
|
|
|
|
/// resource identifier.
|
|
|
|
pub fn new(string: impl Into<Cow<'a, str>>) -> Result<Ident<'a>, IdentParseError<'a>> {
|
2022-04-15 07:55:45 +10:00
|
|
|
#![allow(bindings_with_variant_name)]
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
let cow = match string.into() {
|
2022-04-15 07:55:45 +10:00
|
|
|
Cow::Borrowed(s) => {
|
2022-10-11 19:10:49 +11:00
|
|
|
Cow::Borrowed(s.as_ascii_str().map_err(|_| IdentParseError(s.into()))?)
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
2022-10-11 19:10:49 +11:00
|
|
|
Cow::Owned(s) => Cow::Owned(
|
|
|
|
s.into_ascii_string()
|
|
|
|
.map_err(|e| IdentParseError(e.into_source().into()))?,
|
|
|
|
),
|
2022-04-15 07:55:45 +10:00
|
|
|
};
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
let str = cow.as_ref();
|
2022-04-15 07:55:45 +10:00
|
|
|
|
|
|
|
let check_namespace = |s: &AsciiStr| {
|
|
|
|
!s.is_empty()
|
|
|
|
&& s.chars()
|
|
|
|
.all(|c| matches!(c.as_char(), 'a'..='z' | '0'..='9' | '_' | '-'))
|
|
|
|
};
|
2022-10-11 19:10:49 +11:00
|
|
|
let check_path = |s: &AsciiStr| {
|
2022-04-15 07:55:45 +10:00
|
|
|
!s.is_empty()
|
|
|
|
&& s.chars()
|
|
|
|
.all(|c| matches!(c.as_char(), 'a'..='z' | '0'..='9' | '_' | '/' | '.' | '-'))
|
|
|
|
};
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
match str.chars().position(|c| c == AsciiChar::Colon) {
|
|
|
|
Some(colon_idx)
|
|
|
|
if check_namespace(&str[..colon_idx]) && check_path(&str[colon_idx + 1..]) =>
|
|
|
|
{
|
2022-04-15 07:55:45 +10:00
|
|
|
Ok(Self {
|
2022-10-11 19:10:49 +11:00
|
|
|
string: cow,
|
2022-04-15 07:55:45 +10:00
|
|
|
colon_idx,
|
|
|
|
})
|
|
|
|
}
|
2022-10-11 19:10:49 +11:00
|
|
|
None if check_path(str) => Ok(Self {
|
|
|
|
string: cow,
|
2022-04-15 07:55:45 +10:00
|
|
|
colon_idx: usize::MAX,
|
2022-10-11 19:10:49 +11:00
|
|
|
}),
|
|
|
|
_ => Err(IdentParseError(ascii_cow_to_str_cow(cow))),
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
/// Returns the namespace part of this resource identifier.
|
2022-09-02 17:06:45 +10:00
|
|
|
///
|
2022-04-15 07:55:45 +10:00
|
|
|
/// If this identifier was constructed from a string without a namespace,
|
2022-10-11 19:10:49 +11:00
|
|
|
/// then "minecraft" is returned.
|
|
|
|
pub fn namespace(&self) -> &str {
|
|
|
|
if self.colon_idx != usize::MAX {
|
|
|
|
self.string[..self.colon_idx].as_str()
|
2022-04-15 07:55:45 +10:00
|
|
|
} else {
|
2022-10-11 19:10:49 +11:00
|
|
|
"minecraft"
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
/// Returns the path part of this resource identifier.
|
2022-09-02 17:06:45 +10:00
|
|
|
pub fn path(&self) -> &str {
|
2022-04-15 07:55:45 +10:00
|
|
|
if self.colon_idx == usize::MAX {
|
2022-10-11 19:10:49 +11:00
|
|
|
self.string.as_str()
|
2022-04-15 07:55:45 +10:00
|
|
|
} else {
|
2022-10-11 19:10:49 +11:00
|
|
|
self.string[self.colon_idx + 1..].as_str()
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-23 21:03:21 +10:00
|
|
|
/// Returns the underlying string as a `str`.
|
2022-04-15 07:55:45 +10:00
|
|
|
pub fn as_str(&self) -> &str {
|
2022-10-11 19:10:49 +11:00
|
|
|
self.string.as_str()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Consumes the identifier and returns the underlying string.
|
|
|
|
pub fn into_inner(self) -> Cow<'a, str> {
|
|
|
|
ascii_cow_to_str_cow(self.string)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Used as the argument to `#[serde(deserialize_with = "...")]` when you
|
|
|
|
/// don't want to borrow data from the `'de` lifetime.
|
|
|
|
pub fn deserialize_to_owned<'de, D>(deserializer: D) -> Result<Ident<'static>, D::Error>
|
|
|
|
where
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
{
|
|
|
|
Ident::new(String::deserialize(deserializer)?).map_err(de::Error::custom)
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn ascii_cow_to_str_cow(cow: Cow<AsciiStr>) -> Cow<str> {
|
|
|
|
match cow {
|
|
|
|
Cow::Borrowed(s) => Cow::Borrowed(s.as_str()),
|
|
|
|
Cow::Owned(s) => Cow::Owned(s.into()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> fmt::Debug for Ident<'a> {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
f.debug_tuple("Ident").field(&self.as_str()).finish()
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> FromStr for Ident<'a> {
|
|
|
|
type Err = IdentParseError<'a>;
|
2022-04-15 07:55:45 +10:00
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
2022-07-06 12:21:52 +10:00
|
|
|
Ident::new(s.to_owned())
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> From<Ident<'a>> for String {
|
2022-06-10 13:26:21 +10:00
|
|
|
fn from(id: Ident) -> Self {
|
2022-10-11 19:10:49 +11:00
|
|
|
id.string.into_owned().into()
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> From<Ident<'a>> for Cow<'a, str> {
|
|
|
|
fn from(id: Ident<'a>) -> Self {
|
|
|
|
ascii_cow_to_str_cow(id.string)
|
2022-09-23 21:03:21 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> AsRef<str> for Ident<'a> {
|
2022-04-15 07:55:45 +10:00
|
|
|
fn as_ref(&self) -> &str {
|
|
|
|
self.as_str()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a, 'b> PartialEq<Ident<'b>> for Ident<'a> {
|
|
|
|
fn eq(&self, other: &Ident<'b>) -> bool {
|
|
|
|
(self.namespace(), self.path()) == (other.namespace(), other.path())
|
|
|
|
}
|
|
|
|
}
|
2022-04-15 07:55:45 +10:00
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a, 'b> PartialOrd<Ident<'b>> for Ident<'a> {
|
|
|
|
fn partial_cmp(&self, other: &Ident<'b>) -> Option<Ordering> {
|
|
|
|
(self.namespace(), self.path()).partial_cmp(&(other.namespace(), other.path()))
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> Hash for Ident<'a> {
|
|
|
|
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
|
|
|
self.namespace().hash(state);
|
|
|
|
self.path().hash(state);
|
|
|
|
}
|
|
|
|
}
|
2022-04-15 07:55:45 +10:00
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> fmt::Display for Ident<'a> {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
write!(f, "{}:{}", self.namespace(), self.path())
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> TryFrom<String> for Ident<'a> {
|
|
|
|
type Error = IdentParseError<'a>;
|
|
|
|
|
|
|
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
|
|
|
Ident::new(value)
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> TryFrom<&'a str> for Ident<'a> {
|
|
|
|
type Error = IdentParseError<'a>;
|
|
|
|
|
|
|
|
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
|
|
|
|
Ident::new(value)
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> From<Ident<'a>> for nbt::Value {
|
|
|
|
fn from(id: Ident<'a>) -> Self {
|
|
|
|
String::from(id).into()
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> Encode for Ident<'a> {
|
2022-04-15 07:55:45 +10:00
|
|
|
fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
|
2022-10-11 19:10:49 +11:00
|
|
|
self.as_str().encode(w)
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> Decode for Ident<'a> {
|
2022-09-09 14:39:08 +10:00
|
|
|
fn decode(r: &mut &[u8]) -> anyhow::Result<Self> {
|
2022-10-11 19:10:49 +11:00
|
|
|
Ok(Ident::new(String::decode(r)?)?)
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'a> Serialize for Ident<'a> {
|
2022-04-15 07:55:45 +10:00
|
|
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
|
|
self.as_str().serialize(serializer)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
/// This uses borrowed data from the `'de` lifetime. If you just want owned
|
|
|
|
/// data, see [`Ident::deserialize_to_owned`].
|
|
|
|
impl<'de> Deserialize<'de> for Ident<'de> {
|
|
|
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
|
|
|
deserializer.deserialize_string(IdentVisitor)
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
struct IdentVisitor;
|
2022-04-15 07:55:45 +10:00
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
impl<'de> Visitor<'de> for IdentVisitor {
|
|
|
|
type Value = Ident<'de>;
|
2022-04-15 07:55:45 +10:00
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
write!(f, "a valid Minecraft resource identifier")
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
|
|
|
|
dbg!("foo");
|
|
|
|
|
2022-06-10 13:26:21 +10:00
|
|
|
Ident::from_str(s).map_err(E::custom)
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
|
2022-10-11 19:10:49 +11:00
|
|
|
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: de::Error,
|
|
|
|
{
|
|
|
|
dbg!("bar");
|
|
|
|
|
|
|
|
Ident::new(v).map_err(E::custom)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_string<E: de::Error>(self, s: String) -> Result<Self::Value, E> {
|
|
|
|
dbg!("baz");
|
|
|
|
|
2022-06-10 13:26:21 +10:00
|
|
|
Ident::new(s).map_err(E::custom)
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-11 22:08:02 +10:00
|
|
|
/// Convenience macro for constructing an [`Ident`] from a format string.
|
2022-04-15 07:55:45 +10:00
|
|
|
///
|
2022-07-11 22:08:02 +10:00
|
|
|
/// The arguments to this macro are forwarded to [`std::format_args`].
|
|
|
|
///
|
|
|
|
/// # Panics
|
|
|
|
///
|
|
|
|
/// The macro will cause a panic if the formatted string is not a valid
|
2022-09-23 21:03:21 +10:00
|
|
|
/// identifier. See [`Ident`] for more information.
|
2022-07-11 22:08:02 +10:00
|
|
|
///
|
|
|
|
/// # Examples
|
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// use valence::ident;
|
|
|
|
///
|
|
|
|
/// let namespace = "my_namespace";
|
2022-10-11 19:10:49 +11:00
|
|
|
/// let path = ident!("{namespace}:my_path");
|
2022-07-11 22:08:02 +10:00
|
|
|
///
|
2022-10-11 19:10:49 +11:00
|
|
|
/// assert_eq!(path.namespace(), "my_namespace");
|
|
|
|
/// assert_eq!(path.path(), "my_path");
|
2022-07-11 22:08:02 +10:00
|
|
|
/// ```
|
2022-04-15 07:55:45 +10:00
|
|
|
#[macro_export]
|
|
|
|
macro_rules! ident {
|
|
|
|
($($arg:tt)*) => {{
|
2022-10-11 19:10:49 +11:00
|
|
|
let errmsg = "invalid resource identifier in `ident` macro";
|
2022-04-15 07:55:45 +10:00
|
|
|
#[allow(clippy::redundant_closure_call)]
|
|
|
|
(|args: ::std::fmt::Arguments| match args.as_str() {
|
2022-07-07 11:27:59 +10:00
|
|
|
Some(s) => $crate::ident::Ident::new(s).expect(errmsg),
|
|
|
|
None => $crate::ident::Ident::new(args.to_string()).expect(errmsg),
|
2022-04-15 07:55:45 +10:00
|
|
|
})(format_args!($($arg)*))
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2022-09-23 21:03:21 +10:00
|
|
|
use std::collections::hash_map::DefaultHasher;
|
2022-10-11 19:10:49 +11:00
|
|
|
use std::hash::Hasher;
|
|
|
|
|
|
|
|
use super::*;
|
2022-09-23 21:03:21 +10:00
|
|
|
|
2022-04-15 07:55:45 +10:00
|
|
|
#[test]
|
|
|
|
fn parse_valid() {
|
|
|
|
ident!("minecraft:whatever");
|
|
|
|
ident!("_what-ever55_:.whatever/whatever123456789_");
|
2022-10-11 19:10:49 +11:00
|
|
|
ident!("valence:frobnicator");
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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"));
|
|
|
|
}
|
2022-09-23 21:03:21 +10:00
|
|
|
|
|
|
|
#[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());
|
|
|
|
}
|
2022-10-11 19:10:49 +11:00
|
|
|
|
|
|
|
fn check_borrowed(id: Ident) {
|
|
|
|
if let Cow::Owned(_) = id.into_inner() {
|
|
|
|
panic!("not borrowed!");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn literal_is_borrowed() {
|
|
|
|
check_borrowed(ident!("akjghsjkhebf"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn visit_borrowed_str_works() {
|
|
|
|
let data = String::from("valence:frobnicator");
|
|
|
|
|
|
|
|
check_borrowed(
|
|
|
|
IdentVisitor
|
|
|
|
.visit_borrowed_str::<de::value::Error>(data.as_ref())
|
|
|
|
.unwrap(),
|
|
|
|
);
|
|
|
|
}
|
2022-04-15 07:55:45 +10:00
|
|
|
}
|