Skip to main content

sayiir_core/
deps.rs

1//! Dependency-injection container and integration trait.
2//!
3//! Two halves sit in this module:
4//!
5//! 1. The [`Deps`] type-keyed service container plus its [`DepsBuilder`] —
6//!    register cloneable values once, resolve them by type from anywhere a
7//!    `&Deps` is available.
8//! 2. The [`DepsInjectable`] trait that ties [`crate::task::RegisterableTask`]
9//!    to `Deps`. Implemented automatically by the `#[task]` proc-macro; used
10//!    by [`crate::registry::TaskRegistry::register_from_deps`]
11//!    and the `workflow! { deps: … }` expansion.
12//!
13//! # Quick Example
14//!
15//! ```
16//! use sayiir_core::deps::Deps;
17//! use std::sync::Arc;
18//!
19//! #[derive(Clone)]
20//! struct HttpClient;
21//!
22//! let deps = Deps::builder()
23//!     .insert(Arc::new(HttpClient))
24//!     .build();
25//!
26//! let client: Arc<HttpClient> = deps.expect();
27//! ```
28//!
29//! # Lookup Rules
30//!
31//! Resolution is by **exact `TypeId`**. Insert `Arc<HttpClient>` → resolve
32//! `Arc<HttpClient>`. There is no coercion to traits or supertypes.
33//!
34//! Stored values must be `Send + Sync + 'static`, and `get` / `expect` /
35//! `try_get` require `Clone` because the container owns one copy per type.
36
37use std::any::{Any, TypeId, type_name};
38use std::collections::HashMap;
39use std::fmt;
40
41use crate::task::RegisterableTask;
42
43/// A type-keyed container of cloneable services.
44///
45/// Built with [`Deps::builder`] and read by type via [`Deps::get`], [`Deps::expect`],
46/// or [`Deps::try_get`]. Used by `#[task]`-generated `from_deps` constructors.
47#[derive(Default)]
48pub struct Deps {
49    map: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
50}
51
52impl Deps {
53    /// Create an empty container.
54    #[must_use]
55    pub fn new() -> Self {
56        Self {
57            map: HashMap::new(),
58        }
59    }
60
61    /// Start a builder.
62    #[must_use]
63    pub fn builder() -> DepsBuilder {
64        DepsBuilder { inner: Deps::new() }
65    }
66
67    /// Resolve `T`, returning a fresh clone, or `None` if the type was never inserted.
68    #[must_use]
69    pub fn get<T>(&self) -> Option<T>
70    where
71        T: Clone + Send + Sync + 'static,
72    {
73        self.map
74            .get(&TypeId::of::<T>())
75            .and_then(|v| v.downcast_ref::<T>())
76            .cloned()
77    }
78
79    /// Resolve `T`, returning a fresh clone, or [`MissingDep`] describing the missing type.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`MissingDep`] if no value of type `T` was inserted into the container.
84    pub fn try_get<T>(&self) -> Result<T, MissingDep>
85    where
86        T: Clone + Send + Sync + 'static,
87    {
88        self.get::<T>().ok_or_else(MissingDep::of::<T>)
89    }
90
91    /// Resolve `T`, panicking with the type name on miss.
92    ///
93    /// Use [`Deps::try_get`] (or [`Deps::get`]) when a missing dependency should
94    /// be a recoverable error. `expect` is meant for codegen sites that have
95    /// *already* been verified by `verify_deps`.
96    ///
97    /// # Panics
98    ///
99    /// Panics if no value of type `T` was inserted into the container.
100    #[must_use]
101    pub fn expect<T>(&self) -> T
102    where
103        T: Clone + Send + Sync + 'static,
104    {
105        match self.get::<T>() {
106            Some(v) => v,
107            None => missing_panic(type_name::<T>()),
108        }
109    }
110
111    /// Returns `true` if a value of type `T` is present.
112    #[must_use]
113    pub fn contains<T>(&self) -> bool
114    where
115        T: 'static,
116    {
117        self.map.contains_key(&TypeId::of::<T>())
118    }
119
120    /// Number of registered types.
121    #[must_use]
122    pub fn len(&self) -> usize {
123        self.map.len()
124    }
125
126    /// Whether no types are registered.
127    #[must_use]
128    pub fn is_empty(&self) -> bool {
129        self.map.is_empty()
130    }
131
132    /// Move every entry from `other` into `self`. For any type present in both
133    /// containers, the entry from `other` wins.
134    ///
135    /// Use this to layer service containers — e.g. start from a base
136    /// container provided by a library, then merge in application-specific
137    /// services before passing the result to `workflow! { deps: … }`.
138    ///
139    /// Merging does not retroactively affect tasks that were already
140    /// constructed from this container (they hold their own clones).
141    pub fn merge(&mut self, other: Self) {
142        self.map.extend(other.map);
143    }
144}
145
146impl fmt::Debug for Deps {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        f.debug_struct("Deps")
149            .field("registered_types", &self.map.len())
150            .finish()
151    }
152}
153
154/// Builder for [`Deps`]. Use [`Deps::builder`] to create one.
155pub struct DepsBuilder {
156    inner: Deps,
157}
158
159impl DepsBuilder {
160    /// Insert (or replace) a value of type `T`.
161    ///
162    /// `T` is the key — if two `insert` calls share the same type, the later
163    /// one wins.
164    #[must_use]
165    pub fn insert<T>(mut self, dep: T) -> Self
166    where
167        T: Clone + Send + Sync + 'static,
168    {
169        self.inner.map.insert(TypeId::of::<T>(), Box::new(dep));
170        self
171    }
172
173    /// Merge every entry from `other` into the builder. For any type present
174    /// in both, the entry from `other` wins.
175    ///
176    /// Useful for layering: start from a pre-built library container and
177    /// extend it with application-specific services before calling `build`.
178    #[must_use]
179    pub fn merge(mut self, other: Deps) -> Self {
180        self.inner.merge(other);
181        self
182    }
183
184    /// Finalize and return the [`Deps`] container.
185    #[must_use]
186    pub fn build(self) -> Deps {
187        self.inner
188    }
189}
190
191impl Default for DepsBuilder {
192    fn default() -> Self {
193        Deps::builder()
194    }
195}
196
197/// A dependency that was requested from a [`Deps`] container but not present.
198///
199/// `#[task]`-generated `verify_deps` returns a `Vec<MissingDep>`; the
200/// `workflow!` macro converts those into build errors.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct MissingDep {
203    /// The `std::any::type_name` of the missing type.
204    pub type_name: &'static str,
205}
206
207impl MissingDep {
208    /// Build a `MissingDep` for type `T`.
209    #[must_use]
210    pub fn of<T: ?Sized + 'static>() -> Self {
211        Self {
212            type_name: type_name::<T>(),
213        }
214    }
215}
216
217impl fmt::Display for MissingDep {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        write!(
220            f,
221            "missing dependency `{}` in Deps container",
222            self.type_name
223        )
224    }
225}
226
227impl std::error::Error for MissingDep {}
228
229#[cold]
230#[inline(never)]
231#[allow(clippy::panic)]
232fn missing_panic(type_name: &'static str) -> ! {
233    panic!(
234        "Deps::expect: missing dependency `{type_name}` (verify_deps should have caught this at workflow build time)"
235    )
236}
237
238/// A [`RegisterableTask`] whose dependencies can be resolved from a [`Deps`]
239/// container.
240///
241/// Implemented automatically by the `#[task]` proc-macro. Drives the
242/// `workflow! { deps: … }` expansion and
243/// [`TaskRegistry::register_from_deps`](crate::registry::TaskRegistry::register_from_deps)
244/// when they need to construct task instances generically.
245pub trait DepsInjectable: RegisterableTask
246where
247    Self::Input: Send + 'static,
248    Self::Output: Send + 'static,
249    Self::Future: Send + 'static,
250{
251    /// Build an instance by resolving every `#[inject]` parameter from
252    /// `deps`. Panics on miss — call [`Self::verify_deps`] first when the
253    /// container's contents are not statically known.
254    fn from_deps(deps: &Deps) -> Self;
255
256    /// Return one [`MissingDep`] per `#[inject]` type that is absent from
257    /// `deps`. Empty slice means [`Self::from_deps`] will not panic.
258    fn verify_deps(deps: &Deps) -> ::std::vec::Vec<MissingDep>;
259}
260
261#[cfg(test)]
262#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
263mod tests {
264    use super::*;
265    use std::sync::Arc;
266
267    #[derive(Clone, Debug, PartialEq, Eq)]
268    struct ServiceA(u32);
269
270    #[derive(Clone, Debug, PartialEq, Eq)]
271    struct ServiceB(&'static str);
272
273    #[test]
274    fn insert_and_get_concrete() {
275        let deps = Deps::builder().insert(ServiceA(7)).build();
276        assert_eq!(deps.get::<ServiceA>(), Some(ServiceA(7)));
277    }
278
279    #[test]
280    fn insert_arc_keeps_arc_key() {
281        let deps = Deps::builder().insert(Arc::new(ServiceA(7))).build();
282        assert!(deps.contains::<Arc<ServiceA>>());
283        assert!(!deps.contains::<ServiceA>());
284        let resolved: Arc<ServiceA> = deps.expect();
285        assert_eq!(*resolved, ServiceA(7));
286    }
287
288    #[test]
289    fn multiple_types_coexist() {
290        let deps = Deps::builder()
291            .insert(ServiceA(1))
292            .insert(ServiceB("hi"))
293            .build();
294        assert_eq!(deps.len(), 2);
295        assert_eq!(deps.get::<ServiceA>(), Some(ServiceA(1)));
296        assert_eq!(deps.get::<ServiceB>(), Some(ServiceB("hi")));
297    }
298
299    #[test]
300    fn missing_type_returns_none() {
301        let deps = Deps::new();
302        assert!(deps.get::<ServiceA>().is_none());
303    }
304
305    #[test]
306    fn try_get_reports_type_name() {
307        let deps = Deps::new();
308        let err = deps.try_get::<ServiceA>().unwrap_err();
309        assert!(err.type_name.contains("ServiceA"));
310    }
311
312    #[test]
313    fn last_insert_wins_for_same_type() {
314        let deps = Deps::builder()
315            .insert(ServiceA(1))
316            .insert(ServiceA(2))
317            .build();
318        assert_eq!(deps.get::<ServiceA>(), Some(ServiceA(2)));
319    }
320
321    #[test]
322    fn expect_returns_value() {
323        let deps = Deps::builder().insert(ServiceA(42)).build();
324        let value: ServiceA = deps.expect();
325        assert_eq!(value, ServiceA(42));
326    }
327
328    #[test]
329    #[should_panic(expected = "missing dependency")]
330    fn expect_panics_with_message() {
331        let deps = Deps::new();
332        let _: ServiceA = deps.expect();
333    }
334
335    #[test]
336    fn missing_dep_display() {
337        let m = MissingDep::of::<ServiceA>();
338        let rendered = format!("{m}");
339        assert!(rendered.contains("ServiceA"));
340        assert!(rendered.contains("missing dependency"));
341    }
342
343    #[test]
344    fn empty_and_len() {
345        let mut deps = Deps::new();
346        assert!(deps.is_empty());
347        deps = Deps::builder().insert(ServiceA(0)).build();
348        assert!(!deps.is_empty());
349        assert_eq!(deps.len(), 1);
350    }
351
352    #[test]
353    fn merge_non_overlapping() {
354        let mut base = Deps::builder().insert(ServiceA(1)).build();
355        let extra = Deps::builder().insert(ServiceB("x")).build();
356        base.merge(extra);
357
358        assert_eq!(base.len(), 2);
359        assert_eq!(base.get::<ServiceA>(), Some(ServiceA(1)));
360        assert_eq!(base.get::<ServiceB>(), Some(ServiceB("x")));
361    }
362
363    #[test]
364    fn merge_overlap_other_wins() {
365        let mut base = Deps::builder().insert(ServiceA(1)).build();
366        let extra = Deps::builder().insert(ServiceA(99)).build();
367        base.merge(extra);
368
369        assert_eq!(base.len(), 1);
370        assert_eq!(base.get::<ServiceA>(), Some(ServiceA(99)));
371    }
372
373    #[test]
374    fn merge_empty_into_populated() {
375        let mut base = Deps::builder().insert(ServiceA(1)).build();
376        base.merge(Deps::new());
377        assert_eq!(base.len(), 1);
378        assert_eq!(base.get::<ServiceA>(), Some(ServiceA(1)));
379    }
380
381    #[test]
382    fn merge_populated_into_empty() {
383        let mut base = Deps::new();
384        let extra = Deps::builder().insert(ServiceA(7)).build();
385        base.merge(extra);
386        assert_eq!(base.len(), 1);
387        assert_eq!(base.get::<ServiceA>(), Some(ServiceA(7)));
388    }
389
390    #[test]
391    fn builder_merge_layers_containers() {
392        let library = Deps::builder().insert(ServiceA(1)).build();
393        let combined = Deps::builder()
394            .insert(ServiceB("local"))
395            .merge(library)
396            .build();
397
398        assert_eq!(combined.len(), 2);
399        assert_eq!(combined.get::<ServiceA>(), Some(ServiceA(1)));
400        assert_eq!(combined.get::<ServiceB>(), Some(ServiceB("local")));
401    }
402
403    #[test]
404    fn builder_merge_other_wins_on_overlap() {
405        let library = Deps::builder().insert(ServiceA(2)).build();
406        let combined = Deps::builder().insert(ServiceA(1)).merge(library).build();
407
408        assert_eq!(combined.get::<ServiceA>(), Some(ServiceA(2)));
409    }
410}