Skip to main content

zenith_core/asset/
provider.rs

1//! Asset sourcing layer for Zenith.
2//!
3//! Provides a deterministic, file-IO-free registry for resolving asset bytes
4//! by stable id. All ordering-sensitive collections use `BTreeMap` for
5//! determinism. No external crate dependencies — only `std`.
6
7use std::collections::BTreeMap;
8use std::sync::Arc;
9
10use crate::ast::AssetKind;
11
12/// Resolved asset bytes, ready for rendering or embedding. Cheap to clone (`Arc`).
13#[derive(Clone)]
14pub struct AssetData {
15    /// Stable identifier, e.g. `"asset.logo"`.
16    pub id: String,
17    /// Raw asset file bytes.
18    pub bytes: Arc<[u8]>,
19    /// The asset kind (image, svg, font, …).
20    pub kind: AssetKind,
21}
22
23impl std::fmt::Debug for AssetData {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("AssetData")
26            .field("id", &self.id)
27            .field("bytes_len", &self.bytes.len())
28            .field("kind", &self.kind)
29            .finish()
30    }
31}
32
33/// Resolve asset bytes by stable id.
34///
35/// Implementations must never perform file I/O. The CLI (or other callers)
36/// load bytes externally and register them before passing the provider in.
37pub trait AssetProvider {
38    /// Resolve an asset by its stable id.
39    ///
40    /// Returns `None` if no asset with the given id has been registered.
41    #[must_use]
42    fn by_id(&self, id: &str) -> Option<AssetData>;
43}
44
45/// In-memory asset registry. Register assets up front; this implementation
46/// never scans the filesystem.
47#[derive(Default)]
48pub struct BytesAssetProvider {
49    by_id: BTreeMap<String, AssetData>,
50}
51
52impl std::fmt::Debug for BytesAssetProvider {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct("BytesAssetProvider")
55            .field("registered_assets", &self.by_id.keys().collect::<Vec<_>>())
56            .finish()
57    }
58}
59
60impl BytesAssetProvider {
61    /// Create an empty registry.
62    #[must_use]
63    pub fn new() -> Self {
64        Self {
65            by_id: BTreeMap::new(),
66        }
67    }
68
69    /// Register an asset. If an asset with the same `id` already exists, the
70    /// most recent registration wins (bytes and kind are replaced).
71    pub fn register(&mut self, id: &str, kind: AssetKind, bytes: Arc<[u8]>) {
72        let key = id.to_owned();
73        let data = AssetData {
74            id: key.clone(),
75            bytes,
76            kind,
77        };
78        self.by_id.insert(key, data);
79    }
80}
81
82impl AssetProvider for BytesAssetProvider {
83    fn by_id(&self, id: &str) -> Option<AssetData> {
84        self.by_id.get(id).cloned()
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    fn dummy_bytes(fill: u8, len: usize) -> Arc<[u8]> {
93        Arc::from(vec![fill; len].as_slice())
94    }
95
96    #[test]
97    fn provider_new_is_empty() {
98        let p = BytesAssetProvider::new();
99        assert!(p.by_id("asset.missing").is_none());
100    }
101
102    #[test]
103    fn provider_register_and_by_id() {
104        let mut p = BytesAssetProvider::new();
105        let bytes_a = dummy_bytes(0xAA, 4);
106        let bytes_b = dummy_bytes(0xBB, 8);
107
108        p.register("asset.logo", AssetKind::Svg, bytes_a.clone());
109        p.register("asset.hero", AssetKind::Image, bytes_b.clone());
110
111        let logo = p.by_id("asset.logo").expect("asset.logo must be found");
112        assert_eq!(logo.id, "asset.logo");
113        assert_eq!(logo.kind, AssetKind::Svg);
114        assert_eq!(logo.bytes[0], 0xAA);
115        assert_eq!(logo.bytes.len(), 4);
116
117        let hero = p.by_id("asset.hero").expect("asset.hero must be found");
118        assert_eq!(hero.id, "asset.hero");
119        assert_eq!(hero.kind, AssetKind::Image);
120        assert_eq!(hero.bytes[0], 0xBB);
121        assert_eq!(hero.bytes.len(), 8);
122    }
123
124    #[test]
125    fn provider_unknown_id_returns_none() {
126        let mut p = BytesAssetProvider::new();
127        p.register("asset.a", AssetKind::Font, dummy_bytes(0, 1));
128        assert!(p.by_id("asset.does-not-exist").is_none());
129    }
130
131    #[test]
132    fn provider_re_register_overwrites() {
133        let mut p = BytesAssetProvider::new();
134        p.register("asset.x", AssetKind::Image, dummy_bytes(0x01, 2));
135        p.register("asset.x", AssetKind::Svg, dummy_bytes(0x02, 3));
136
137        let data = p.by_id("asset.x").expect("must be found");
138        assert_eq!(data.kind, AssetKind::Svg);
139        assert_eq!(data.bytes[0], 0x02);
140        assert_eq!(data.bytes.len(), 3);
141    }
142
143    #[test]
144    fn provider_by_id_clones_independently() {
145        let mut p = BytesAssetProvider::new();
146        p.register("asset.a", AssetKind::Image, dummy_bytes(0xFF, 4));
147
148        let d1 = p.by_id("asset.a").expect("first lookup");
149        let d2 = p.by_id("asset.a").expect("second lookup");
150        // Arc clones share bytes but are independent references.
151        assert_eq!(d1.bytes.len(), d2.bytes.len());
152        assert_eq!(d1.id, d2.id);
153    }
154}