You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
134 lines
3.8 KiB
134 lines
3.8 KiB
use std::borrow::Cow; |
|
use std::marker::PhantomData; |
|
use std::path::Path; |
|
|
|
use bevy::prelude::*; |
|
|
|
use bevy::asset::{UntypedAssetId, VisitAssetDependencies}; |
|
use bevy::platform::collections::HashMap; |
|
use derive_where::derive_where; |
|
use serde::{Deserialize, Serialize}; |
|
use thiserror::Error; |
|
|
|
#[derive(Reflect, Deref)] |
|
#[derive_where(Deserialize, Serialize, Clone, PartialEq, Eq, Hash, Debug)] |
|
#[serde(try_from = "String", into = "String")] |
|
pub struct Identifier<T> { |
|
#[deref] |
|
value: Cow<'static, str>, |
|
#[reflect(ignore)] |
|
identifier_type: PhantomData<fn(T)>, |
|
} |
|
|
|
impl<T> Identifier<T> { |
|
pub const unsafe fn new_unsafe(value: Cow<'static, str>) -> Self { |
|
Self { |
|
value, |
|
identifier_type: PhantomData, |
|
} |
|
} |
|
|
|
pub fn new(value: impl Into<String>) -> Result<Self, IdentifierParseError> { |
|
let value = value.into(); |
|
Self::validate(&value)?; |
|
Ok(unsafe { Self::new_unsafe(Cow::Owned(value)) }) |
|
} |
|
|
|
pub const fn new_const(value: &'static str) -> Self { |
|
match Self::validate(&value) { |
|
Ok(_) => unsafe { Self::new_unsafe(Cow::Borrowed(value)) }, |
|
Err(_) => panic!("invalid identifier"), |
|
} |
|
} |
|
|
|
const fn validate(value: &str) -> Result<(), IdentifierParseError> { |
|
if value.is_empty() { |
|
return Err(IdentifierParseError::Empty); |
|
} |
|
if value.len() > 32 { |
|
return Err(IdentifierParseError::Length); |
|
} |
|
// if value.chars().any(|c| !c.is_ascii_lowercase()) |
|
// NOTE: This mess is to allow the function to be called in a const context. |
|
let bytes = value.as_bytes(); |
|
let mut i = 0; |
|
while i < value.len() { |
|
if let Some(c) = char::from_u32(bytes[i] as u32) { |
|
if c.is_ascii_lowercase() { |
|
i += 1; |
|
continue; |
|
} |
|
} |
|
return Err(IdentifierParseError::Invalid); |
|
} |
|
Ok(()) |
|
} |
|
} |
|
|
|
#[derive(Error, Debug)] |
|
pub enum IdentifierParseError { |
|
#[error("String is empty")] |
|
Empty, |
|
#[error("String is too long")] |
|
Length, |
|
#[error("String contains invalid characters")] |
|
Invalid, |
|
} |
|
|
|
impl<T> std::fmt::Display for Identifier<T> { |
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
|
write!(f, "{}", self.value) |
|
} |
|
} |
|
|
|
impl<T> From<Identifier<T>> for String { |
|
fn from(value: Identifier<T>) -> Self { |
|
value.to_string() |
|
} |
|
} |
|
|
|
impl<T> TryFrom<String> for Identifier<T> { |
|
type Error = IdentifierParseError; |
|
fn try_from(value: String) -> std::result::Result<Self, Self::Error> { |
|
Self::new(value) |
|
} |
|
} |
|
|
|
impl<T> TryFrom<&Path> for Identifier<T> { |
|
type Error = IdentifierParseError; |
|
fn try_from(value: &Path) -> Result<Self, Self::Error> { |
|
// SAFETY: Since this is an asset path, it should never fail. |
|
let file_name = value.file_name().unwrap(); |
|
// SAFETY: If the asset path contains invalid UTF-8, it's someone else's fault. |
|
let file_name_str = file_name.to_str().expect("invalid utf8"); |
|
|
|
let id = if let Some((prefix, _)) = file_name_str.split_once('.') { |
|
if prefix.is_empty() { |
|
panic!("asset file name starts with dot") |
|
} |
|
prefix |
|
} else { |
|
file_name_str |
|
}; |
|
|
|
Self::new(id) |
|
} |
|
} |
|
|
|
#[derive(Deserialize, Serialize, Reflect, Deref, DerefMut)] |
|
#[derive_where(Default)] |
|
pub struct IdentifierMap<T, V>(HashMap<Identifier<T>, V>); |
|
|
|
impl<T, V: Clone> Clone for IdentifierMap<T, V> { |
|
fn clone(&self) -> Self { |
|
Self(self.0.clone()) |
|
} |
|
} |
|
|
|
impl<T, A: Asset> VisitAssetDependencies for IdentifierMap<T, Handle<A>> { |
|
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { |
|
for dependency in self.values() { |
|
visit(dependency.id().untyped()); |
|
} |
|
} |
|
}
|
|
|