use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
use super::component::{AnyComponent, ComponentContext};
#[derive(Debug, Error)]
pub enum FactoryError {
#[error("unknown component kind: {0}")]
UnknownKind(String),
#[error("invalid config for kind {kind}: {source}")]
InvalidConfig {
kind: String,
source: serde_json::Error,
},
#[error("factory for kind {0} failed: {1}")]
FactoryFailed(String, String),
}
pub trait FactoryInvoker: Send + Sync {
fn invoke(
&self,
config: Value,
ctx: ComponentContext,
) -> Result<Box<dyn AnyComponent>, FactoryError>;
}
impl<F> FactoryInvoker for F
where
F: Fn(Value, ComponentContext) -> Result<Box<dyn AnyComponent>, FactoryError>
+ Send
+ Sync
+ 'static,
{
fn invoke(
&self,
config: Value,
ctx: ComponentContext,
) -> Result<Box<dyn AnyComponent>, FactoryError> {
(self)(config, ctx)
}
}
pub type FactoryFn = Arc<
dyn Fn(Value, ComponentContext) -> Result<Box<dyn AnyComponent>, FactoryError> + Send + Sync,
>;
pub struct FactoryRegistry {
by_kind: HashMap<&'static str, Arc<dyn FactoryInvoker>>,
}
impl Default for FactoryRegistry {
fn default() -> Self {
Self::new()
}
}
impl FactoryRegistry {
#[must_use]
pub fn new() -> Self {
Self {
by_kind: HashMap::new(),
}
}
#[must_use]
pub fn register(mut self, kind: &'static str, invoker: impl FactoryInvoker + 'static) -> Self {
self.by_kind.insert(kind, Arc::new(invoker));
self
}
#[must_use]
pub fn contains(&self, kind: &str) -> bool {
self.by_kind.contains_key(kind)
}
pub fn kinds(&self) -> impl Iterator<Item = &'static str> + '_ {
self.by_kind.keys().copied()
}
pub fn invoke(
&self,
kind: &str,
cfg: Value,
ctx: &ComponentContext,
) -> Result<Box<dyn AnyComponent>, FactoryError> {
let invoker = self
.by_kind
.get(kind)
.ok_or_else(|| FactoryError::UnknownKind(kind.to_owned()))?;
let ctx = ctx.clone();
invoker.invoke(cfg, ctx)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, dead_code)]
use super::*;
use crate::component::ComponentContext;
use crate::lifecycle::ShutdownToken;
use crate::registry::TypedAnyComponent;
use async_trait::async_trait;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Deserialize, JsonSchema)]
struct DummyCfg {
v: u32,
}
struct DummyComp {
v: u32,
}
#[async_trait]
impl crate::component::Component for DummyComp {
const NAME: &'static str = "dummy";
type Config = DummyCfg;
type Error = std::io::Error;
async fn init(
cfg: &Self::Config,
_ctx: &ComponentContext,
) -> Result<Self, <Self as crate::component::Component>::Error> {
Ok(Self { v: cfg.v })
}
async fn start(&self) -> Result<(), <Self as crate::component::Component>::Error> {
Ok(())
}
async fn stop(&self) -> Result<(), <Self as crate::component::Component>::Error> {
Ok(())
}
async fn health(&self) -> behest_core::health::HealthStatus {
behest_core::health::HealthStatus::healthy()
}
}
#[test]
fn registry_invokes_known_kind() {
let reg =
FactoryRegistry::new().register("test.dummy", |cfg: Value, _ctx: ComponentContext| {
let v: DummyCfg =
serde_json::from_value(cfg).map_err(|e| FactoryError::InvalidConfig {
kind: "test.dummy".to_owned(),
source: e,
})?;
Ok(Box::new(TypedAnyComponent::new(DummyComp { v: v.v })) as Box<dyn AnyComponent>)
});
let ctx = ComponentContext::new(ShutdownToken::new());
let comp = reg
.invoke("test.dummy", serde_json::json!({ "v": 42 }), &ctx)
.expect("invoke should succeed");
assert_eq!(comp.name(), "dummy");
}
#[test]
fn registry_rejects_unknown_kind() {
let reg = FactoryRegistry::new();
let ctx = ComponentContext::new(ShutdownToken::new());
let result = reg.invoke("nope", serde_json::json!({}), &ctx);
assert!(result.is_err());
assert!(matches!(result, Err(FactoryError::UnknownKind(_))));
}
}