limnus_asset_id/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/limnus
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5use crate::owner::{AssetOwner, DropMessage};
6use fixstr::FixStr;
7use message_channel::Sender;
8use owo_colors::OwoColorize;
9use std::any::{TypeId, type_name};
10use std::cmp::Ordering;
11use std::fmt;
12use std::fmt::{Debug, Display, Formatter};
13use std::marker::PhantomData;
14use std::path::PathBuf;
15use std::sync::Arc;
16pub mod owner;
17
18const FIXED_CAPACITY_SIZE: usize = 32;
19
20pub trait Asset: 'static + Debug + Send + Sync {}
21#[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy)]
22pub struct RawAssetId {
23    pub generation: u8,
24    pub index: u16,
25}
26
27impl RawAssetId {
28    #[must_use]
29    pub const fn new(generation: u8, index: u16) -> Self {
30        Self { generation, index }
31    }
32}
33
34impl From<RawWeakId> for RawAssetId {
35    fn from(value: RawWeakId) -> Self {
36        value.raw_id
37    }
38}
39
40impl Display for RawAssetId {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        write!(
43            f,
44            "{}-{}",
45            self.index.to_string().bright_green(),
46            self.generation.to_string().green()
47        )
48    }
49}
50
51fn short_type_name<T>() -> &'static str {
52    type_name::<T>().split("::").last().unwrap()
53}
54
55#[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy)]
56pub struct RawWeakId {
57    raw_id: RawAssetId,
58    type_id: TypeId,
59    debug_asset_name: AssetName,
60    debug_type_id: FixStr<32>,
61}
62
63impl<A: Asset> From<&Id<A>> for RawWeakId {
64    fn from(id: &Id<A>) -> Self {
65        Self {
66            raw_id: id.owner.raw_id().raw_id,
67            type_id: TypeId::of::<A>(),
68            debug_type_id: FixStr::new_unchecked(short_type_name::<A>()),
69            debug_asset_name: id.owner.asset_name().unwrap(),
70        }
71    }
72}
73
74impl RawWeakId {
75    #[must_use]
76    pub fn with_asset_type<A: Asset>(id: RawAssetId, asset_name: AssetName) -> Self {
77        Self {
78            raw_id: id,
79            type_id: TypeId::of::<A>(),
80            debug_asset_name: asset_name,
81            debug_type_id: FixStr::new_unchecked(short_type_name::<A>()),
82        }
83    }
84
85    #[must_use]
86    pub const fn type_id(&self) -> TypeId {
87        self.type_id
88    }
89}
90
91impl Display for RawWeakId {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
93        write!(
94            f,
95            "({} {} {})",
96            self.raw_id,
97            self.debug_asset_name,
98            self.debug_type_id.as_str().yellow()
99        )
100    }
101}
102
103/// You are free to copy and clone it, it has no ownership (no reference counting or similar)
104#[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
105pub struct WeakId<A: Asset> {
106    raw_id: RawWeakId,
107    phantom_data: PhantomData<A>,
108}
109
110// Manual Copy implementation
111impl<A: Asset> Copy for WeakId<A> {} // No bounds needed since PhantomData<T> is always Copy
112
113// Manual Clone implementation to satisfy Copy requirement
114impl<A: Asset> Clone for WeakId<A> {
115    fn clone(&self) -> Self {
116        *self
117    }
118}
119
120impl<A: Asset> WeakId<A> {
121    #[must_use]
122    pub const fn new(raw_id: RawWeakId) -> Self {
123        Self {
124            raw_id,
125            phantom_data: PhantomData,
126        }
127    }
128
129    #[must_use]
130    pub const fn raw_id(&self) -> RawWeakId {
131        self.raw_id
132    }
133}
134
135impl<A: Asset> Display for WeakId<A> {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        write!(f, "weak {}", self.raw_id)
138    }
139}
140
141impl<A: Asset> From<WeakId<A>> for RawWeakId {
142    fn from(id: WeakId<A>) -> Self {
143        id.raw_id
144    }
145}
146
147// Note: Do not implement a generic copy or clone for Id<A>
148pub struct Id<A: Asset> {
149    owner: Arc<AssetOwner>,
150    _phantom_data: PhantomData<A>,
151}
152
153impl<A: Asset> Debug for Id<A> {
154    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
155        write!(f, "{:?}", self.owner)
156    }
157}
158
159impl<A: Asset> Display for Id<A> {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        write!(f, "{}", self.owner)
162    }
163}
164
165impl<A: Asset> PartialEq<Self> for Id<A> {
166    fn eq(&self, other: &Self) -> bool {
167        self.owner.eq(&other.owner)
168    }
169}
170
171impl<A: Asset> Eq for Id<A> {}
172
173impl<A: Asset> PartialOrd<Self> for Id<A> {
174    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
175        Some(self.cmp(other))
176    }
177}
178
179impl<A: Asset> Ord for Id<A> {
180    fn cmp(&self, other: &Self) -> Ordering {
181        self.owner.cmp(&other.owner)
182    }
183}
184
185impl<A: Asset> Clone for Id<A> {
186    fn clone(&self) -> Self {
187        Self {
188            owner: self.owner.clone(),
189            _phantom_data: PhantomData,
190        }
191    }
192}
193
194impl<A: Asset> From<&Id<A>> for WeakId<A> {
195    fn from(id: &Id<A>) -> Self {
196        Self::new(id.owner.raw_id())
197    }
198}
199
200impl<A: Asset> Id<A> {
201    #[must_use]
202    pub fn new(raw_id: RawAssetId, sender: Sender<DropMessage>, asset_name: AssetName) -> Self {
203        let raw_id_type = RawWeakId::with_asset_type::<A>(raw_id, asset_name);
204        Self {
205            owner: Arc::new(AssetOwner::new(raw_id_type, Some(asset_name), sender)),
206            _phantom_data: PhantomData,
207        }
208    }
209
210    #[must_use]
211    pub fn asset_name(&self) -> Option<AssetName> {
212        self.owner.asset_name()
213    }
214}
215
216/// Validates an asset name according to strict (opinionated) naming conventions:
217///
218/// # Rules
219/// - Must start with a lowercase letter (a-z)
220/// - Can contain lowercase letters, numbers, underscores, hyphens and forward slashes
221/// - Cannot end with special characters: slash (/), underscore (_), dot (.) or hyphen (-)
222/// - Cannot contain consecutive special characters: slashes (//), underscores (__), dots (..) or hyphens (--)
223/// - Forward slashes (/) can be used as path separators
224///
225/// # Examples
226/// ```
227/// use limnus_asset_id::is_valid_asset_name;
228///
229/// assert!(is_valid_asset_name("assets/textures/wood"));
230/// assert!(is_valid_asset_name("player-model"));
231/// assert!(is_valid_asset_name("player2-model"));
232/// assert!(is_valid_asset_name("should.work.png"));
233/// assert!(!is_valid_asset_name("_invalid"));
234/// assert!(!is_valid_asset_name("also__invalid"));
235/// assert!(!is_valid_asset_name("assets//textures"));
236/// ```
237#[must_use]
238pub fn is_valid_asset_name(s: &str) -> bool {
239    if s.len() > FIXED_CAPACITY_SIZE {
240        return false;
241    }
242    let mut chars = s.chars();
243
244    matches!(chars.next(), Some(_c @ 'a'..='z'))
245        && !s.ends_with(['/', '-', '_', '.'])
246        && !s.contains("//")
247        && !s.contains("__")
248        && !s.contains("--")
249        && !s.contains("..")
250        && chars.all(|c| {
251            c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '-' | '/' | '.')
252        })
253}
254
255#[derive(Debug, Copy, Clone, Eq, Ord, PartialOrd, PartialEq, Hash)]
256pub struct AssetName {
257    value: FixStr<FIXED_CAPACITY_SIZE>,
258}
259
260impl AssetName {
261    #[must_use]
262    pub fn with_extension(&self, extension: &str) -> impl Into<Self> + use<> {
263        let added = format!("{}.{}", self.value.as_str(), extension);
264        Self {
265            value: FixStr::new_unchecked(added.as_str()),
266        }
267    }
268}
269
270// Example usage:
271impl AssetName {
272    /// # Panics
273    /// The asset name must be a valid name, checked by `is_valid_asset_name`.
274    #[must_use]
275    pub fn new(value: &str) -> Self {
276        assert!(is_valid_asset_name(value), "invalid asset name: {value}");
277        Self {
278            value: FixStr::new_unchecked(value),
279        }
280    }
281
282    #[must_use]
283    pub fn value(&self) -> &str {
284        self.value.as_str()
285    }
286}
287
288impl Display for AssetName {
289    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
290        let v = format!("{}", self.value);
291        write!(f, "{}", v.cyan())
292    }
293}
294
295impl From<&str> for AssetName {
296    fn from(value: &str) -> Self {
297        Self::new(value)
298    }
299}
300
301impl From<AssetName> for PathBuf {
302    fn from(value: AssetName) -> Self {
303        value.value().into()
304    }
305}
306
307impl<A: Asset> From<&Id<A>> for RawAssetId {
308    fn from(value: &Id<A>) -> Self {
309        value.owner.raw_id().raw_id
310    }
311}