ih_muse_core/
config.rs

1// crates/ih-muse/src/config.rs
2
3use serde::{Deserialize, Serialize};
4
5use super::{MuseError, MuseResult};
6use ih_muse_proto::{ElementKindRegistration, MetricDefinition, TimestampResolution};
7use tokio::time::Duration;
8
9/// Specifies the type of client to use with the Muse system.
10///
11/// - `Poet`: Communicates with the Poet service.
12/// - `Mock`: Uses a mock client for testing purposes.
13#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
14pub enum ClientType {
15    /// Communicates with the Poet service.
16    Poet,
17    /// Uses a mock client for testing.
18    Mock,
19}
20
21/// Configuration settings for the Muse client.
22///
23/// The `Config` struct contains all necessary parameters to initialize the Muse client.
24#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
25pub struct Config {
26    /// List of endpoint URLs for the Muse client.
27    pub endpoints: Vec<String>,
28    /// The type of client to use (`Poet` or `Mock`).
29    pub client_type: ClientType,
30    /// Enables event recording if set to `true`.
31    pub recording_enabled: bool,
32    /// File path for recording events (required if `recording_enabled` is `true`).
33    pub recording_path: Option<String>,
34    /// Interval for flushing the recorder when recording is enabled.
35    pub recording_flush_interval: Option<Duration>,
36    /// Default timestamp resolution for metrics.
37    pub default_resolution: TimestampResolution,
38    /// List of element kinds to register upon initialization.
39    pub element_kinds: Vec<ElementKindRegistration>,
40    /// List of metric definitions available for reporting.
41    pub metric_definitions: Vec<MetricDefinition>,
42    /// Interval for initialization tasks (optional).
43    pub initialization_interval: Option<Duration>,
44    /// Interval for cluster monitoring tasks (optional).
45    pub cluster_monitor_interval: Option<Duration>,
46    /// Maximum number of retries for element registration.
47    pub max_reg_elem_retries: usize,
48}
49
50impl Config {
51    /// Creates a new `Config` instance with the provided settings.
52    ///
53    /// # Arguments
54    ///
55    /// - `endpoints`: A vector of endpoint URLs.
56    /// - `client_type`: The client type to use.
57    /// - `recording_enabled`: Enables event recording.
58    /// - `recording_path`: File path for recording events.
59    /// - `recording_flush_interval`: Interval to flush recording events.
60    /// - `default_resolution`: Default timestamp resolution.
61    /// - `element_kinds`: Element kinds to register.
62    /// - `metric_definitions`: Metric definitions for reporting.
63    /// - `initialization_interval`: Interval for the initialization task.
64    /// - `cluster_monitor_interval`: Interval for cluster monitoring.
65    /// - `max_reg_elem_retries`: Max retries for element registration.
66    ///
67    /// # Errors
68    ///
69    /// Returns a [`MuseError::Configuration`] if validation fails.
70    ///
71    /// # Examples
72    ///
73    /// ```rust
74    /// use ih_muse_core::prelude::*;
75    /// use ih_muse_proto::prelude::*;
76    ///
77    /// let config = Config::new(
78    ///     vec!["http://localhost:8080".to_string()],
79    ///     ClientType::Poet,
80    ///     false,
81    ///     None,
82    ///     None,
83    ///     TimestampResolution::Milliseconds,
84    ///     vec![ElementKindRegistration::new("kind_code", Some("parent_code"), "kind_name", "description")],
85    ///     vec![MetricDefinition::new("metric_code", "metric_name", "description")],
86    ///     Some(std::time::Duration::from_secs(60)),
87    ///     Some(std::time::Duration::from_secs(60)),
88    ///     3,
89    /// ).expect("Failed to create config");
90    /// ```
91    #[allow(clippy::too_many_arguments)]
92    pub fn new(
93        endpoints: Vec<String>,
94        client_type: ClientType,
95        recording_enabled: bool,
96        recording_path: Option<String>,
97        recording_flush_interval: Option<Duration>,
98        default_resolution: TimestampResolution,
99        element_kinds: Vec<ElementKindRegistration>,
100        metric_definitions: Vec<MetricDefinition>,
101        initialization_interval: Option<Duration>,
102        cluster_monitor_interval: Option<Duration>,
103        max_reg_elem_retries: usize,
104    ) -> MuseResult<Self> {
105        let config = Self {
106            endpoints,
107            client_type,
108            recording_enabled,
109            recording_path,
110            recording_flush_interval,
111            default_resolution,
112            element_kinds,
113            metric_definitions,
114            initialization_interval,
115            cluster_monitor_interval,
116            max_reg_elem_retries,
117        };
118        config.validate()?;
119        Ok(config)
120    }
121
122    /// Validates the configuration settings.
123    ///
124    /// Ensures all required fields are properly set.
125    ///
126    /// # Errors
127    ///
128    /// Returns a [`MuseError::Configuration`] if any validation check fails.
129    pub fn validate(&self) -> MuseResult<()> {
130        if self.client_type == ClientType::Poet && self.endpoints.is_empty() {
131            return Err(MuseError::Configuration(
132                "At least one endpoint needs to be specified for Poet client.".to_string(),
133            ));
134        }
135        if self.element_kinds.is_empty() {
136            return Err(MuseError::Configuration(
137                "Element kinds cannot be empty.".to_string(),
138            ));
139        }
140        if self.metric_definitions.is_empty() {
141            return Err(MuseError::Configuration(
142                "Metric definitions cannot be empty.".to_string(),
143            ));
144        }
145        if self.recording_enabled && self.recording_path.is_none() {
146            return Err(MuseError::Configuration(
147                "Recording enabled without a file path.".to_string(),
148            ));
149        }
150        Ok(())
151    }
152
153    /// Compares two configurations and returns a vector of strings describing the differences.
154    ///
155    /// # Arguments
156    /// * `other` - The configuration to compare with.
157    ///
158    /// # Returns
159    /// A vector of strings where each entry describes a difference between the two configurations.
160    pub fn diff(&self, other: &Config) -> Vec<String> {
161        let mut differences = Vec::new();
162
163        if self.endpoints != other.endpoints {
164            differences.push(format!(
165                "endpoints: {:?} != {:?}",
166                self.endpoints, other.endpoints
167            ));
168        }
169
170        if self.client_type != other.client_type {
171            differences.push(format!(
172                "client_type: {:?} != {:?}",
173                self.client_type, other.client_type
174            ));
175        }
176
177        if self.recording_enabled != other.recording_enabled {
178            differences.push(format!(
179                "recording_enabled: {} != {}",
180                self.recording_enabled, other.recording_enabled
181            ));
182        }
183
184        if self.recording_path != other.recording_path {
185            differences.push(format!(
186                "recording_path: {:?} != {:?}",
187                self.recording_path, other.recording_path
188            ));
189        }
190
191        if self.default_resolution != other.default_resolution {
192            differences.push(format!(
193                "default_resolution: {:?} != {:?}",
194                self.default_resolution, other.default_resolution
195            ));
196        }
197
198        if self.element_kinds != other.element_kinds {
199            differences.push(format!(
200                "element_kinds: {:?} != {:?}",
201                self.element_kinds, other.element_kinds
202            ));
203        }
204
205        if self.metric_definitions != other.metric_definitions {
206            differences.push(format!(
207                "metric_definitions: {:?} != {:?}",
208                self.metric_definitions, other.metric_definitions
209            ));
210        }
211
212        if self.cluster_monitor_interval != other.cluster_monitor_interval {
213            differences.push(format!(
214                "cluster_monitor_interval: {:?} != {:?}",
215                self.cluster_monitor_interval, other.cluster_monitor_interval
216            ));
217        }
218
219        if self.max_reg_elem_retries != other.max_reg_elem_retries {
220            differences.push(format!(
221                "max_reg_elem_retries: {} != {}",
222                self.max_reg_elem_retries, other.max_reg_elem_retries
223            ));
224        }
225
226        differences
227    }
228
229    /// Helper function to pretty-print the differences as a single string.
230    ///
231    /// # Arguments
232    /// * `other` - The configuration to compare with.
233    ///
234    /// # Returns
235    /// A single string containing all differences separated by newlines.
236    pub fn pretty_diff(&self, other: &Config) -> String {
237        self.diff(other).join("\n")
238    }
239
240    /// Checks if two Config instances are equivalent based on relevant fields,
241    /// ignoring `recording_enabled` and `recording_path`.
242    ///
243    /// # Arguments
244    /// - `other`: The `Config` instance to compare with.
245    ///
246    /// # Returns
247    /// `true` if the relevant fields are equal, `false` otherwise.
248    pub fn is_relevantly_equal(&self, other: &Config) -> bool {
249        self.endpoints == other.endpoints
250            && self.client_type == other.client_type
251            && self.default_resolution == other.default_resolution
252            && self.element_kinds == other.element_kinds
253            && self.metric_definitions == other.metric_definitions
254            && self.cluster_monitor_interval == other.cluster_monitor_interval
255            && self.max_reg_elem_retries == other.max_reg_elem_retries
256    }
257}
258
259#[cfg(test)]
260mod tests {
261
262    use super::*;
263
264    #[test]
265    fn test_relevant_config_comparison() {
266        let config1 = Config {
267            endpoints: vec!["http://localhost:8000".to_string()],
268            client_type: ClientType::Poet,
269            recording_enabled: true, // Ignored for comparison
270            recording_path: Some("recording.json".to_string()), // Ignored for comparison
271            recording_flush_interval: Some(Duration::from_millis(1)),
272            default_resolution: TimestampResolution::Milliseconds,
273            element_kinds: vec![ElementKindRegistration::new(
274                "kind_code",
275                None,
276                "kind_name",
277                "desc",
278            )],
279            metric_definitions: vec![MetricDefinition::new("metric_code", "metric_name", "desc")],
280            initialization_interval: Some(Duration::from_secs(60)),
281            cluster_monitor_interval: Some(Duration::from_secs(60)),
282            max_reg_elem_retries: 3,
283        };
284
285        let config2 = Config {
286            endpoints: vec!["http://localhost:8000".to_string()],
287            client_type: ClientType::Poet,
288            recording_enabled: false,
289            recording_path: None,
290            recording_flush_interval: None,
291            default_resolution: TimestampResolution::Milliseconds,
292            element_kinds: vec![ElementKindRegistration::new(
293                "kind_code",
294                None,
295                "kind_name",
296                "desc",
297            )],
298            metric_definitions: vec![MetricDefinition::new("metric_code", "metric_name", "desc")],
299            initialization_interval: Some(Duration::from_secs(60)),
300            cluster_monitor_interval: Some(Duration::from_secs(60)),
301            max_reg_elem_retries: 3,
302        };
303
304        assert!(
305            config1.is_relevantly_equal(&config2),
306            "Configs should be considered equal based on relevant fields"
307        );
308    }
309}