Skip to main content

khora_core/runtime/
resources.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! `Resources` — typed container for **engine resources**: long-lived
16//! shared state without a service-style API (caches, registries, settings,
17//! lookup tables).
18//!
19//! See [`crate::runtime`] for the broader Services / Backends / Resources
20//! taxonomy.
21
22use std::any::{type_name, Any, TypeId};
23use std::collections::HashMap;
24use std::sync::Arc;
25
26/// Container of engine resources — long-lived shared state that is
27/// principally data (with at most trivial accessors), not a service.
28///
29/// **Admission criteria.** A resource lives here when it is principally
30/// data with trivial accessors (HashMaps, settings, caches), is shared
31/// between several lanes / agents / data systems but has no rich business
32/// API, and is *long-lived* (per-tick state belongs in
33/// [`OutputDeck`](crate::lane::OutputDeck) or `LaneContext`).
34///
35/// Many resources will internally use `Arc<RwLock<…>>` or `Arc<Mutex<…>>`
36/// to support concurrent access. That is the resource's responsibility,
37/// not the container's.
38///
39/// API mirrors the legacy `ServiceRegistry` (drop-in replacement).
40#[derive(Default)]
41pub struct Resources {
42    inner: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
43    parent: Option<Arc<Resources>>,
44}
45
46impl Resources {
47    /// Creates an empty container.
48    #[must_use]
49    pub fn new() -> Self {
50        Self {
51            inner: HashMap::new(),
52            parent: None,
53        }
54    }
55
56    /// Creates a container that delegates lookups to `parent` when a key
57    /// is absent locally.
58    #[must_use]
59    pub fn with_parent(parent: Arc<Resources>) -> Self {
60        Self {
61            inner: HashMap::new(),
62            parent: Some(parent),
63        }
64    }
65
66    /// Inserts a resource, keyed by `T`'s `TypeId`.
67    pub fn insert<T: Send + Sync + 'static>(&mut self, resource: T) {
68        self.inner.insert(TypeId::of::<T>(), Box::new(resource));
69    }
70
71    /// Returns a borrow of the registered resource, walking the parent chain.
72    #[must_use]
73    pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
74        self.inner
75            .get(&TypeId::of::<T>())
76            .and_then(|b| b.downcast_ref::<T>())
77            .or_else(|| self.parent.as_deref()?.get::<T>())
78    }
79
80    /// Returns a borrow of the registered resource, panicking if absent.
81    pub fn require<T: Send + Sync + 'static>(&self) -> &T {
82        self.get::<T>().unwrap_or_else(|| {
83            panic!(
84                "Resources: required resource `{}` is not registered",
85                type_name::<T>()
86            )
87        })
88    }
89
90    /// Reports whether a resource of the given type is registered.
91    #[must_use]
92    pub fn contains<T: Send + Sync + 'static>(&self) -> bool {
93        self.inner.contains_key(&TypeId::of::<T>())
94            || self
95                .parent
96                .as_deref()
97                .map(|p| p.contains::<T>())
98                .unwrap_or(false)
99    }
100
101    /// Number of resources registered locally.
102    #[must_use]
103    pub fn len(&self) -> usize {
104        self.inner.len()
105    }
106
107    /// Whether no resources are registered locally.
108    #[must_use]
109    pub fn is_empty(&self) -> bool {
110        self.inner.is_empty()
111    }
112}
113
114impl std::fmt::Debug for Resources {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.debug_struct("Resources")
117            .field("registered", &self.inner.len())
118            .field("has_parent", &self.parent.is_some())
119            .finish()
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[derive(Default, Debug)]
128    struct InputMap {
129        bindings: u32,
130    }
131
132    #[test]
133    fn insert_and_get() {
134        let mut r = Resources::new();
135        r.insert(InputMap { bindings: 42 });
136        assert_eq!(r.get::<InputMap>().unwrap().bindings, 42);
137    }
138
139    #[test]
140    fn parent_chain_delegates() {
141        let mut parent = Resources::new();
142        parent.insert(InputMap { bindings: 7 });
143        let child = Resources::with_parent(Arc::new(parent));
144        assert_eq!(child.get::<InputMap>().unwrap().bindings, 7);
145    }
146}