use std::sync::OnceLock;
use dashmap::DashMap;
use crate::step::StepRegistration;
pub type StepBuilderFn = fn() -> StepRegistration;
pub struct StepDeserializerRegistry {
inner: DashMap<&'static str, StepBuilderFn>,
}
impl StepDeserializerRegistry {
#[must_use]
fn new() -> Self {
Self {
inner: DashMap::new(),
}
}
pub fn register(&self, step_id: &'static str, builder: StepBuilderFn) {
let entry = self.inner.entry(step_id).or_insert(builder);
debug_assert!(
std::ptr::fn_addr_eq(*entry.value(), builder),
"step ID collision: `{step_id}` already registered with a different builder"
);
}
#[must_use]
pub fn lookup(&self, step_id: &str) -> Option<StepRegistration> {
self.inner.get(step_id).map(|builder| (*builder)())
}
#[must_use]
pub fn step_ids(&self) -> Vec<&'static str> {
self.inner.iter().map(|entry| *entry.key()).collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl std::fmt::Debug for StepDeserializerRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StepDeserializerRegistry")
.field("len", &self.inner.len())
.finish()
}
}
fn global_registry() -> &'static StepDeserializerRegistry {
static REGISTRY: OnceLock<StepDeserializerRegistry> = OnceLock::new();
REGISTRY.get_or_init(StepDeserializerRegistry::new)
}
pub fn register_step_builder(step_id: &'static str, builder: StepBuilderFn) {
global_registry().register(step_id, builder);
}
#[must_use]
pub fn lookup_step_builder(step_id: &str) -> Option<StepRegistration> {
global_registry().lookup(step_id)
}
#[must_use]
pub fn registered_step_ids() -> Vec<&'static str> {
global_registry().step_ids()
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use blazen_events::{Event, StartEvent, StopEvent};
use crate::step::{StepFn, StepOutput, StepRegistration};
use super::{
StepDeserializerRegistry, lookup_step_builder, register_step_builder, registered_step_ids,
};
fn make_echo_step(name: &'static str) -> StepRegistration {
let handler: StepFn = Arc::new(|event, _ctx| {
Box::pin(async move {
let start = event
.as_any()
.downcast_ref::<StartEvent>()
.expect("expected StartEvent");
let stop = StopEvent {
result: start.data.clone(),
};
Ok(StepOutput::Single(Box::new(stop)))
})
});
StepRegistration {
name: name.into(),
accepts: vec![StartEvent::event_type()],
emits: vec![StopEvent::event_type()],
handler,
max_concurrency: 0,
}
}
fn echo_step_a() -> StepRegistration {
make_echo_step("step_a")
}
fn echo_step_b() -> StepRegistration {
make_echo_step("step_b")
}
#[test]
fn register_and_lookup_single_step() {
let registry = StepDeserializerRegistry::new();
registry.register("test::step_a", echo_step_a);
let looked_up = registry
.lookup("test::step_a")
.expect("registered step must resolve");
assert_eq!(looked_up.name, "step_a");
assert_eq!(looked_up.accepts, vec![StartEvent::event_type()]);
}
#[test]
fn lookup_unknown_step_returns_none() {
let registry = StepDeserializerRegistry::new();
assert!(registry.lookup("test::does_not_exist").is_none());
}
#[test]
fn register_same_builder_twice_is_idempotent() {
let registry = StepDeserializerRegistry::new();
registry.register("test::step_a", echo_step_a);
registry.register("test::step_a", echo_step_a);
assert_eq!(registry.len(), 1);
}
#[test]
fn step_ids_lists_all_registered() {
let registry = StepDeserializerRegistry::new();
registry.register("test::step_a", echo_step_a);
registry.register("test::step_b", echo_step_b);
let mut ids = registry.step_ids();
ids.sort_unstable();
assert_eq!(ids, vec!["test::step_a", "test::step_b"]);
}
#[test]
fn debug_impl_reports_len() {
let registry = StepDeserializerRegistry::new();
registry.register("test::step_a", echo_step_a);
let s = format!("{registry:?}");
assert!(s.contains("len: 1"));
}
#[test]
fn global_register_and_lookup() {
register_step_builder("blazen_core::step_registry::tests::global_a", echo_step_a);
let looked_up = lookup_step_builder("blazen_core::step_registry::tests::global_a")
.expect("global registration must resolve");
assert_eq!(looked_up.name, "step_a");
let ids = registered_step_ids();
assert!(
ids.contains(&"blazen_core::step_registry::tests::global_a"),
"registered_step_ids() must include globally-registered IDs"
);
}
}