elemaudio_rs/
resource.rs

1//! Rust-native resource registry for Elementary-style integrations.
2//!
3//! This module keeps resource ownership in Rust so hosts can store file-backed
4//! buffers, decoded media, ring buffers, or custom shared state behind a small
5//! and safe API.
6
7use crate::error::{Error, Result};
8use std::any::Any;
9use std::collections::HashMap;
10use std::collections::HashSet;
11use std::fmt::{Debug, Formatter};
12use std::sync::Arc;
13
14/// Identifier used to address a resource.
15#[derive(Clone, Debug, PartialEq, Eq, Hash)]
16pub struct ResourceId(String);
17
18impl ResourceId {
19    /// Creates a new resource identifier.
20    pub fn new(id: impl Into<String>) -> Result<Self> {
21        let id = id.into();
22
23        if id.trim().is_empty() {
24            return Err(Error::InvalidArgument("resource id cannot be empty"));
25        }
26
27        Ok(Self(id))
28    }
29
30    /// Returns the identifier as a string slice.
31    pub fn as_str(&self) -> &str {
32        &self.0
33    }
34}
35
36impl From<ResourceId> for String {
37    fn from(value: ResourceId) -> Self {
38        value.0
39    }
40}
41
42impl AsRef<str> for ResourceId {
43    fn as_ref(&self) -> &str {
44        self.as_str()
45    }
46}
47
48/// A resource stored in the manager.
49#[derive(Clone)]
50pub enum Resource {
51    /// Shared decoded audio buffer.
52    Audio(AudioBuffer),
53    /// Shared floating-point samples.
54    F32(Arc<[f32]>),
55    /// Shared double-precision samples.
56    F64(Arc<[f64]>),
57    /// Raw shared bytes.
58    Bytes(Arc<[u8]>),
59    /// UTF-8 text payload.
60    Text(Arc<str>),
61    /// Any custom Rust type wrapped in `Arc`.
62    Any(Arc<dyn Any + Send + Sync>),
63}
64
65impl Debug for Resource {
66    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
67        match self {
68            Self::Audio(buffer) => f
69                .debug_struct("Audio")
70                .field("sample_rate", &buffer.sample_rate)
71                .field("samples", &buffer.samples.len())
72                .finish(),
73            Self::F32(data) => f.debug_tuple("F32").field(&data.len()).finish(),
74            Self::F64(data) => f.debug_tuple("F64").field(&data.len()).finish(),
75            Self::Bytes(data) => f.debug_tuple("Bytes").field(&data.len()).finish(),
76            Self::Text(data) => f.debug_tuple("Text").field(&data.len()).finish(),
77            Self::Any(_) => f.write_str("Any(<opaque>)"),
78        }
79    }
80}
81
82impl Resource {
83    /// Wraps a `f32` slice in an owned shared resource.
84    pub fn f32(data: impl Into<Arc<[f32]>>) -> Self {
85        Self::F32(data.into())
86    }
87
88    /// Wraps a decoded audio buffer.
89    pub fn audio(buffer: AudioBuffer) -> Self {
90        Self::Audio(buffer)
91    }
92
93    /// Wraps a `f64` slice in an owned shared resource.
94    pub fn f64(data: impl Into<Arc<[f64]>>) -> Self {
95        Self::F64(data.into())
96    }
97
98    /// Wraps a byte slice in an owned shared resource.
99    pub fn bytes(data: impl Into<Arc<[u8]>>) -> Self {
100        Self::Bytes(data.into())
101    }
102
103    /// Wraps text in an owned shared resource.
104    pub fn text(data: impl Into<Arc<str>>) -> Self {
105        Self::Text(data.into())
106    }
107
108    /// Wraps any `Send + Sync + 'static` Rust value in an opaque resource.
109    pub fn custom<T>(value: T) -> Self
110    where
111        T: Any + Send + Sync,
112    {
113        Self::Any(Arc::new(value))
114    }
115
116    /// Wraps an already shared Rust value in an opaque resource.
117    pub fn shared<T>(value: Arc<T>) -> Self
118    where
119        T: Any + Send + Sync,
120    {
121        Self::Any(value)
122    }
123
124    /// Wraps a boxed Rust value in an opaque resource.
125    pub fn boxed<T>(value: Box<T>) -> Self
126    where
127        T: Any + Send + Sync,
128    {
129        let value: Arc<T> = Arc::from(value);
130        Self::Any(value)
131    }
132
133    /// Returns the resource kind for diagnostics.
134    pub fn kind(&self) -> &'static str {
135        match self {
136            Self::Audio(_) => "audio",
137            Self::F32(_) => "f32",
138            Self::F64(_) => "f64",
139            Self::Bytes(_) => "bytes",
140            Self::Text(_) => "text",
141            Self::Any(_) => "any",
142        }
143    }
144
145    /// Attempts to view the resource as shared `f32` samples.
146    pub fn as_f32(&self) -> Option<&[f32]> {
147        match self {
148            Self::F32(data) => Some(data.as_ref()),
149            _ => None,
150        }
151    }
152
153    /// Attempts to view the resource as a decoded audio buffer.
154    pub fn as_audio(&self) -> Option<&AudioBuffer> {
155        match self {
156            Self::Audio(buffer) => Some(buffer),
157            _ => None,
158        }
159    }
160
161    /// Attempts to view the resource as shared `f64` samples.
162    pub fn as_f64(&self) -> Option<&[f64]> {
163        match self {
164            Self::F64(data) => Some(data.as_ref()),
165            _ => None,
166        }
167    }
168
169    /// Attempts to view the resource as bytes.
170    pub fn as_bytes(&self) -> Option<&[u8]> {
171        match self {
172            Self::Bytes(data) => Some(data.as_ref()),
173            _ => None,
174        }
175    }
176
177    /// Attempts to view the resource as text.
178    pub fn as_text(&self) -> Option<&str> {
179        match self {
180            Self::Text(data) => Some(data.as_ref()),
181            _ => None,
182        }
183    }
184
185    /// Attempts to downcast an opaque resource to a concrete Rust type.
186    pub fn downcast<T>(&self) -> Option<Arc<T>>
187    where
188        T: Any + Send + Sync,
189    {
190        match self {
191            Self::Any(value) => value.clone().downcast::<T>().ok(),
192            _ => None,
193        }
194    }
195}
196
197/// Decoded audio stored in Rust-owned memory.
198#[derive(Clone, Debug)]
199pub struct AudioBuffer {
200    /// Interleaved or mono PCM data.
201    pub samples: Arc<[f32]>,
202    /// Source sample rate in Hz.
203    pub sample_rate: u32,
204    /// Number of channels in `samples`.
205    pub channels: u16,
206}
207
208impl AudioBuffer {
209    /// Creates a mono audio buffer.
210    pub fn mono(samples: impl Into<Arc<[f32]>>, sample_rate: u32) -> Self {
211        Self {
212            samples: samples.into(),
213            sample_rate,
214            channels: 1,
215        }
216    }
217
218    /// Returns the number of frames in the buffer.
219    pub fn frames(&self) -> usize {
220        self.samples.len() / self.channels as usize
221    }
222}
223
224/// Simple resource registry with safe add, replace, remove, and rename operations.
225#[derive(Debug, Clone, Default)]
226pub struct ResourceManager {
227    resources: HashMap<ResourceId, Resource>,
228}
229
230impl ResourceManager {
231    /// Creates an empty registry.
232    pub fn new() -> Self {
233        Self::default()
234    }
235
236    fn normalize_id(id: impl AsRef<str>) -> Result<ResourceId> {
237        ResourceId::new(id.as_ref())
238    }
239
240    /// Returns the number of registered resources.
241    pub fn len(&self) -> usize {
242        self.resources.len()
243    }
244
245    /// Returns `true` when the registry is empty.
246    pub fn is_empty(&self) -> bool {
247        self.resources.is_empty()
248    }
249
250    /// Returns a resource by id.
251    pub fn get(&self, id: impl AsRef<str>) -> Option<&Resource> {
252        let id = Self::normalize_id(id).ok()?;
253        self.resources.get(&id)
254    }
255
256    /// Returns a cloned resource by id.
257    pub fn get_cloned(&self, id: impl AsRef<str>) -> Option<Resource> {
258        self.get(id).cloned()
259    }
260
261    /// Returns a shared `f32` slice by id.
262    pub fn get_f32(&self, id: impl AsRef<str>) -> Option<&[f32]> {
263        self.get(id)?.as_f32()
264    }
265
266    /// Returns a shared `f64` slice by id.
267    pub fn get_f64(&self, id: impl AsRef<str>) -> Option<&[f64]> {
268        self.get(id)?.as_f64()
269    }
270
271    /// Returns raw bytes by id.
272    pub fn get_bytes(&self, id: impl AsRef<str>) -> Option<&[u8]> {
273        self.get(id)?.as_bytes()
274    }
275
276    /// Returns text by id.
277    pub fn get_text(&self, id: impl AsRef<str>) -> Option<&str> {
278        self.get(id)?.as_text()
279    }
280
281    /// Attempts to downcast a custom resource to a concrete Rust type.
282    pub fn get_custom<T>(&self, id: impl AsRef<str>) -> Option<Arc<T>>
283    where
284        T: Any + Send + Sync,
285    {
286        self.get(id)?.downcast::<T>()
287    }
288
289    /// Returns a shared `f32` slice or a type mismatch error.
290    pub fn require_f32(&self, id: impl AsRef<str>) -> Result<&[f32]> {
291        let id = Self::normalize_id(id)?;
292        let resource = self
293            .resources
294            .get(&id)
295            .ok_or_else(|| Error::ResourceNotFound(id.as_str().to_string()))?;
296
297        resource
298            .as_f32()
299            .ok_or_else(|| Error::ResourceTypeMismatch {
300                id: id.as_str().to_string(),
301                expected: "f32",
302                actual: resource.kind(),
303            })
304    }
305
306    /// Returns `true` if a resource with the given id exists.
307    pub fn contains(&self, id: impl AsRef<str>) -> bool {
308        self.get(id).is_some()
309    }
310
311    /// Inserts or replaces a resource.
312    pub fn insert(&mut self, id: impl AsRef<str>, resource: Resource) -> Result<Option<Resource>> {
313        let id = Self::normalize_id(id)?;
314        Ok(self.resources.insert(id, resource))
315    }
316
317    /// Adds a resource if the id is unused.
318    pub fn add(&mut self, id: impl AsRef<str>, resource: Resource) -> Result<()> {
319        let id = Self::normalize_id(id)?;
320
321        if self.resources.contains_key(&id) {
322            return Err(Error::ResourceExists(id.as_str().to_string()));
323        }
324
325        self.resources.insert(id, resource);
326        Ok(())
327    }
328
329    /// Replaces an existing resource.
330    pub fn replace(&mut self, id: impl AsRef<str>, resource: Resource) -> Result<Resource> {
331        let id = Self::normalize_id(id)?;
332
333        if !self.resources.contains_key(&id) {
334            return Err(Error::ResourceNotFound(id.as_str().to_string()));
335        }
336
337        Ok(self
338            .resources
339            .insert(id, resource)
340            .expect("resource existed before replace"))
341    }
342
343    /// Removes a resource from the registry.
344    pub fn remove(&mut self, id: impl AsRef<str>) -> Result<Resource> {
345        let id = Self::normalize_id(id)?;
346
347        self.resources
348            .remove(&id)
349            .ok_or_else(|| Error::ResourceNotFound(id.as_str().to_string()))
350    }
351
352    /// Renames a resource identifier without changing the underlying value.
353    pub fn rename(&mut self, from: impl AsRef<str>, to: impl AsRef<str>) -> Result<()> {
354        let from = Self::normalize_id(from)?;
355        let to = Self::normalize_id(to)?;
356
357        if from == to {
358            return Ok(());
359        }
360
361        if self.resources.contains_key(&to) {
362            return Err(Error::ResourceExists(to.as_str().to_string()));
363        }
364
365        let resource = self
366            .resources
367            .remove(&from)
368            .ok_or_else(|| Error::ResourceNotFound(from.as_str().to_string()))?;
369
370        self.resources.insert(to, resource);
371        Ok(())
372    }
373
374    /// Clears the registry.
375    pub fn clear(&mut self) {
376        self.resources.clear();
377    }
378
379    /// Returns an iterator over all registered ids and resources.
380    pub fn iter(&self) -> impl Iterator<Item = (&ResourceId, &Resource)> {
381        self.resources.iter()
382    }
383
384    /// Returns a cloned snapshot of all registered resources.
385    pub fn snapshot(&self) -> Vec<(ResourceId, Resource)> {
386        self.resources
387            .iter()
388            .map(|(id, resource)| (id.clone(), resource.clone()))
389            .collect()
390    }
391
392    /// Removes every resource whose id is not listed in `keep`.
393    pub fn prune_except<I, S>(&mut self, keep: I) -> Vec<(ResourceId, Resource)>
394    where
395        I: IntoIterator<Item = S>,
396        S: AsRef<str>,
397    {
398        let keep: HashSet<String> = keep.into_iter().map(|id| id.as_ref().to_string()).collect();
399
400        let mut removed = Vec::new();
401        self.resources.retain(|id, resource| {
402            if keep.contains(id.as_str()) {
403                true
404            } else {
405                removed.push((id.clone(), resource.clone()));
406                false
407            }
408        });
409
410        removed
411    }
412}