openzeppelin_monitor/repositories/
trigger.rs

1//! Trigger configuration repository implementation.
2//!
3//! This module provides storage and retrieval of trigger configurations, which define
4//! actions to take when monitor conditions are met. The repository loads trigger
5//! configurations from JSON files.
6
7#![allow(clippy::result_large_err)]
8
9use std::{collections::HashMap, path::Path};
10
11use async_trait::async_trait;
12
13use crate::{
14	models::{ConfigLoader, Trigger},
15	repositories::error::RepositoryError,
16};
17
18/// Repository for storing and retrieving trigger configurations
19#[derive(Clone)]
20pub struct TriggerRepository {
21	/// Map of trigger names to their configurations
22	pub triggers: HashMap<String, Trigger>,
23}
24
25impl TriggerRepository {
26	/// Create a new trigger repository from the given path
27	///
28	/// Loads all trigger configurations from JSON files in the specified directory
29	/// (or default config directory if None is provided).
30	pub async fn new(path: Option<&Path>) -> Result<Self, RepositoryError> {
31		let triggers = Self::load_all(path).await?;
32		Ok(TriggerRepository { triggers })
33	}
34}
35
36/// Interface for trigger repository implementations
37///
38/// This trait defines the standard operations that any trigger repository must support,
39/// allowing for different storage backends while maintaining a consistent interface.
40#[async_trait]
41pub trait TriggerRepositoryTrait: Clone {
42	/// Create a new trigger repository from the given path
43	async fn new(path: Option<&Path>) -> Result<Self, RepositoryError>
44	where
45		Self: Sized;
46
47	/// Load all trigger configurations from the given path
48	///
49	/// If no path is provided, uses the default config directory.
50	/// This is a static method that doesn't require an instance.
51	async fn load_all(path: Option<&Path>) -> Result<HashMap<String, Trigger>, RepositoryError>;
52
53	/// Get a specific trigger by ID
54	///
55	/// Returns None if the trigger doesn't exist.
56	fn get(&self, trigger_id: &str) -> Option<Trigger>;
57
58	/// Get all triggers
59	///
60	/// Returns a copy of the trigger map to prevent external mutation.
61	fn get_all(&self) -> HashMap<String, Trigger>;
62}
63
64#[async_trait]
65impl TriggerRepositoryTrait for TriggerRepository {
66	async fn new(path: Option<&Path>) -> Result<Self, RepositoryError> {
67		TriggerRepository::new(path).await
68	}
69
70	async fn load_all(path: Option<&Path>) -> Result<HashMap<String, Trigger>, RepositoryError> {
71		Trigger::load_all(path).await.map_err(|e| {
72			RepositoryError::load_error(
73				"Failed to load triggers",
74				Some(Box::new(e)),
75				Some(HashMap::from([(
76					"path".to_string(),
77					path.map_or_else(|| "default".to_string(), |p| p.display().to_string()),
78				)])),
79			)
80		})
81	}
82
83	fn get(&self, trigger_id: &str) -> Option<Trigger> {
84		self.triggers.get(trigger_id).cloned()
85	}
86
87	fn get_all(&self) -> HashMap<String, Trigger> {
88		self.triggers.clone()
89	}
90}
91
92/// Service layer for trigger repository operations
93///
94/// This type provides a higher-level interface for working with trigger configurations,
95/// handling repository initialization and access through a trait-based interface.
96#[derive(Clone)]
97pub struct TriggerService<T: TriggerRepositoryTrait> {
98	repository: T,
99}
100
101impl<T: TriggerRepositoryTrait> TriggerService<T> {
102	/// Create a new trigger service with the default repository implementation
103	pub async fn new(
104		path: Option<&Path>,
105	) -> Result<TriggerService<TriggerRepository>, RepositoryError> {
106		let repository = TriggerRepository::new(path).await?;
107		Ok(TriggerService { repository })
108	}
109
110	/// Create a new trigger service with a custom repository implementation
111	pub fn new_with_repository(repository: T) -> Result<Self, RepositoryError> {
112		Ok(TriggerService { repository })
113	}
114
115	/// Create a new trigger service with a specific configuration path
116	pub async fn new_with_path(
117		path: Option<&Path>,
118	) -> Result<TriggerService<TriggerRepository>, RepositoryError> {
119		let repository = TriggerRepository::new(path).await?;
120		Ok(TriggerService { repository })
121	}
122
123	/// Get a specific trigger by ID
124	pub fn get(&self, trigger_id: &str) -> Option<Trigger> {
125		self.repository.get(trigger_id)
126	}
127
128	/// Get all triggers
129	pub fn get_all(&self) -> HashMap<String, Trigger> {
130		self.repository.get_all()
131	}
132}
133
134#[cfg(test)]
135mod tests {
136	use super::*;
137	use crate::repositories::error::RepositoryError;
138	use std::path::PathBuf;
139
140	#[tokio::test]
141	async fn test_load_error_messages() {
142		// Test with invalid path to trigger load error
143		let invalid_path = PathBuf::from("/non/existent/path");
144		let result = TriggerRepository::load_all(Some(&invalid_path)).await;
145		assert!(result.is_err());
146		let err = result.unwrap_err();
147		match err {
148			RepositoryError::LoadError(message) => {
149				assert!(message.to_string().contains("Failed to load triggers"));
150			}
151			_ => panic!("Expected RepositoryError::LoadError"),
152		}
153	}
154}