fynd_core/algorithm/
registry.rs1use std::{collections::HashMap, sync::Arc};
19
20use crate::{
21 algorithm::{Algorithm, AlgorithmConfig},
22 feed::events::MarketEventHandler,
23 graph::EdgeWeightUpdaterWithDerived,
24 worker_pool::{
25 pool::WorkerPoolBuilder,
26 registry::{UnknownAlgorithmError, AVAILABLE_ALGORITHMS},
27 },
28};
29
30type Configure = Arc<dyn Fn(WorkerPoolBuilder) -> WorkerPoolBuilder + Send + Sync>;
35
36#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
38pub enum RegisterAlgorithmError {
39 #[error("'{name}' is a built-in algorithm; registering it would replace the shipped one")]
41 ShadowsBuiltIn {
42 name: String,
44 },
45
46 #[error("'{name}' is already registered")]
48 AlreadyRegistered {
49 name: String,
51 },
52}
53
54#[derive(Default, Clone)]
58pub struct AlgorithmRegistry {
59 by_name: HashMap<String, Configure>,
60}
61
62impl AlgorithmRegistry {
63 #[must_use]
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 pub fn with_algorithm<A, F>(
80 mut self,
81 name: impl Into<String>,
82 factory: F,
83 ) -> Result<Self, RegisterAlgorithmError>
84 where
85 A: Algorithm + 'static,
86 A::GraphManager: MarketEventHandler + EdgeWeightUpdaterWithDerived + 'static,
87 F: Fn(AlgorithmConfig) -> A + Clone + Send + Sync + 'static,
88 {
89 let name = name.into();
90 if AVAILABLE_ALGORITHMS.contains(&name.as_str()) {
91 return Err(RegisterAlgorithmError::ShadowsBuiltIn { name });
92 }
93 if self.by_name.contains_key(&name) {
94 return Err(RegisterAlgorithmError::AlreadyRegistered { name });
95 }
96 let registered = name.clone();
97 self.by_name.insert(
98 name,
99 Arc::new(move |builder: WorkerPoolBuilder| {
100 builder.with_algorithm(registered.clone(), factory.clone())
101 }),
102 );
103 Ok(self)
104 }
105
106 pub fn names(&self) -> impl Iterator<Item = &str> {
108 self.by_name.keys().map(String::as_str)
109 }
110
111 pub(crate) fn configure(
121 &self,
122 name: &str,
123 builder: WorkerPoolBuilder,
124 ) -> Result<WorkerPoolBuilder, UnknownAlgorithmError> {
125 if let Some(configure) = self.by_name.get(name) {
126 return Ok(configure(builder));
127 }
128 if AVAILABLE_ALGORITHMS.contains(&name) {
129 return Ok(builder.algorithm(name));
130 }
131 Err(UnknownAlgorithmError::of(
132 name,
133 self.names()
134 .map(str::to_string)
135 .collect(),
136 ))
137 }
138}
139
140impl std::fmt::Debug for AlgorithmRegistry {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 f.debug_struct("AlgorithmRegistry")
144 .field("names", &self.by_name.keys().collect::<Vec<_>>())
145 .finish()
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::algorithm::{most_liquid::MostLiquidAlgorithm, AlgorithmConfig};
153
154 fn most_liquid(config: AlgorithmConfig) -> MostLiquidAlgorithm {
156 MostLiquidAlgorithm::with_config(config).expect("the default config is valid")
157 }
158
159 fn registry_with(name: &str) -> AlgorithmRegistry {
160 AlgorithmRegistry::new()
161 .with_algorithm(name, most_liquid)
162 .expect("the name is neither built in nor taken")
163 }
164
165 #[test]
166 fn test_registry_is_empty_by_default() {
167 assert_eq!(AlgorithmRegistry::new().names().count(), 0);
168 }
169
170 #[test]
172 fn test_registry_refuses_a_name_already_registered() {
173 let error = registry_with("mine")
174 .with_algorithm("mine", most_liquid)
175 .expect_err("the name is taken");
176
177 assert_eq!(error, RegisterAlgorithmError::AlreadyRegistered { name: "mine".to_string() });
178 }
179
180 #[test]
182 fn test_registry_refuses_a_built_in_name() {
183 let error = AlgorithmRegistry::new()
184 .with_algorithm("most_liquid", most_liquid)
185 .expect_err("the name ships with this crate");
186
187 assert_eq!(
188 error,
189 RegisterAlgorithmError::ShadowsBuiltIn { name: "most_liquid".to_string() }
190 );
191 }
192
193 #[test]
194 fn test_configure_serves_a_registered_name() {
195 let Ok(builder) = registry_with("brought_from_outside")
196 .configure("brought_from_outside", WorkerPoolBuilder::new())
197 else {
198 panic!("a registered name is served");
199 };
200
201 assert!(builder.serves_custom_algorithm(), "a registered name is served by its factory");
202 }
203
204 #[test]
205 fn test_configure_leaves_a_built_in_name_to_the_built_in() {
206 let Ok(builder) = registry_with("mine").configure("water_fill", WorkerPoolBuilder::new())
207 else {
208 panic!("a built-in name is served");
209 };
210
211 assert!(
212 !builder.serves_custom_algorithm(),
213 "a built-in name is not served from the registry"
214 );
215 }
216
217 #[test]
220 fn test_configure_rejects_a_name_neither_side_holds() {
221 let Err(error) = registry_with("brought_from_outside")
222 .configure("brought_from_outsid", WorkerPoolBuilder::new())
223 else {
224 panic!("a name neither side holds must be refused");
225 };
226
227 let message = error.to_string();
228 assert!(message.contains("brought_from_outside"), "lists the registered name: {message}");
229 assert!(message.contains("water_fill"), "lists the built-ins too: {message}");
230 }
231}