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}