Skip to main content

allora_runtime/dsl/
pattern_registry.rs

1//! [`PatternRegistry`]: runtime resolver from YAML-declared names to consumer-supplied
2//! impls of Allora's pattern traits (`CompletionCondition`, `AggregationStrategy`,
3//! `GroupStore`).
4//!
5//! # Motivation
6//!
7//! The chain's quorum logic (validator registry lookups, dynamic threshold over a
8//! changing validator set, etc.) cannot be expressed in pure YAML — it needs Rust
9//! code. The same is true for any non-trivial completion condition, custom
10//! aggregation, or alternative storage backend. The
11//! [`crate::spec::AggregatorSpec`] therefore refers to *names* — and those names
12//! resolve here at build time. Mirrors how `processor_registry` works downstream
13//! (Fialucci chain) for HTTP-route processor lookup.
14//!
15//! # Default Registrations
16//!
17//! [`PatternRegistry::with_defaults`] pre-populates Allora's three built-in
18//! strategies, keyed by `allora.<name>`:
19//!
20//! * `allora.concat_text` → [`allora_core::patterns::aggregator::ConcatText`]
21//! * `allora.json_array`  → [`allora_core::patterns::aggregator::JsonArray`]
22//! * `allora.emit_signal` → [`allora_core::patterns::aggregator::EmitSignal`]
23//!
24//! No default *completions* or *stores* are registered — those are inherently
25//! consumer-specific (the chain registers `chain.validator_quorum`, etc.).
26//!
27//! # Example
28//! ```rust,no_run
29//! use std::sync::Arc;
30//! use allora_core::Message;
31//! use allora_core::patterns::aggregator::CompletionCondition;
32//! use allora_runtime::dsl::PatternRegistry;
33//!
34//! struct MyQuorum;
35//! impl CompletionCondition for MyQuorum {
36//!     fn is_complete(&self, group: &[Message], _: std::time::Instant) -> bool {
37//!         group.len() >= 2
38//!     }
39//! }
40//!
41//! let mut registry = PatternRegistry::with_defaults();
42//! registry.register_completion("chain.my_quorum", Arc::new(MyQuorum));
43//! assert!(registry.completion("chain.my_quorum").is_some());
44//! assert!(registry.strategy("allora.emit_signal").is_some());
45//! ```
46
47use std::collections::HashMap;
48use std::sync::Arc;
49
50use allora_core::patterns::aggregator::{
51    AggregationStrategy, CompletionCondition, ConcatText, EmitSignal, GroupStore, JsonArray,
52};
53
54/// Registry name for Allora's built-in [`ConcatText`] strategy.
55pub const STRATEGY_CONCAT_TEXT: &str = "allora.concat_text";
56/// Registry name for Allora's built-in [`JsonArray`] strategy.
57pub const STRATEGY_JSON_ARRAY: &str = "allora.json_array";
58/// Registry name for Allora's built-in [`EmitSignal`] strategy.
59pub const STRATEGY_EMIT_SIGNAL: &str = "allora.emit_signal";
60
61/// Resolver from YAML names → concrete pattern-component impls.
62#[derive(Default, Clone)]
63pub struct PatternRegistry {
64    completions: HashMap<String, Arc<dyn CompletionCondition>>,
65    strategies: HashMap<String, Arc<dyn AggregationStrategy>>,
66    stores: HashMap<String, Arc<dyn GroupStore>>,
67}
68
69impl PatternRegistry {
70    /// Empty registry — no defaults. Caller registers every impl explicitly.
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Registry pre-populated with Allora's built-in strategies:
76    /// `allora.concat_text`, `allora.json_array`, `allora.emit_signal`.
77    pub fn with_defaults() -> Self {
78        let mut r = Self::default();
79        r.register_strategy(STRATEGY_CONCAT_TEXT, Arc::new(ConcatText));
80        r.register_strategy(STRATEGY_JSON_ARRAY, Arc::new(JsonArray));
81        r.register_strategy(STRATEGY_EMIT_SIGNAL, Arc::new(EmitSignal));
82        r
83    }
84
85    // ── Registration ─────────────────────────────────────────────────────────
86
87    pub fn register_completion<N: Into<String>>(
88        &mut self,
89        name: N,
90        impl_: Arc<dyn CompletionCondition>,
91    ) {
92        self.completions.insert(name.into(), impl_);
93    }
94
95    pub fn register_strategy<N: Into<String>>(
96        &mut self,
97        name: N,
98        impl_: Arc<dyn AggregationStrategy>,
99    ) {
100        self.strategies.insert(name.into(), impl_);
101    }
102
103    pub fn register_store<N: Into<String>>(&mut self, name: N, impl_: Arc<dyn GroupStore>) {
104        self.stores.insert(name.into(), impl_);
105    }
106
107    // ── Lookup ───────────────────────────────────────────────────────────────
108
109    pub fn completion(&self, name: &str) -> Option<Arc<dyn CompletionCondition>> {
110        self.completions.get(name).cloned()
111    }
112
113    pub fn strategy(&self, name: &str) -> Option<Arc<dyn AggregationStrategy>> {
114        self.strategies.get(name).cloned()
115    }
116
117    pub fn store(&self, name: &str) -> Option<Arc<dyn GroupStore>> {
118        self.stores.get(name).cloned()
119    }
120
121    // ── Diagnostics ──────────────────────────────────────────────────────────
122
123    pub fn completion_names(&self) -> Vec<&str> {
124        self.completions.keys().map(|s| s.as_str()).collect()
125    }
126    pub fn strategy_names(&self) -> Vec<&str> {
127        self.strategies.keys().map(|s| s.as_str()).collect()
128    }
129    pub fn store_names(&self) -> Vec<&str> {
130        self.stores.keys().map(|s| s.as_str()).collect()
131    }
132}
133
134// ---------------------------------------------------------------------------
135// Tests
136// ---------------------------------------------------------------------------
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use allora_core::Message;
142    use std::time::Instant;
143
144    struct CountAtLeast(usize);
145    impl CompletionCondition for CountAtLeast {
146        fn is_complete(&self, group: &[Message], _: Instant) -> bool {
147            group.len() >= self.0
148        }
149    }
150
151    #[test]
152    fn empty_registry_returns_none_for_everything() {
153        let r = PatternRegistry::new();
154        assert!(r.completion("anything").is_none());
155        assert!(r.strategy("anything").is_none());
156        assert!(r.store("anything").is_none());
157        assert!(r.completion_names().is_empty());
158    }
159
160    #[test]
161    fn with_defaults_registers_all_three_built_in_strategies() {
162        let r = PatternRegistry::with_defaults();
163        assert!(r.strategy(STRATEGY_CONCAT_TEXT).is_some());
164        assert!(r.strategy(STRATEGY_JSON_ARRAY).is_some());
165        assert!(r.strategy(STRATEGY_EMIT_SIGNAL).is_some());
166        // No default completions / stores.
167        assert!(r.completion_names().is_empty());
168        assert!(r.store_names().is_empty());
169    }
170
171    #[test]
172    fn register_then_lookup_completion() {
173        let mut r = PatternRegistry::new();
174        r.register_completion("chain.test_quorum", Arc::new(CountAtLeast(3)));
175        let c = r.completion("chain.test_quorum").expect("registered");
176        let g: Vec<Message> = (0..3).map(|_| Message::default()).collect();
177        assert!(c.is_complete(&g, Instant::now()));
178        let g2: Vec<Message> = (0..2).map(|_| Message::default()).collect();
179        assert!(!c.is_complete(&g2, Instant::now()));
180    }
181
182    #[test]
183    fn registration_overwrites_existing_entry() {
184        let mut r = PatternRegistry::new();
185        r.register_completion("k", Arc::new(CountAtLeast(1)));
186        r.register_completion("k", Arc::new(CountAtLeast(99)));
187        let c = r.completion("k").unwrap();
188        let g: Vec<Message> = (0..5).map(|_| Message::default()).collect();
189        assert!(!c.is_complete(&g, Instant::now())); // second registration wins, threshold 99
190    }
191}