Skip to main content

entelix_runnable/
router.rs

1//! `RunnableRouter` — predicate-based dispatch.
2//!
3//! Routes are tried in registration order; the first matching predicate
4//! wins. An optional default branch handles inputs no predicate accepts.
5//! No match + no default ⇒ `Error::InvalidRequest`.
6
7use std::sync::Arc;
8
9use entelix_core::{Error, ExecutionContext, Result};
10
11use crate::runnable::Runnable;
12
13type Predicate<I> = Arc<dyn Fn(&I) -> bool + Send + Sync>;
14type Branch<I, O> = (Predicate<I>, Arc<dyn Runnable<I, O>>);
15
16/// `Runnable<I, O>` that picks one of several runnables based on a predicate
17/// over the input.
18pub struct RunnableRouter<I, O>
19where
20    I: Send + 'static,
21    O: Send + 'static,
22{
23    routes: Vec<Branch<I, O>>,
24    fallback: Option<Arc<dyn Runnable<I, O>>>,
25}
26
27impl<I, O> RunnableRouter<I, O>
28where
29    I: Send + 'static,
30    O: Send + 'static,
31{
32    /// Empty router with no routes and no default.
33    pub fn new() -> Self {
34        Self {
35            routes: Vec::new(),
36            fallback: None,
37        }
38    }
39
40    /// Register a (predicate, runnable) pair. Routes are evaluated in
41    /// registration order.
42    #[must_use]
43    pub fn route<F, R>(mut self, predicate: F, runnable: R) -> Self
44    where
45        F: Fn(&I) -> bool + Send + Sync + 'static,
46        R: Runnable<I, O> + 'static,
47    {
48        self.routes.push((Arc::new(predicate), Arc::new(runnable)));
49        self
50    }
51
52    /// Set the fallback branch (used when no predicate matches). Calling
53    /// twice replaces the previous default.
54    #[must_use]
55    pub fn fallback<R>(mut self, runnable: R) -> Self
56    where
57        R: Runnable<I, O> + 'static,
58    {
59        self.fallback = Some(Arc::new(runnable));
60        self
61    }
62
63    /// Number of registered routes (excludes the fallback).
64    pub fn len(&self) -> usize {
65        self.routes.len()
66    }
67
68    /// True when no routes are registered (the fallback alone does not
69    /// count).
70    pub fn is_empty(&self) -> bool {
71        self.routes.is_empty()
72    }
73}
74
75impl<I, O> Default for RunnableRouter<I, O>
76where
77    I: Send + 'static,
78    O: Send + 'static,
79{
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85#[async_trait::async_trait]
86impl<I, O> Runnable<I, O> for RunnableRouter<I, O>
87where
88    I: Send + 'static,
89    O: Send + 'static,
90{
91    async fn invoke(&self, input: I, ctx: &ExecutionContext) -> Result<O> {
92        for (predicate, runnable) in &self.routes {
93            if predicate(&input) {
94                return runnable.invoke(input, ctx).await;
95            }
96        }
97        if let Some(fallback) = &self.fallback {
98            return fallback.invoke(input, ctx).await;
99        }
100        Err(Error::invalid_request(
101            "RunnableRouter: no route matched and no fallback was set",
102        ))
103    }
104}