Skip to main content

khora_core/runtime/
services.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//! `Services` — typed container for engine **services** (long-lived
16//! stateful objects with rich business APIs).
17//!
18//! See [`crate::runtime`] for the broader Services / Backends / Resources
19//! taxonomy.
20
21use std::any::{type_name, Any, TypeId};
22use std::collections::HashMap;
23use std::sync::Arc;
24
25/// Container of engine services — concrete stateful objects with rich APIs
26/// (asset loading, serialization, telemetry, DCC orchestration).
27///
28/// **Admission criteria.** A service is registered here when it is a
29/// concrete type with a non-trivial business API (≥ 3 methods that make
30/// sense together), lives for the engine lifetime, and is invoked by name.
31/// Trait implementations belong in [`crate::runtime::Backends`]. Plain
32/// shared state belongs in [`crate::runtime::Resources`].
33///
34/// API mirrors the legacy `ServiceRegistry` (drop-in replacement).
35#[derive(Default)]
36pub struct Services {
37    inner: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
38    parent: Option<Arc<Services>>,
39}
40
41impl Services {
42    /// Creates an empty container.
43    #[must_use]
44    pub fn new() -> Self {
45        Self {
46            inner: HashMap::new(),
47            parent: None,
48        }
49    }
50
51    /// Creates a container that delegates lookups to `parent` when a key
52    /// is absent locally. Local inserts shadow parent entries.
53    #[must_use]
54    pub fn with_parent(parent: Arc<Services>) -> Self {
55        Self {
56            inner: HashMap::new(),
57            parent: Some(parent),
58        }
59    }
60
61    /// Inserts a service, keyed by `T`'s `TypeId`. Replaces any prior
62    /// entry of the same type.
63    pub fn insert<T: Send + Sync + 'static>(&mut self, service: T) {
64        self.inner.insert(TypeId::of::<T>(), Box::new(service));
65    }
66
67    /// Returns a borrow of the registered service, walking the parent
68    /// chain on miss.
69    #[must_use]
70    pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
71        self.inner
72            .get(&TypeId::of::<T>())
73            .and_then(|b| b.downcast_ref::<T>())
74            .or_else(|| self.parent.as_deref()?.get::<T>())
75    }
76
77    /// Returns a borrow of the registered service, panicking if absent.
78    pub fn require<T: Send + Sync + 'static>(&self) -> &T {
79        self.get::<T>().unwrap_or_else(|| {
80            panic!(
81                "Services: required service `{}` is not registered",
82                type_name::<T>()
83            )
84        })
85    }
86
87    /// Reports whether a service of the given type is registered.
88    #[must_use]
89    pub fn contains<T: Send + Sync + 'static>(&self) -> bool {
90        self.inner.contains_key(&TypeId::of::<T>())
91            || self
92                .parent
93                .as_deref()
94                .map(|p| p.contains::<T>())
95                .unwrap_or(false)
96    }
97
98    /// Number of services registered locally (excluding parent chain).
99    #[must_use]
100    pub fn len(&self) -> usize {
101        self.inner.len()
102    }
103
104    /// Whether no services are registered locally.
105    #[must_use]
106    pub fn is_empty(&self) -> bool {
107        self.inner.is_empty()
108    }
109}
110
111impl std::fmt::Debug for Services {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        f.debug_struct("Services")
114            .field("registered", &self.inner.len())
115            .field("has_parent", &self.parent.is_some())
116            .finish()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    struct FakeAssetService {
125        name: String,
126    }
127    struct FakeSerializationService;
128
129    #[test]
130    fn insert_and_get() {
131        let mut s = Services::new();
132        s.insert(FakeAssetService {
133            name: "assets".into(),
134        });
135        assert_eq!(s.get::<FakeAssetService>().unwrap().name, "assets");
136    }
137
138    #[test]
139    fn require_panics_when_absent() {
140        let s = Services::new();
141        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
142            let _ = s.require::<FakeAssetService>().name.len();
143        }));
144        assert!(result.is_err());
145    }
146
147    #[test]
148    fn parent_chain_delegates() {
149        let mut parent = Services::new();
150        parent.insert(FakeAssetService {
151            name: "from-parent".into(),
152        });
153        let child = Services::with_parent(Arc::new(parent));
154        assert_eq!(child.get::<FakeAssetService>().unwrap().name, "from-parent");
155    }
156
157    #[test]
158    fn local_shadows_parent() {
159        let mut parent = Services::new();
160        parent.insert(FakeAssetService {
161            name: "parent".into(),
162        });
163        let mut child = Services::with_parent(Arc::new(parent));
164        child.insert(FakeAssetService {
165            name: "child".into(),
166        });
167        assert_eq!(child.get::<FakeAssetService>().unwrap().name, "child");
168    }
169
170    #[test]
171    fn contains_walks_parent() {
172        let mut parent = Services::new();
173        parent.insert(FakeSerializationService);
174        let child = Services::with_parent(Arc::new(parent));
175        assert!(child.contains::<FakeSerializationService>());
176    }
177}