behest_runtime/
factory_registry.rs1use serde_json::Value;
8use std::collections::HashMap;
9use std::sync::Arc;
10use thiserror::Error;
11
12use super::component::{AnyComponent, ComponentContext};
13
14#[derive(Debug, Error)]
16pub enum FactoryError {
17 #[error("unknown component kind: {0}")]
19 UnknownKind(String),
20
21 #[error("invalid config for kind {kind}: {source}")]
23 InvalidConfig {
24 kind: String,
26 source: serde_json::Error,
28 },
29
30 #[error("factory for kind {0} failed: {1}")]
32 FactoryFailed(String, String),
33}
34
35pub trait FactoryInvoker: Send + Sync {
38 fn invoke(
45 &self,
46 config: Value,
47 ctx: ComponentContext,
48 ) -> Result<Box<dyn AnyComponent>, FactoryError>;
49}
50
51impl<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
69pub type FactoryFn = Arc<
74 dyn Fn(Value, ComponentContext) -> Result<Box<dyn AnyComponent>, FactoryError> + Send + Sync,
75>;
76
77pub 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 #[must_use]
105 pub fn new() -> Self {
106 Self {
107 by_kind: HashMap::new(),
108 }
109 }
110
111 #[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 #[must_use]
122 pub fn contains(&self, kind: &str) -> bool {
123 self.by_kind.contains_key(kind)
124 }
125
126 pub fn kinds(&self) -> impl Iterator<Item = &'static str> + '_ {
128 self.by_kind.keys().copied()
129 }
130
131 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}