bothan_lib/registry.rs
1//! Registry for asset signals and their computation dependencies.
2//!
3//! This module provides a registry system for managing asset signals and their computational
4//! relationships. It defines data structures and validation logic to ensure that signal
5//! computations can be performed correctly.
6//!
7//! The module provides:
8//!
9//! - The [`Registry`] struct which stores signal definitions and their relationships
10//! - Type-level states (`Valid` and `Invalid`) to represent validation status
11//! - Validation logic to ensure the registry is consistent and free of cycles
12//! - Serialization and deserialization support for registry persistence
13//!
14//! # Registry Structure
15//!
16//! The registry is a collection of [`Signal`]s, each identified by a unique ID. Signals can
17//! depend on other signals for their computation, forming a directed graph. The validation
18//! process ensures this graph is acyclic and all dependencies exist.
19//!
20//! # Module Organization
21//!
22//! The registry module is organized into several submodules:
23//!
24//! - [`signal`] - Defines the structure of signals and their components
25//! - [`source`] - Defines data sources for signals
26//! - [`processor`] - Defines how to process source data into a signal value
27//! - [`post_processor`] - Defines transformations applied after initial processing
28//!
29//! # Type States
30//!
31//! The registry uses type states to distinguish between validated and unvalidated registries:
32//!
33//! - [`Registry<Invalid>`] - A registry that has not been validated or has failed validation
34//! - [`Registry<Valid>`] - A registry that has been successfully validated and is cycle free
35//!
36//! This pattern ensures at compile time that only valid registries can be used in contexts
37//! where a valid registry is required.
38
39use std::collections::HashMap;
40use std::marker::PhantomData;
41
42use bincode::de::Decoder;
43use bincode::enc::Encoder;
44use bincode::error::{DecodeError, EncodeError};
45use bincode::{Decode, Encode};
46use serde::{Deserialize, Serialize};
47
48use crate::registry::signal::Signal;
49pub use crate::registry::validate::ValidationError;
50use crate::registry::validate::validate_signal;
51
52pub mod post_processor;
53pub mod processor;
54pub mod signal;
55pub mod source;
56pub(crate) mod validate;
57
58/// Marker type representing an unvalidated registry state.
59///
60/// This type is used as a type parameter for [`Registry`] to indicate that
61/// the registry has not been validated or has failed validation. A registry
62/// in the `Invalid` state cannot be used for certain operations that require
63/// a valid registry.
64#[derive(Clone, Debug, PartialEq)]
65pub struct Invalid;
66
67/// Marker type representing a validated registry state.
68///
69/// This type is used as a type parameter for [`Registry`] to indicate that
70/// the registry has been successfully validated. A registry in the `Valid` state
71/// is guaranteed to have consistent signal dependencies and no cycles.
72#[derive(Clone, Debug, PartialEq)]
73pub struct Valid;
74
75/// A collection of signals with their computational dependencies.
76///
77/// The `Registry` struct serves as the central data structure for managing asset signals.
78/// It stores signal definitions indexed by their unique IDs and provides methods for
79/// accessing and validating them.
80///
81/// The `State` type parameter indicates whether the registry has been validated:
82/// - [`Registry<Invalid>`] - A registry that has not been validated or has failed validation
83/// - [`Registry<Valid>`] - A registry that has been successfully validated
84///
85/// # Examples
86///
87/// Creating and validating a registry:
88///
89/// ```
90/// use bothan_lib::registry::{Registry, Invalid, Valid};
91/// use serde_json::json;
92///
93/// // Create a registry from JSON
94/// let json_data = json!({
95/// "BTC-USD": {
96/// "sources": [
97/// {
98/// "source_id": "binance",
99/// "id": "btcusdt",
100/// "routes": []
101/// }
102/// ],
103/// "processor": {
104/// "function": "median",
105/// "params": {
106/// "min_source_count": 1
107/// }
108/// },
109/// "post_processors": []
110/// }
111/// });
112///
113/// let registry_json = serde_json::to_string(&json_data).unwrap();
114/// let registry: Registry<Invalid> = serde_json::from_str(®istry_json).unwrap();
115///
116/// // Validate the registry
117/// let valid_registry: Registry<Valid> = registry.validate().unwrap();
118///
119/// // Use the validated registry
120/// assert!(valid_registry.contains("BTC-USD"));
121/// ```
122#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
123pub struct Registry<State = Invalid> {
124 #[serde(flatten)]
125 inner: HashMap<String, Signal>,
126 #[serde(skip)]
127 _state: PhantomData<State>,
128}
129
130impl Registry<Invalid> {
131 /// Validates the registry, ensuring all dependencies exist and there are no cycles.
132 ///
133 /// This method performs a thorough validation of the registry, checking that:
134 /// - All signal dependencies (referenced by signal IDs) exist in the registry
135 /// - There are no circular dependencies between signals
136 ///
137 /// If validation is successful, the registry is converted to the `Valid` state.
138 /// If validation fails, an error is returned explaining the validation failure.
139 ///
140 /// # Returns
141 ///
142 /// - `Ok(Registry<Valid>)` if validation is successful
143 /// - `Err(ValidationError)` if validation fails
144 ///
145 /// # Examples
146 ///
147 /// ```
148 /// use bothan_lib::registry::{Registry, ValidationError};
149 /// use serde_json::json;
150 ///
151 /// // Create a valid registry
152 /// let valid_json = json!({
153 /// "USDT-USD": {
154 /// "sources": [
155 /// {
156 /// "source_id": "coingecko",
157 /// "id": "tether",
158 /// "routes": []
159 /// }
160 /// ],
161 /// "processor": {
162 /// "function": "median",
163 /// "params": {
164 /// "min_source_count": 1
165 /// }
166 /// },
167 /// "post_processors": []
168 /// }
169 /// });
170 ///
171 /// let valid_registry: Registry = serde_json::from_value(valid_json).unwrap();
172 /// assert!(valid_registry.validate().is_ok());
173 ///
174 /// // Create an invalid registry with a missing dependency
175 /// let invalid_json = json!({
176 /// "BTC-USD": {
177 /// "sources": [
178 /// {
179 /// "source_id": "binance",
180 /// "id": "btcusdt",
181 /// "routes": [
182 /// {
183 /// "signal_id": "USDT-USD", // This dependency doesn't exist
184 /// "operation": "*"
185 /// }
186 /// ]
187 /// }
188 /// ],
189 /// "processor": {
190 /// "function": "median",
191 /// "params": {
192 /// "min_source_count": 1
193 /// }
194 /// },
195 /// "post_processors": []
196 /// }
197 /// });
198 ///
199 /// let invalid_registry: Registry = serde_json::from_value(invalid_json).unwrap();
200 /// assert!(invalid_registry.validate().is_err());
201 /// ```
202 pub fn validate(self) -> Result<Registry<Valid>, ValidationError> {
203 let mut visited = HashMap::new();
204 for root in self.inner.keys() {
205 validate_signal(root, &mut visited, &self)?;
206 }
207
208 Ok(Registry {
209 inner: self.inner,
210 _state: PhantomData,
211 })
212 }
213}
214
215impl<T> Registry<T> {
216 /// Returns the signal for a given signal id.
217 ///
218 /// This method retrieves a reference to the [`Signal`] with the specified ID,
219 /// if it exists in the registry.
220 ///
221 /// # Returns
222 ///
223 /// * `Some(&Signal)` if the signal exists in the registry
224 /// * `None` if the signal does not exist
225 pub fn get(&self, signal_id: &str) -> Option<&Signal> {
226 self.inner.get(signal_id)
227 }
228
229 /// Returns `true` if the registry contains the given signal id.
230 ///
231 /// This method checks whether a signal with the specified ID exists in the registry.
232 ///
233 /// # Returns
234 ///
235 /// * `true` if the signal exists in the registry
236 /// * `false` if the signal does not exist
237 pub fn contains(&self, signal_id: &str) -> bool {
238 self.inner.contains_key(signal_id)
239 }
240
241 /// An iterator visiting all signal ids in the registry in arbitrary order.
242 ///
243 /// This method returns an iterator over all signal IDs in the registry.
244 /// The order of iteration is not specified and may vary between calls.
245 ///
246 /// # Returns
247 ///
248 /// An iterator yielding references to signal IDs
249 pub fn signal_ids(&self) -> impl Iterator<Item = &String> {
250 self.inner.keys()
251 }
252
253 /// An iterator visiting all the signal ids and their signals in the registry in arbitrary order.
254 ///
255 /// This method returns an iterator over all signal IDs and their corresponding signals
256 /// in the registry. The order of iteration is not specified and may vary between calls.
257 ///
258 /// # Returns
259 ///
260 /// An iterator yielding pairs of references to signal IDs and their signals
261 pub fn iter(&self) -> impl Iterator<Item = (&String, &Signal)> {
262 self.inner.iter()
263 }
264}
265
266impl Default for Registry<Valid> {
267 /// Creates a new empty validated registry.
268 ///
269 /// This method creates a default validated registry, which is empty.
270 /// This is useful as a starting point for building a registry programmatically.
271 ///
272 /// # Returns
273 ///
274 /// An empty `Registry<Valid>`
275 fn default() -> Self {
276 Registry {
277 inner: HashMap::new(),
278 _state: PhantomData,
279 }
280 }
281}
282
283impl<State> Encode for Registry<State> {
284 /// Encodes the registry for binary serialization.
285 ///
286 /// This implementation enables efficient binary serialization of the registry
287 /// using the `bincode` crate. It encodes the inner map of signals.
288 ///
289 /// # Errors
290 ///
291 /// Returns an `EncodeError` if encoding fails
292 fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
293 self.inner.encode(encoder)
294 }
295}
296
297impl<Context> Decode<Context> for Registry<Invalid> {
298 /// Decodes a registry from binary serialization.
299 ///
300 /// This implementation enables efficient binary deserialization of the registry
301 /// using the `bincode` crate. It decodes the inner map of signals and creates
302 /// an invalid registry that must be validated before use.
303 ///
304 /// # Errors
305 ///
306 /// Returns a `DecodeError` if decoding fails
307 fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
308 let inner = HashMap::decode(decoder)?;
309 Ok(Registry {
310 inner,
311 _state: PhantomData,
312 })
313 }
314}
315
316#[cfg(test)]
317pub(crate) mod tests {
318 use crate::registry::{Invalid, Registry, ValidationError};
319
320 pub fn valid_mock_registry() -> Registry<Invalid> {
321 let json_string = "{\"CS:USDT-USD\":{\"sources\":[{\"source_id\":\"coingecko\",\"id\":\"tether\",\"routes\":[]}],\"processor\":{\"function\":\"median\",\"params\":{\"min_source_count\":1}},\"post_processors\":[]},\"CS:BTC-USD\":{\"sources\":[{\"source_id\":\"binance\",\"id\":\"btcusdt\",\"routes\":[{\"signal_id\":\"CS:USDT-USD\",\"operation\":\"*\"}]},{\"source_id\":\"coingecko\",\"id\":\"bitcoin\",\"routes\":[]}],\"processor\":{\"function\":\"median\",\"params\":{\"min_source_count\":1}},\"post_processors\":[]}}";
322 serde_json::from_str::<Registry>(json_string).unwrap()
323 }
324
325 pub fn invalid_dependency_mock_registry() -> Registry<Invalid> {
326 let json_string = "{\"CS:BTC-USD\":{\"sources\":[{\"source_id\":\"binance\",\"id\":\"btcusdt\",\"routes\":[{\"signal_id\":\"CS:USDT-USD\",\"operation\":\"*\"}]},{\"source_id\":\"coingecko\",\"id\":\"bitcoin\",\"routes\":[]}],\"processor\":{\"function\":\"median\",\"params\":{\"min_source_count\":1}},\"post_processors\":[]}}";
327 serde_json::from_str::<Registry>(json_string).unwrap()
328 }
329
330 pub fn complete_circular_dependency_mock_registry() -> Registry<Invalid> {
331 let json_string = "{\"CS:USDT-USD\":{\"sources\":[{\"source_id\":\"binance\",\"id\":\"usdtusdc\",\"routes\":[{\"signal_id\":\"CS:USDC-USD\",\"operation\":\"*\"}]}],\"processor\":{\"function\":\"median\",\"params\":{\"min_source_count\":1}},\"post_processors\":[]},\"CS:USDC-USD\":{\"sources\":[{\"source_id\":\"binance\",\"id\":\"usdcusdt\",\"routes\":[{\"signal_id\":\"CS:USDT-USD\",\"operation\":\"*\"}]}],\"processor\":{\"function\":\"median\",\"params\":{\"min_source_count\":1}},\"post_processors\":[]}}";
332 serde_json::from_str::<Registry>(json_string).unwrap()
333 }
334
335 pub fn circular_dependency_mock_registry() -> Registry<Invalid> {
336 let json_string = "{\"CS:USDT-USD\":{\"sources\":[{\"source_id\":\"binance\",\"id\":\"usdtusdc\",\"routes\":[{\"signal_id\":\"CS:USDC-USD\",\"operation\":\"*\"}]}],\"processor\":{\"function\":\"median\",\"params\":{\"min_source_count\":1}},\"post_processors\":[]},\"CS:USDC-USD\":{\"sources\":[{\"source_id\":\"binance\",\"id\":\"usdcdai\",\"routes\":[{\"signal_id\":\"CS:DAI-USD\",\"operation\":\"*\"}]}],\"processor\":{\"function\":\"median\",\"params\":{\"min_source_count\":1}},\"post_processors\":[]},\"CS:DAI-USD\":{\"sources\":[{\"source_id\":\"binance\",\"id\":\"daiusdt\",\"routes\":[{\"signal_id\":\"CS:USDT-USD\",\"operation\":\"*\"}]}],\"processor\":{\"function\":\"median\",\"params\":{\"min_source_count\":1}},\"post_processors\":[]}}";
337 serde_json::from_str::<Registry>(json_string).unwrap()
338 }
339
340 #[test]
341 fn test_registry_validate_valid_registry() {
342 let registry = valid_mock_registry();
343 let valid_registry = registry.validate();
344 assert!(valid_registry.is_ok());
345 }
346
347 #[test]
348 fn test_registry_validate_registry_with_invalid_dependency() {
349 let registry = invalid_dependency_mock_registry();
350 assert_eq!(
351 registry.validate(),
352 Err(ValidationError::InvalidDependency("CS:BTC-USD".to_string()))
353 );
354 }
355
356 #[test]
357 fn test_registry_validate_registry_with_complete_circular_dependency() {
358 let registry = complete_circular_dependency_mock_registry();
359 assert!(matches!(
360 registry.validate(),
361 Err(ValidationError::CycleDetected(_))
362 ))
363 }
364
365 #[test]
366 fn test_registry_validate_registry_with_circular_dependency() {
367 let registry = circular_dependency_mock_registry();
368 assert!(matches!(
369 registry.validate(),
370 Err(ValidationError::CycleDetected(_))
371 ))
372 }
373}