bothan-lib 0.0.1

Library contain base functionality and types for Bothan
Documentation
//! Registry for asset signals and their computation dependencies.
//!
//! This module provides a registry system for managing asset signals and their computational
//! relationships. It defines data structures and validation logic to ensure that signal
//! computations can be performed correctly.
//!
//! The module provides:
//!
//! - The [`Registry`] struct which stores signal definitions and their relationships
//! - Type-level states (`Valid` and `Invalid`) to represent validation status
//! - Validation logic to ensure the registry is consistent and free of cycles
//! - Serialization and deserialization support for registry persistence
//!
//! # Registry Structure
//!
//! The registry is a collection of [`Signal`]s, each identified by a unique ID. Signals can
//! depend on other signals for their computation, forming a directed graph. The validation
//! process ensures this graph is acyclic and all dependencies exist.
//!
//! # Module Organization
//!
//! The registry module is organized into several submodules:
//!
//! - [`signal`] - Defines the structure of signals and their components
//! - [`source`] - Defines data sources for signals
//! - [`processor`] - Defines how to process source data into a signal value
//! - [`post_processor`] - Defines transformations applied after initial processing
//!
//! # Type States
//!
//! The registry uses type states to distinguish between validated and unvalidated registries:
//!
//! - [`Registry<Invalid>`] - A registry that has not been validated or has failed validation
//! - [`Registry<Valid>`] - A registry that has been successfully validated and is cycle free
//!
//! This pattern ensures at compile time that only valid registries can be used in contexts
//! where a valid registry is required.

use std::collections::HashMap;
use std::marker::PhantomData;

use bincode::de::Decoder;
use bincode::enc::Encoder;
use bincode::error::{DecodeError, EncodeError};
use bincode::{Decode, Encode};
use serde::{Deserialize, Serialize};

use crate::registry::signal::Signal;
pub use crate::registry::validate::ValidationError;
use crate::registry::validate::validate_signal;

pub mod post_processor;
pub mod processor;
pub mod signal;
pub mod source;
pub(crate) mod validate;

/// Marker type representing an unvalidated registry state.
///
/// This type is used as a type parameter for [`Registry`] to indicate that
/// the registry has not been validated or has failed validation. A registry
/// in the `Invalid` state cannot be used for certain operations that require
/// a valid registry.
#[derive(Clone, Debug, PartialEq)]
pub struct Invalid;

/// Marker type representing a validated registry state.
///
/// This type is used as a type parameter for [`Registry`] to indicate that
/// the registry has been successfully validated. A registry in the `Valid` state
/// is guaranteed to have consistent signal dependencies and no cycles.
#[derive(Clone, Debug, PartialEq)]
pub struct Valid;

/// A collection of signals with their computational dependencies.
///
/// The `Registry` struct serves as the central data structure for managing asset signals.
/// It stores signal definitions indexed by their unique IDs and provides methods for
/// accessing and validating them.
///
/// The `State` type parameter indicates whether the registry has been validated:
/// - [`Registry<Invalid>`] - A registry that has not been validated or has failed validation
/// - [`Registry<Valid>`] - A registry that has been successfully validated
///
/// # Examples
///
/// Creating and validating a registry:
///
/// ```
/// use bothan_lib::registry::{Registry, Invalid, Valid};
/// use serde_json::json;
///
/// // Create a registry from JSON
/// let json_data = json!({
///     "BTC-USD": {
///         "sources": [
///             {
///                 "source_id": "binance",
///                 "id": "btcusdt",
///                 "routes": []
///             }
///         ],
///         "processor": {
///             "function": "median",
///             "params": {
///                 "min_source_count": 1
///             }
///         },
///         "post_processors": []
///     }
/// });
///
/// let registry_json = serde_json::to_string(&json_data).unwrap();
/// let registry: Registry<Invalid> = serde_json::from_str(&registry_json).unwrap();
///
/// // Validate the registry
/// let valid_registry: Registry<Valid> = registry.validate().unwrap();
///
/// // Use the validated registry
/// assert!(valid_registry.contains("BTC-USD"));
/// ```
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Registry<State = Invalid> {
    #[serde(flatten)]
    inner: HashMap<String, Signal>,
    #[serde(skip)]
    _state: PhantomData<State>,
}

impl Registry<Invalid> {
    /// Validates the registry, ensuring all dependencies exist and there are no cycles.
    ///
    /// This method performs a thorough validation of the registry, checking that:
    /// - All signal dependencies (referenced by signal IDs) exist in the registry
    /// - There are no circular dependencies between signals
    ///
    /// If validation is successful, the registry is converted to the `Valid` state.
    /// If validation fails, an error is returned explaining the validation failure.
    ///
    /// # Returns
    ///
    /// - `Ok(Registry<Valid>)` if validation is successful
    /// - `Err(ValidationError)` if validation fails
    ///
    /// # Examples
    ///
    /// ```
    /// use bothan_lib::registry::{Registry, ValidationError};
    /// use serde_json::json;
    ///
    /// // Create a valid registry
    /// let valid_json = json!({
    ///     "USDT-USD": {
    ///         "sources": [
    ///             {
    ///                 "source_id": "coingecko",
    ///                 "id": "tether",
    ///                 "routes": []
    ///             }
    ///         ],
    ///         "processor": {
    ///             "function": "median",
    ///             "params": {
    ///                 "min_source_count": 1
    ///             }
    ///         },
    ///         "post_processors": []
    ///     }
    /// });
    ///
    /// let valid_registry: Registry = serde_json::from_value(valid_json).unwrap();
    /// assert!(valid_registry.validate().is_ok());
    ///
    /// // Create an invalid registry with a missing dependency
    /// let invalid_json = json!({
    ///     "BTC-USD": {
    ///         "sources": [
    ///             {
    ///                 "source_id": "binance",
    ///                 "id": "btcusdt",
    ///                 "routes": [
    ///                     {
    ///                         "signal_id": "USDT-USD", // This dependency doesn't exist
    ///                         "operation": "*"
    ///                     }
    ///                 ]
    ///             }
    ///         ],
    ///         "processor": {
    ///             "function": "median",
    ///             "params": {
    ///                 "min_source_count": 1
    ///             }
    ///         },
    ///         "post_processors": []
    ///     }
    /// });
    ///
    /// let invalid_registry: Registry = serde_json::from_value(invalid_json).unwrap();
    /// assert!(invalid_registry.validate().is_err());
    /// ```
    pub fn validate(self) -> Result<Registry<Valid>, ValidationError> {
        let mut visited = HashMap::new();
        for root in self.inner.keys() {
            validate_signal(root, &mut visited, &self)?;
        }

        Ok(Registry {
            inner: self.inner,
            _state: PhantomData,
        })
    }
}

impl<T> Registry<T> {
    /// Returns the signal for a given signal id.
    ///
    /// This method retrieves a reference to the [`Signal`] with the specified ID,
    /// if it exists in the registry.
    ///
    /// # Returns
    ///
    /// * `Some(&Signal)` if the signal exists in the registry
    /// * `None` if the signal does not exist
    pub fn get(&self, signal_id: &str) -> Option<&Signal> {
        self.inner.get(signal_id)
    }

    /// Returns `true` if the registry contains the given signal id.
    ///
    /// This method checks whether a signal with the specified ID exists in the registry.
    ///
    /// # Returns
    ///
    /// * `true` if the signal exists in the registry
    /// * `false` if the signal does not exist
    pub fn contains(&self, signal_id: &str) -> bool {
        self.inner.contains_key(signal_id)
    }

    /// An iterator visiting all signal ids in the registry in arbitrary order.
    ///
    /// This method returns an iterator over all signal IDs in the registry.
    /// The order of iteration is not specified and may vary between calls.
    ///
    /// # Returns
    ///
    /// An iterator yielding references to signal IDs
    pub fn signal_ids(&self) -> impl Iterator<Item = &String> {
        self.inner.keys()
    }

    /// An iterator visiting all the signal ids and their signals in the registry in arbitrary order.
    ///
    /// This method returns an iterator over all signal IDs and their corresponding signals
    /// in the registry. The order of iteration is not specified and may vary between calls.
    ///
    /// # Returns
    ///
    /// An iterator yielding pairs of references to signal IDs and their signals
    pub fn iter(&self) -> impl Iterator<Item = (&String, &Signal)> {
        self.inner.iter()
    }
}

impl Default for Registry<Valid> {
    /// Creates a new empty validated registry.
    ///
    /// This method creates a default validated registry, which is empty.
    /// This is useful as a starting point for building a registry programmatically.
    ///
    /// # Returns
    ///
    /// An empty `Registry<Valid>`
    fn default() -> Self {
        Registry {
            inner: HashMap::new(),
            _state: PhantomData,
        }
    }
}

impl<State> Encode for Registry<State> {
    /// Encodes the registry for binary serialization.
    ///
    /// This implementation enables efficient binary serialization of the registry
    /// using the `bincode` crate. It encodes the inner map of signals.
    ///
    /// # Errors
    ///
    /// Returns an `EncodeError` if encoding fails
    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
        self.inner.encode(encoder)
    }
}

impl<Context> Decode<Context> for Registry<Invalid> {
    /// Decodes a registry from binary serialization.
    ///
    /// This implementation enables efficient binary deserialization of the registry
    /// using the `bincode` crate. It decodes the inner map of signals and creates
    /// an invalid registry that must be validated before use.
    ///
    /// # Errors
    ///
    /// Returns a `DecodeError` if decoding fails
    fn decode<D: Decoder>(decoder: &mut D) -> Result<Self, DecodeError> {
        let inner = HashMap::decode(decoder)?;
        Ok(Registry {
            inner,
            _state: PhantomData,
        })
    }
}

#[cfg(test)]
pub(crate) mod tests {
    use crate::registry::{Invalid, Registry, ValidationError};

    pub fn valid_mock_registry() -> Registry<Invalid> {
        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\":[]}}";
        serde_json::from_str::<Registry>(json_string).unwrap()
    }

    pub fn invalid_dependency_mock_registry() -> Registry<Invalid> {
        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\":[]}}";
        serde_json::from_str::<Registry>(json_string).unwrap()
    }

    pub fn complete_circular_dependency_mock_registry() -> Registry<Invalid> {
        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\":[]}}";
        serde_json::from_str::<Registry>(json_string).unwrap()
    }

    pub fn circular_dependency_mock_registry() -> Registry<Invalid> {
        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\":[]}}";
        serde_json::from_str::<Registry>(json_string).unwrap()
    }

    #[test]
    fn test_registry_validate_valid_registry() {
        let registry = valid_mock_registry();
        let valid_registry = registry.validate();
        assert!(valid_registry.is_ok());
    }

    #[test]
    fn test_registry_validate_registry_with_invalid_dependency() {
        let registry = invalid_dependency_mock_registry();
        assert_eq!(
            registry.validate(),
            Err(ValidationError::InvalidDependency("CS:BTC-USD".to_string()))
        );
    }

    #[test]
    fn test_registry_validate_registry_with_complete_circular_dependency() {
        let registry = complete_circular_dependency_mock_registry();
        assert!(matches!(
            registry.validate(),
            Err(ValidationError::CycleDetected(_))
        ))
    }

    #[test]
    fn test_registry_validate_registry_with_circular_dependency() {
        let registry = circular_dependency_mock_registry();
        assert!(matches!(
            registry.validate(),
            Err(ValidationError::CycleDetected(_))
        ))
    }
}