use std::fmt;
use crate::{Decode, Encode, EncodedSize, Error, Result, VarInt};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Identifier {
pub namespace: String,
pub path: String,
}
impl Identifier {
pub fn new(namespace: impl Into<String>, path: impl Into<String>) -> Result<Self> {
let namespace = namespace.into();
let path = path.into();
if !namespace.chars().all(is_valid_namespace_char) {
return Err(Error::InvalidData(format!(
"invalid namespace character in '{namespace}'"
)));
}
if !path.chars().all(is_valid_path_char) {
return Err(Error::InvalidData(format!(
"invalid path character in '{path}'"
)));
}
Ok(Self { namespace, path })
}
pub fn minecraft(path: impl Into<String>) -> Result<Self> {
Self::new("minecraft", path)
}
#[deprecated(note = "use Display impl via to_string() instead")]
pub fn as_str(&self) -> String {
self.to_string()
}
}
fn is_valid_namespace_char(c: char) -> bool {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '.' || c == '_' || c == '-'
}
fn is_valid_path_char(c: char) -> bool {
is_valid_namespace_char(c) || c == '/'
}
fn parse_identifier(s: &str) -> Result<Identifier> {
if s.is_empty() {
return Err(Error::InvalidData("empty identifier".into()));
}
let (namespace, path) = match s.find(':') {
Some(pos) => (&s[..pos], &s[pos + 1..]),
None => ("minecraft", s),
};
Identifier::new(namespace, path)
}
impl Encode for Identifier {
fn encode(&self, buf: &mut Vec<u8>) -> Result<()> {
self.to_string().encode(buf)
}
}
impl Decode for Identifier {
fn decode(buf: &mut &[u8]) -> Result<Self> {
let s = String::decode(buf)?;
parse_identifier(&s)
}
}
impl EncodedSize for Identifier {
fn encoded_size(&self) -> usize {
let str_len = self.namespace.len() + 1 + self.path.len();
VarInt(str_len as i32).encoded_size() + str_len
}
}
impl fmt::Display for Identifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.namespace, self.path)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(namespace: &str, path: &str) {
let id = Identifier::new(namespace, path).unwrap();
let mut buf = Vec::with_capacity(id.encoded_size());
id.encode(&mut buf).unwrap();
assert_eq!(buf.len(), id.encoded_size());
let mut cursor = buf.as_slice();
let decoded = Identifier::decode(&mut cursor).unwrap();
assert!(cursor.is_empty());
assert_eq!(decoded, id);
}
#[test]
fn new_valid() {
let id = Identifier::new("minecraft", "stone").unwrap();
assert_eq!(id.namespace, "minecraft");
assert_eq!(id.path, "stone");
}
#[test]
fn minecraft_shorthand() {
let id = Identifier::minecraft("diamond").unwrap();
assert_eq!(id.namespace, "minecraft");
assert_eq!(id.path, "diamond");
}
#[test]
fn custom_namespace() {
let id = Identifier::new("mymod", "custom_block").unwrap();
assert_eq!(id.namespace, "mymod");
assert_eq!(id.path, "custom_block");
}
#[test]
fn path_with_slashes() {
let id = Identifier::new("minecraft", "textures/block/dirt").unwrap();
assert_eq!(id.path, "textures/block/dirt");
}
#[test]
fn invalid_namespace_uppercase() {
assert!(Identifier::new("Minecraft", "stone").is_err());
}
#[test]
fn invalid_namespace_space() {
assert!(Identifier::new("my mod", "stone").is_err());
}
#[test]
fn invalid_path_uppercase() {
assert!(Identifier::new("minecraft", "Stone").is_err());
}
#[test]
fn valid_special_chars() {
assert!(Identifier::new("my-mod.v2", "custom_item-v3").is_ok());
}
#[test]
fn parse_with_namespace() {
let id = parse_identifier("minecraft:stone").unwrap();
assert_eq!(id.namespace, "minecraft");
assert_eq!(id.path, "stone");
}
#[test]
fn parse_without_namespace() {
let id = parse_identifier("stone").unwrap();
assert_eq!(id.namespace, "minecraft");
assert_eq!(id.path, "stone");
}
#[test]
fn parse_empty() {
assert!(parse_identifier("").is_err());
}
#[test]
fn parse_custom_namespace() {
let id = parse_identifier("mymod:custom_block").unwrap();
assert_eq!(id.namespace, "mymod");
assert_eq!(id.path, "custom_block");
}
#[test]
fn roundtrip_minecraft() {
roundtrip("minecraft", "stone");
}
#[test]
fn roundtrip_custom() {
roundtrip("mymod", "custom_block");
}
#[test]
fn roundtrip_with_path_slashes() {
roundtrip("minecraft", "textures/block/dirt");
}
#[test]
fn decode_bare_path() {
let mut buf = Vec::new();
"stone".to_string().encode(&mut buf).unwrap();
let mut cursor = buf.as_slice();
let id = Identifier::decode(&mut cursor).unwrap();
assert_eq!(id.namespace, "minecraft");
assert_eq!(id.path, "stone");
}
#[test]
fn display() {
let id = Identifier::new("minecraft", "stone").unwrap();
assert_eq!(id.to_string(), "minecraft:stone");
}
#[test]
fn to_string_format() {
let id = Identifier::new("mymod", "item").unwrap();
assert_eq!(id.to_string(), "mymod:item");
}
#[test]
fn encoded_size_includes_colon() {
let id = Identifier::new("minecraft", "stone").unwrap();
assert_eq!(id.encoded_size(), 16);
}
mod proptests {
use super::*;
use proptest::prelude::*;
fn namespace_strategy() -> impl Strategy<Value = String> {
"[a-z0-9._\\-]{1,20}"
}
fn path_strategy() -> impl Strategy<Value = String> {
"[a-z0-9._\\-/]{1,50}"
}
proptest! {
#[test]
fn identifier_roundtrip(
namespace in namespace_strategy(),
path in path_strategy(),
) {
roundtrip(&namespace, &path);
}
}
}
}