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(&registry_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}