bothan_lib/registry/
validate.rs1use std::collections::HashMap;
14
15use thiserror::Error;
16
17use crate::registry::Registry;
18use crate::registry::processor::Processor;
19use crate::registry::processor::weighted_median::WeightedMedianProcessor;
20use crate::registry::source::SourceQuery;
21
22#[derive(Clone, Debug, Error, PartialEq)]
32pub enum ValidationError {
33 #[error("Signal {0} contains a cycle")]
35 CycleDetected(String),
36
37 #[error("Signal {0} contains an invalid dependency")]
39 InvalidDependency(String),
40
41 #[error("Signal {0} contains an invalid weighted median processor: {1}")]
43 InvalidWeightedMedianProcessor(String, String),
44}
45
46pub enum VisitState {
51 InProgress,
53
54 Complete,
56}
57
58pub(crate) fn validate_signal(
59 signal_id: &str,
60 visited: &mut HashMap<String, VisitState>,
61 registry: &Registry,
62) -> Result<(), ValidationError> {
63 match visited.get(signal_id) {
64 Some(VisitState::InProgress) => {
65 return Err(ValidationError::CycleDetected(signal_id.to_string()));
66 }
67 Some(VisitState::Complete) => {
68 return Ok(());
69 }
70 None => {
71 visited.insert(signal_id.to_string(), VisitState::InProgress);
72 }
73 }
74
75 let signal = registry
76 .get(signal_id)
77 .ok_or(ValidationError::InvalidDependency(signal_id.to_string()))?;
78
79 if let Processor::WeightedMedian(weighted_median) = &signal.processor {
80 validate_weighted_median_weights(weighted_median, signal_id, &signal.source_queries)?;
81 }
82
83 for source_query in signal.source_queries.iter() {
84 for route in source_query.routes.iter() {
85 if !registry.contains(&route.signal_id) {
86 return Err(ValidationError::InvalidDependency(signal_id.to_string()));
87 }
88 validate_signal(&route.signal_id, visited, registry)?;
89 }
90 }
91
92 visited.insert(signal_id.to_string(), VisitState::Complete);
93
94 Ok(())
95}
96
97fn validate_weighted_median_weights(
98 weighted_median: &WeightedMedianProcessor,
99 signal_id: &str,
100 source_queries: &[SourceQuery],
101) -> Result<(), ValidationError> {
102 source_queries.iter().try_for_each(|source_query| {
103 if !weighted_median
104 .source_weights
105 .contains_key(&source_query.source_id)
106 {
107 Err(ValidationError::InvalidWeightedMedianProcessor(
108 signal_id.to_string(),
109 source_query.source_id.to_string(),
110 ))
111 } else {
112 Ok(())
113 }
114 })
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::registry::tests::{complete_circular_dependency_mock_registry, valid_mock_registry};
121
122 #[test]
123 fn test_validate_signal() {
124 let registry = valid_mock_registry();
125
126 let res = validate_signal("CS:BTC-USD", &mut HashMap::new(), ®istry);
127 assert!(res.is_ok());
128 }
129
130 #[test]
131 fn test_validate_signal_with_invalid_signal() {
132 let registry = valid_mock_registry();
133
134 let res = validate_signal("CS:DNE-USD", &mut HashMap::new(), ®istry);
135 assert_eq!(
136 res,
137 Err(ValidationError::InvalidDependency("CS:DNE-USD".to_string()))
138 );
139 }
140
141 #[test]
142 fn test_validate_signal_with_circular_dependency() {
143 let registry = complete_circular_dependency_mock_registry();
144 let mut visited = HashMap::new();
145
146 let res = validate_signal("CS:USDT-USD", &mut visited, ®istry);
147 assert_eq!(
148 res,
149 Err(ValidationError::CycleDetected("CS:USDT-USD".to_string()))
150 );
151 }
152}