bothan_lib/registry/
processor.rs

1//! Processing strategies for combining source data into signal values.
2//!
3//! This module provides processors that transform multiple input values from
4//! various sources into a single output value. Each processor implements a different
5//! strategy for combining these values, such as taking the median or weighted median.
6//!
7//! The module provides:
8//!
9//! - The [`Processor`] enum which represents different processing strategies
10//! - Specialized processor implementations in submodules
11//! - Error handling for the processing operations
12//!
13//! # Available Processors
14//!
15//! The following processing strategies are available:
16//!
17//! - [`Median`](median::MedianProcessor) - Computes the median of the input values
18//! - [`WeightedMedian`](weighted_median::WeightedMedianProcessor) - Computes a weighted median based on configurable weights
19//!
20//! # Extensibility
21//!
22//! This module is designed to be extensible. New processing strategies can be added by:
23//!
24//! 1. Creating a new processor implementation in a submodule
25//! 2. Adding a new variant to the [`Processor`] enum
26//! 3. Implementing the necessary logic in the `name` and `process` methods
27//!
28//! # Usage in Registry
29//!
30//! Processors are used within the registry system to define how source data should be
31//! combined into a single value for each signal. Each signal specifies a processor
32//! and its configuration parameters.
33
34use bincode::{Decode, Encode};
35use rust_decimal::Decimal;
36use serde::{Deserialize, Serialize};
37use thiserror::Error;
38
39pub mod median;
40pub mod weighted_median;
41
42/// Error type for processor operations.
43///
44/// This error is returned when a processor encounters an issue while
45/// processing data, such as insufficient data points or mathematical errors.
46///
47/// # Examples
48///
49/// ```
50/// use bothan_lib::registry::processor::ProcessError;
51///
52/// let error = ProcessError::new("Insufficient data points");
53/// assert_eq!(error.to_string(), "Insufficient data points");
54/// ```
55#[derive(Clone, Debug, Error, PartialEq, Serialize, Deserialize)]
56#[error("{msg}")]
57pub struct ProcessError {
58    msg: String,
59}
60
61impl ProcessError {
62    /// Creates a new ProcessError with the specified message.
63    pub fn new<T: Into<String>>(msg: T) -> Self {
64        ProcessError { msg: msg.into() }
65    }
66}
67
68/// Represents different strategies for processing source data into a signal value.
69///
70/// The `Processor` enum encapsulates different algorithms for combining multiple
71/// input values from various sources into a single output value. Each variant
72/// contains its own configuration parameters.
73///
74/// # Variants
75///
76/// * `Median` - Computes the median of the input values
77/// * `WeightedMedian` - Computes a weighted median based on configured weights
78///
79/// # Examples
80///
81/// Creating a median processor:
82///
83/// ```
84/// use bothan_lib::registry::processor::{Processor, median::MedianProcessor};
85/// use serde_json::json;
86///
87/// // Create a processor from JSON
88/// let json_data = json!({
89///     "function": "median",
90///     "params": {
91///         "min_source_count": 3
92///     }
93/// });
94///
95/// let processor: Processor = serde_json::from_value(json_data).unwrap();
96/// assert_eq!(processor.name(), "median");
97/// ```
98///
99/// Using a processor:
100///
101/// ```
102/// use bothan_lib::registry::processor::{Processor, median::MedianProcessor};
103/// use rust_decimal::Decimal;
104///
105/// // Create a median processor that requires at least 3 sources
106/// let processor = Processor::Median(MedianProcessor { min_source_count: 3 });
107///
108/// // Process some data
109/// let data = vec![
110///     ("source1".to_string(), Decimal::new(100, 0)),
111///     ("source2".to_string(), Decimal::new(200, 0)),
112///     ("source3".to_string(), Decimal::new(300, 0)),
113/// ];
114///
115/// let result = processor.process(data).unwrap();
116/// assert_eq!(result, Decimal::new(200, 0));
117/// ```
118#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
119#[serde(rename_all = "snake_case", tag = "function", content = "params")]
120pub enum Processor {
121    /// Median processor that computes the median of input values.
122    Median(median::MedianProcessor),
123
124    /// Weighted median processor that applies weights to input values.
125    WeightedMedian(weighted_median::WeightedMedianProcessor),
126}
127
128impl Processor {
129    /// Returns the name of the processor.
130    ///
131    /// This method returns a string identifier for the processor type,
132    /// which can be used for logging, debugging, or serialization purposes.
133    pub fn name(&self) -> &str {
134        match self {
135            Processor::Median(_) => "median",
136            Processor::WeightedMedian(_) => "weighted_median",
137        }
138    }
139
140    /// Processes input data into a single output value.
141    ///
142    /// This method applies the processor's algorithm to the input data
143    /// to produce a single output value. The input data consists of pairs
144    /// of source identifiers and their corresponding values.
145    ///
146    /// # Errors
147    ///
148    /// Returns a `ProcessError` if the processing operation fails, such as when:
149    /// - There are insufficient data points
150    /// - The data does not meet the processor's requirements
151    /// - A mathematical error occurs during processing
152    pub fn process(&self, data: Vec<(String, Decimal)>) -> Result<Decimal, ProcessError> {
153        match self {
154            Processor::Median(median) => {
155                let data = data.into_iter().map(|(_, value)| value).collect();
156                median.process(data)
157            }
158            Processor::WeightedMedian(weighted_median) => weighted_median.process(data),
159        }
160    }
161}