Skip to main content

behest_runtime/
factory_registry.rs

1//! Factory registry for runtime components.
2//!
3//! Maps `kind` strings (e.g. `"provider.openai"`, `"store.session.memory"`)
4//! to factory invokers that deserialize a JSON config and produce a
5//! [`Box<dyn AnyComponent>`](super::component::AnyComponent).
6
7use serde_json::Value;
8use std::collections::HashMap;
9use std::sync::Arc;
10use thiserror::Error;
11
12use super::component::{AnyComponent, ComponentContext};
13
14/// Errors from factory creation or registry operations.
15#[derive(Debug, Error)]
16pub enum FactoryError {
17    /// No factory registered for the given kind.
18    #[error("unknown component kind: {0}")]
19    UnknownKind(String),
20
21    /// Config deserialization failed for the matched factory.
22    #[error("invalid config for kind {kind}: {source}")]
23    InvalidConfig {
24        /// The component kind that failed to deserialize.
25        kind: String,
26        /// The underlying deserialization error.
27        source: serde_json::Error,
28    },
29
30    /// Factory implementation returned an error.
31    #[error("factory for kind {0} failed: {1}")]
32    FactoryFailed(String, String),
33}
34
35/// A factory invocable — given a JSON config and a [`ComponentContext`],
36/// produces a [`Box<dyn AnyComponent>`].
37pub trait FactoryInvoker: Send + Sync {
38    /// Create a component from the given JSON config.
39    ///
40    /// # Errors
41    ///
42    /// Returns [`FactoryError::InvalidConfig`] when deserialization fails,
43    /// or [`FactoryError::FactoryFailed`] when construction fails.
44    fn invoke(
45        &self,
46        config: Value,
47        ctx: ComponentContext,
48    ) -> Result<Box<dyn AnyComponent>, FactoryError>;
49}
50
51/// Blanket implementation so that any matching closure can be used as a
52/// [`FactoryInvoker`] directly.
53impl<F> FactoryInvoker for F
54where
55    F: Fn(Value, ComponentContext) -> Result<Box<dyn AnyComponent>, FactoryError>
56        + Send
57        + Sync
58        + 'static,
59{
60    fn invoke(
61        &self,
62        config: Value,
63        ctx: ComponentContext,
64    ) -> Result<Box<dyn AnyComponent>, FactoryError> {
65        (self)(config, ctx)
66    }
67}
68
69/// Thread-safe factory function pointer — an [`Arc`] around a boxed closure.
70///
71/// This type alias is provided so that downstream code (e.g. Task 7's
72/// provider/store adapters) can store and pass factory functions easily.
73pub type FactoryFn = Arc<
74    dyn Fn(Value, ComponentContext) -> Result<Box<dyn AnyComponent>, FactoryError> + Send + Sync,
75>;
76
77/// A registry that maps `kind` strings (e.g. `"provider.openai"`) to
78/// [`FactoryInvoker`] instances.
79///
80/// Invoking an unknown kind returns [`FactoryError::UnknownKind`]:
81///
82/// ```
83/// # use behest_runtime::factory_registry::{FactoryError, FactoryRegistry};
84/// # use behest_runtime::component::ComponentContext;
85/// # use behest_runtime::lifecycle::ShutdownToken;
86/// let reg = FactoryRegistry::new();
87/// let ctx = ComponentContext::new(ShutdownToken::new());
88/// let result = reg.invoke("nope", serde_json::json!({}), &ctx);
89/// assert!(result.is_err());
90/// assert!(matches!(result, Err(FactoryError::UnknownKind(_))));
91/// ```
92pub struct FactoryRegistry {
93    by_kind: HashMap<&'static str, Arc<dyn FactoryInvoker>>,
94}
95
96impl Default for FactoryRegistry {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102impl FactoryRegistry {
103    /// Creates an empty registry.
104    #[must_use]
105    pub fn new() -> Self {
106        Self {
107            by_kind: HashMap::new(),
108        }
109    }
110
111    /// Registers a factory invoker under the given `kind`.
112    ///
113    /// Returns `self` for chaining.
114    #[must_use]
115    pub fn register(mut self, kind: &'static str, invoker: impl FactoryInvoker + 'static) -> Self {
116        self.by_kind.insert(kind, Arc::new(invoker));
117        self
118    }
119
120    /// Returns `true` if a factory is registered for `kind`.
121    #[must_use]
122    pub fn contains(&self, kind: &str) -> bool {
123        self.by_kind.contains_key(kind)
124    }
125
126    /// Iterates over all registered kind strings.
127    pub fn kinds(&self) -> impl Iterator<Item = &'static str> + '_ {
128        self.by_kind.keys().copied()
129    }
130
131    /// Invokes the factory registered for `kind` with the given JSON config.
132    ///
133    /// # Errors
134    ///
135    /// Returns [`FactoryError::UnknownKind`] when no factory is registered,
136    /// or the factory's own error on failure.
137    pub fn invoke(
138        &self,
139        kind: &str,
140        cfg: Value,
141        ctx: &ComponentContext,
142    ) -> Result<Box<dyn AnyComponent>, FactoryError> {
143        let invoker = self
144            .by_kind
145            .get(kind)
146            .ok_or_else(|| FactoryError::UnknownKind(kind.to_owned()))?;
147        let ctx = ctx.clone();
148        invoker.invoke(cfg, ctx)
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    #![allow(clippy::expect_used, dead_code)]
155    use super::*;
156    use crate::component::ComponentContext;
157    use crate::lifecycle::ShutdownToken;
158    use crate::registry::TypedAnyComponent;
159    use async_trait::async_trait;
160    use schemars::JsonSchema;
161    use serde::Deserialize;
162
163    #[derive(Debug, Deserialize, JsonSchema)]
164    struct DummyCfg {
165        v: u32,
166    }
167
168    struct DummyComp {
169        v: u32,
170    }
171
172    #[async_trait]
173    impl crate::component::Component for DummyComp {
174        const NAME: &'static str = "dummy";
175        type Config = DummyCfg;
176        type Error = std::io::Error;
177
178        async fn init(
179            cfg: &Self::Config,
180            _ctx: &ComponentContext,
181        ) -> Result<Self, <Self as crate::component::Component>::Error> {
182            Ok(Self { v: cfg.v })
183        }
184
185        async fn start(&self) -> Result<(), <Self as crate::component::Component>::Error> {
186            Ok(())
187        }
188
189        async fn stop(&self) -> Result<(), <Self as crate::component::Component>::Error> {
190            Ok(())
191        }
192
193        async fn health(&self) -> behest_core::health::HealthStatus {
194            behest_core::health::HealthStatus::healthy()
195        }
196    }
197
198    #[test]
199    fn registry_invokes_known_kind() {
200        let reg =
201            FactoryRegistry::new().register("test.dummy", |cfg: Value, _ctx: ComponentContext| {
202                let v: DummyCfg =
203                    serde_json::from_value(cfg).map_err(|e| FactoryError::InvalidConfig {
204                        kind: "test.dummy".to_owned(),
205                        source: e,
206                    })?;
207                Ok(Box::new(TypedAnyComponent::new(DummyComp { v: v.v })) as Box<dyn AnyComponent>)
208            });
209        let ctx = ComponentContext::new(ShutdownToken::new());
210        let comp = reg
211            .invoke("test.dummy", serde_json::json!({ "v": 42 }), &ctx)
212            .expect("invoke should succeed");
213        assert_eq!(comp.name(), "dummy");
214    }
215
216    #[test]
217    fn registry_rejects_unknown_kind() {
218        let reg = FactoryRegistry::new();
219        let ctx = ComponentContext::new(ShutdownToken::new());
220        let result = reg.invoke("nope", serde_json::json!({}), &ctx);
221        assert!(result.is_err());
222        assert!(matches!(result, Err(FactoryError::UnknownKind(_))));
223    }
224}