1#![allow(clippy::result_large_err)]
8
9use std::{collections::HashMap, marker::PhantomData, path::Path};
10
11use async_trait::async_trait;
12
13use crate::{
14	models::{ConfigLoader, Monitor, Network, Trigger, SCRIPT_LANGUAGE_EXTENSIONS},
15	repositories::{
16		error::RepositoryError,
17		network::{NetworkRepository, NetworkRepositoryTrait, NetworkService},
18		trigger::{TriggerRepository, TriggerRepositoryTrait, TriggerService},
19	},
20};
21
22#[derive(Clone)]
24pub struct MonitorRepository<
25	N: NetworkRepositoryTrait + Send + 'static,
26	T: TriggerRepositoryTrait + Send + 'static,
27> {
28	pub monitors: HashMap<String, Monitor>,
30	_network_repository: PhantomData<N>,
31	_trigger_repository: PhantomData<T>,
32}
33
34impl<
35		N: NetworkRepositoryTrait + Send + Sync + 'static,
36		T: TriggerRepositoryTrait + Send + Sync + 'static,
37	> MonitorRepository<N, T>
38{
39	pub async fn new(
44		path: Option<&Path>,
45		network_service: Option<NetworkService<N>>,
46		trigger_service: Option<TriggerService<T>>,
47	) -> Result<Self, RepositoryError> {
48		let monitors = Self::load_all(path, network_service, trigger_service).await?;
49		Ok(MonitorRepository {
50			monitors,
51			_network_repository: PhantomData,
52			_trigger_repository: PhantomData,
53		})
54	}
55
56	pub fn new_with_monitors(monitors: HashMap<String, Monitor>) -> Self {
58		MonitorRepository {
59			monitors,
60			_network_repository: PhantomData,
61			_trigger_repository: PhantomData,
62		}
63	}
64
65	pub fn validate_monitor_references(
67		monitors: &HashMap<String, Monitor>,
68		triggers: &HashMap<String, Trigger>,
69		networks: &HashMap<String, Network>,
70	) -> Result<(), RepositoryError> {
71		let mut validation_errors = Vec::new();
72		let mut metadata = HashMap::new();
73
74		for (monitor_name, monitor) in monitors {
75			for trigger_id in &monitor.triggers {
77				if !triggers.contains_key(trigger_id) {
78					validation_errors.push(format!(
79						"Monitor '{}' references non-existent trigger '{}'",
80						monitor_name, trigger_id
81					));
82					metadata.insert(
83						format!("monitor_{}_invalid_trigger", monitor_name),
84						trigger_id.clone(),
85					);
86				}
87			}
88
89			for network_slug in &monitor.networks {
91				if !networks.contains_key(network_slug) {
92					validation_errors.push(format!(
93						"Monitor '{}' references non-existent network '{}'",
94						monitor_name, network_slug
95					));
96					metadata.insert(
97						format!("monitor_{}_invalid_network", monitor_name),
98						network_slug.clone(),
99					);
100				}
101			}
102
103			for condition in &monitor.trigger_conditions {
105				let script_path = Path::new(&condition.script_path);
106				if !script_path.exists() {
107					validation_errors.push(format!(
108						"Monitor '{}' has a custom filter script that does not exist: {}",
109						monitor_name, condition.script_path
110					));
111				}
112
113				let expected_extension = match SCRIPT_LANGUAGE_EXTENSIONS
115					.iter()
116					.find(|(lang, _)| *lang == &condition.language)
117					.map(|(_, ext)| *ext)
118				{
119					Some(ext) => ext,
120					None => {
121						validation_errors.push(format!(
122							"Monitor '{}' uses unsupported script language {:?}",
123							monitor_name, condition.language
124						));
125						continue;
126					}
127				};
128
129				match script_path.extension().and_then(|ext| ext.to_str()) {
130					Some(ext) if ext == expected_extension => (), _ => validation_errors.push(format!(
132						"Monitor '{}' has a custom filter script with invalid extension - must be \
133						 .{} for {:?} language: {}",
134						monitor_name, expected_extension, condition.language, condition.script_path
135					)),
136				}
137
138				if condition.timeout_ms == 0 {
139					validation_errors.push(format!(
140						"Monitor '{}' should have a custom filter timeout_ms greater than 0",
141						monitor_name
142					));
143				}
144			}
145		}
146
147		if !validation_errors.is_empty() {
148			return Err(RepositoryError::validation_error(
149				format!(
150					"Configuration validation failed:\n{}",
151					validation_errors.join("\n"),
152				),
153				None,
154				Some(metadata),
155			));
156		}
157
158		Ok(())
159	}
160}
161
162#[async_trait]
167pub trait MonitorRepositoryTrait<
168	N: NetworkRepositoryTrait + Send + 'static,
169	T: TriggerRepositoryTrait + Send + 'static,
170>: Clone + Send
171{
172	async fn new(
174		path: Option<&Path>,
175		network_service: Option<NetworkService<N>>,
176		trigger_service: Option<TriggerService<T>>,
177	) -> Result<Self, RepositoryError>
178	where
179		Self: Sized;
180
181	async fn load_all(
187		path: Option<&Path>,
188		network_service: Option<NetworkService<N>>,
189		trigger_service: Option<TriggerService<T>>,
190	) -> Result<HashMap<String, Monitor>, RepositoryError>;
191
192	async fn load_from_path(
196		&self,
197		path: Option<&Path>,
198		network_service: Option<NetworkService<N>>,
199		trigger_service: Option<TriggerService<T>>,
200	) -> Result<Monitor, RepositoryError>;
201
202	fn get(&self, monitor_id: &str) -> Option<Monitor>;
206
207	fn get_all(&self) -> HashMap<String, Monitor>;
211}
212
213#[async_trait]
214impl<
215		N: NetworkRepositoryTrait + Send + Sync + 'static,
216		T: TriggerRepositoryTrait + Send + Sync + 'static,
217	> MonitorRepositoryTrait<N, T> for MonitorRepository<N, T>
218{
219	async fn new(
220		path: Option<&Path>,
221		network_service: Option<NetworkService<N>>,
222		trigger_service: Option<TriggerService<T>>,
223	) -> Result<Self, RepositoryError> {
224		MonitorRepository::new(path, network_service, trigger_service).await
225	}
226
227	async fn load_all(
228		path: Option<&Path>,
229		network_service: Option<NetworkService<N>>,
230		trigger_service: Option<TriggerService<T>>,
231	) -> Result<HashMap<String, Monitor>, RepositoryError> {
232		let monitors = Monitor::load_all(path).await.map_err(|e| {
233			RepositoryError::load_error(
234				"Failed to load monitors",
235				Some(Box::new(e)),
236				Some(HashMap::from([(
237					"path".to_string(),
238					path.map_or_else(|| "default".to_string(), |p| p.display().to_string()),
239				)])),
240			)
241		})?;
242
243		let networks = match network_service {
244			Some(service) => service.get_all(),
245			None => {
246				NetworkRepository::new(None)
247					.await
248					.map_err(|e| {
249						RepositoryError::load_error(
250							"Failed to load networks for monitor validation",
251							Some(Box::new(e)),
252							None,
253						)
254					})?
255					.networks
256			}
257		};
258
259		let triggers = match trigger_service {
260			Some(service) => service.get_all(),
261			None => {
262				TriggerRepository::new(None)
263					.await
264					.map_err(|e| {
265						RepositoryError::load_error(
266							"Failed to load triggers for monitor validation",
267							Some(Box::new(e)),
268							None,
269						)
270					})?
271					.triggers
272			}
273		};
274
275		Self::validate_monitor_references(&monitors, &triggers, &networks)?;
276		Ok(monitors)
277	}
278
279	async fn load_from_path(
283		&self,
284		path: Option<&Path>,
285		network_service: Option<NetworkService<N>>,
286		trigger_service: Option<TriggerService<T>>,
287	) -> Result<Monitor, RepositoryError> {
288		match path {
289			Some(path) => {
290				let monitor = Monitor::load_from_path(path).await.map_err(|e| {
291					RepositoryError::load_error(
292						"Failed to load monitors",
293						Some(Box::new(e)),
294						Some(HashMap::from([(
295							"path".to_string(),
296							path.display().to_string(),
297						)])),
298					)
299				})?;
300
301				let networks = match network_service {
302					Some(service) => service.get_all(),
303					None => NetworkRepository::new(None).await?.networks,
304				};
305
306				let triggers = match trigger_service {
307					Some(service) => service.get_all(),
308					None => TriggerRepository::new(None).await?.triggers,
309				};
310				let monitors = HashMap::from([(monitor.name.clone(), monitor)]);
311				Self::validate_monitor_references(&monitors, &triggers, &networks)?;
312				match monitors.values().next() {
313					Some(monitor) => Ok(monitor.clone()),
314					None => Err(RepositoryError::load_error("No monitors found", None, None)),
315				}
316			}
317			None => Err(RepositoryError::load_error(
318				"Failed to load monitors",
319				None,
320				None,
321			)),
322		}
323	}
324
325	fn get(&self, monitor_id: &str) -> Option<Monitor> {
326		self.monitors.get(monitor_id).cloned()
327	}
328
329	fn get_all(&self) -> HashMap<String, Monitor> {
330		self.monitors.clone()
331	}
332}
333
334#[derive(Clone)]
340pub struct MonitorService<
341	M: MonitorRepositoryTrait<N, T> + Send,
342	N: NetworkRepositoryTrait + Send + Sync + 'static,
343	T: TriggerRepositoryTrait + Send + Sync + 'static,
344> {
345	repository: M,
346	_network_repository: PhantomData<N>,
347	_trigger_repository: PhantomData<T>,
348}
349
350impl<
351		M: MonitorRepositoryTrait<N, T> + Send,
352		N: NetworkRepositoryTrait + Send + Sync + 'static,
353		T: TriggerRepositoryTrait + Send + Sync + 'static,
354	> MonitorService<M, N, T>
355{
356	pub async fn new(
361		path: Option<&Path>,
362		network_service: Option<NetworkService<N>>,
363		trigger_service: Option<TriggerService<T>>,
364	) -> Result<MonitorService<M, N, T>, RepositoryError> {
365		let repository = M::new(path, network_service, trigger_service).await?;
366		Ok(MonitorService {
367			repository,
368			_network_repository: PhantomData,
369			_trigger_repository: PhantomData,
370		})
371	}
372
373	pub async fn new_with_path(
377		path: Option<&Path>,
378	) -> Result<MonitorService<M, N, T>, RepositoryError> {
379		let repository = M::new(path, None, None).await?;
380		Ok(MonitorService {
381			repository,
382			_network_repository: PhantomData,
383			_trigger_repository: PhantomData,
384		})
385	}
386
387	pub fn new_with_repository(repository: M) -> Result<Self, RepositoryError> {
391		Ok(MonitorService {
392			repository,
393			_network_repository: PhantomData,
394			_trigger_repository: PhantomData,
395		})
396	}
397
398	pub fn get(&self, monitor_id: &str) -> Option<Monitor> {
402		self.repository.get(monitor_id)
403	}
404
405	pub fn get_all(&self) -> HashMap<String, Monitor> {
409		self.repository.get_all()
410	}
411
412	pub async fn load_from_path(
416		&self,
417		path: Option<&Path>,
418		network_service: Option<NetworkService<N>>,
419		trigger_service: Option<TriggerService<T>>,
420	) -> Result<Monitor, RepositoryError> {
421		self.repository
422			.load_from_path(path, network_service, trigger_service)
423			.await
424	}
425}
426
427#[cfg(test)]
428mod tests {
429	use super::*;
430	use crate::{models::ScriptLanguage, utils::tests::builders::evm::monitor::MonitorBuilder};
431	use std::fs;
432	use tempfile::TempDir;
433
434	#[test]
435	fn test_validate_custom_trigger_conditions() {
436		let temp_dir = TempDir::new().unwrap();
437		let script_path = temp_dir.path().join("test_script.py");
438		fs::write(&script_path, "print('test')").unwrap();
439
440		let mut monitors = HashMap::new();
441		let triggers = HashMap::new();
442		let networks = HashMap::new();
443
444		let monitor = MonitorBuilder::new()
446			.name("test_monitor")
447			.networks(vec![])
448			.trigger_condition(
449				script_path.to_str().unwrap(),
450				1000,
451				ScriptLanguage::Python,
452				None,
453			)
454			.build();
455		monitors.insert("test_monitor".to_string(), monitor);
456
457		let result =
458			MonitorRepository::<NetworkRepository, TriggerRepository>::validate_monitor_references(
459				&monitors, &triggers, &networks,
460			);
461		assert!(result.is_ok());
462
463		let monitor_bad_path = MonitorBuilder::new()
465			.name("test_monitor_bad_path")
466			.trigger_condition("non_existent_script.py", 1000, ScriptLanguage::Python, None)
467			.build();
468		monitors.insert("test_monitor_bad_path".to_string(), monitor_bad_path);
469
470		let err =
471			MonitorRepository::<NetworkRepository, TriggerRepository>::validate_monitor_references(
472				&monitors, &triggers, &networks,
473			)
474			.unwrap_err();
475		assert!(err.to_string().contains("does not exist"));
476
477		let wrong_ext_path = temp_dir.path().join("test_script.js");
479		fs::write(&wrong_ext_path, "print('test')").unwrap();
480
481		let monitor_wrong_ext = MonitorBuilder::new()
482			.name("test_monitor_wrong_ext")
483			.trigger_condition(
484				wrong_ext_path.to_str().unwrap(),
485				1000,
486				ScriptLanguage::Python,
487				None,
488			)
489			.build();
490		monitors.clear();
491		monitors.insert("test_monitor_wrong_ext".to_string(), monitor_wrong_ext);
492
493		let err =
494			MonitorRepository::<NetworkRepository, TriggerRepository>::validate_monitor_references(
495				&monitors, &triggers, &networks,
496			)
497			.unwrap_err();
498		assert!(err.to_string().contains(
499			"Monitor 'test_monitor_wrong_ext' has a custom filter script with invalid extension - \
500			 must be .py for Python language"
501		));
502
503		let monitor_zero_timeout = MonitorBuilder::new()
505			.name("test_monitor_zero_timeout")
506			.trigger_condition(
507				script_path.to_str().unwrap(),
508				0,
509				ScriptLanguage::Python,
510				None,
511			)
512			.build();
513		monitors.clear();
514		monitors.insert(
515			"test_monitor_zero_timeout".to_string(),
516			monitor_zero_timeout,
517		);
518
519		let err =
520			MonitorRepository::<NetworkRepository, TriggerRepository>::validate_monitor_references(
521				&monitors, &triggers, &networks,
522			)
523			.unwrap_err();
524		assert!(err.to_string().contains("timeout_ms greater than 0"));
525	}
526
527	#[tokio::test]
528	async fn test_load_error_messages() {
529		let invalid_path = Path::new("/non/existent/path");
531		let result = MonitorRepository::<NetworkRepository, TriggerRepository>::load_all(
532			Some(invalid_path),
533			None,
534			None,
535		)
536		.await;
537
538		assert!(result.is_err());
539		let err = result.unwrap_err();
540		match err {
541			RepositoryError::LoadError(message) => {
542				assert!(message.to_string().contains("Failed to load monitors"));
543			}
544			_ => panic!("Expected RepositoryError::LoadError"),
545		}
546	}
547
548	#[test]
549	fn test_network_validation_error() {
550		let mut monitors = HashMap::new();
552		let monitor = MonitorBuilder::new()
553			.name("test_monitor")
554			.networks(vec!["non_existent_network".to_string()])
555			.build();
556		monitors.insert("test_monitor".to_string(), monitor);
557
558		let networks = HashMap::new();
560		let triggers = HashMap::new();
561
562		let result =
564			MonitorRepository::<NetworkRepository, TriggerRepository>::validate_monitor_references(
565				&monitors, &triggers, &networks,
566			);
567
568		assert!(result.is_err());
569		let err = result.unwrap_err();
570		assert!(err.to_string().contains("references non-existent network"));
571	}
572
573	#[test]
574	fn test_trigger_validation_error() {
575		let mut monitors = HashMap::new();
577		let monitor = MonitorBuilder::new()
578			.name("test_monitor")
579			.triggers(vec!["non_existent_trigger".to_string()])
580			.build();
581		monitors.insert("test_monitor".to_string(), monitor);
582
583		let networks = HashMap::new();
585		let triggers = HashMap::new();
586
587		let result =
589			MonitorRepository::<NetworkRepository, TriggerRepository>::validate_monitor_references(
590				&monitors, &triggers, &networks,
591			);
592
593		assert!(result.is_err());
594		let err = result.unwrap_err();
595		assert!(err.to_string().contains("references non-existent trigger"));
596	}
597
598	#[tokio::test]
599	async fn test_load_from_path_error_handling() {
600		let temp_dir = TempDir::new().unwrap();
602		let invalid_path = temp_dir.path().join("non_existent_monitor.json");
603
604		let repository =
606			MonitorRepository::<NetworkRepository, TriggerRepository>::new_with_monitors(
607				HashMap::new(),
608			);
609
610		let result = repository
612			.load_from_path(Some(&invalid_path), None, None)
613			.await;
614
615		assert!(result.is_err());
617		let err = result.unwrap_err();
618		match err {
619			RepositoryError::LoadError(message) => {
620				assert!(message.to_string().contains("Failed to load monitors"));
621				assert!(message
623					.to_string()
624					.contains(&invalid_path.display().to_string()));
625			}
626			_ => panic!("Expected RepositoryError::LoadError"),
627		}
628	}
629}