Skip to main content

ccxt_core/error/
config.rs

1//! Configuration validation error types.
2//!
3//! This module provides error types for configuration validation, allowing
4//! developers to catch invalid configurations early with clear error messages.
5//!
6//! # Example
7//!
8//! ```rust
9//! use ccxt_core::error::{ConfigValidationError, ValidationResult};
10//!
11//! fn validate_max_retries(value: u32) -> Result<ValidationResult, ConfigValidationError> {
12//!     if value > 10 {
13//!         return Err(ConfigValidationError::ValueTooHigh {
14//!             field: "max_retries",
15//!             value: value.to_string(),
16//!             max: "10".to_string(),
17//!         });
18//!     }
19//!     Ok(ValidationResult::new())
20//! }
21//! ```
22
23use std::fmt;
24use thiserror::Error;
25
26/// Configuration validation error types.
27///
28/// This enum represents different types of validation failures that can occur
29/// when validating configuration parameters. Each variant includes the field
30/// name and relevant values for debugging.
31///
32/// # Example
33///
34/// ```rust
35/// use ccxt_core::error::ConfigValidationError;
36///
37/// let err = ConfigValidationError::ValueTooHigh {
38///     field: "max_retries",
39///     value: "15".to_string(),
40///     max: "10".to_string(),
41/// };
42/// assert!(err.to_string().contains("max_retries"));
43/// assert!(err.to_string().contains("15"));
44/// ```
45#[derive(Error, Debug, Clone, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum ConfigValidationError {
48    /// Field value exceeds the maximum allowed value.
49    #[error("Field '{field}' value {value} exceeds maximum {max}")]
50    ValueTooHigh {
51        /// The name of the configuration field
52        field: &'static str,
53        /// The actual value that was provided
54        value: String,
55        /// The maximum allowed value
56        max: String,
57    },
58
59    /// Field value is below the minimum allowed value.
60    #[error("Field '{field}' value {value} is below minimum {min}")]
61    ValueTooLow {
62        /// The name of the configuration field
63        field: &'static str,
64        /// The actual value that was provided
65        value: String,
66        /// The minimum allowed value
67        min: String,
68    },
69
70    /// Field value is invalid for reasons other than range.
71    #[error("Field '{field}' has invalid value: {reason}")]
72    ValueInvalid {
73        /// The name of the configuration field
74        field: &'static str,
75        /// The reason why the value is invalid
76        reason: String,
77    },
78
79    /// Required field is missing.
80    #[error("Required field '{field}' is missing")]
81    ValueMissing {
82        /// The name of the missing configuration field
83        field: &'static str,
84    },
85}
86
87impl ConfigValidationError {
88    /// Returns the field name associated with this error.
89    ///
90    /// # Example
91    ///
92    /// ```rust
93    /// use ccxt_core::error::ConfigValidationError;
94    ///
95    /// let err = ConfigValidationError::ValueTooHigh {
96    ///     field: "max_retries",
97    ///     value: "15".to_string(),
98    ///     max: "10".to_string(),
99    /// };
100    /// assert_eq!(err.field_name(), "max_retries");
101    /// ```
102    #[must_use]
103    pub fn field_name(&self) -> &'static str {
104        match self {
105            ConfigValidationError::ValueTooHigh { field, .. }
106            | ConfigValidationError::ValueTooLow { field, .. }
107            | ConfigValidationError::ValueInvalid { field, .. }
108            | ConfigValidationError::ValueMissing { field } => field,
109        }
110    }
111
112    /// Creates a new `ValueTooHigh` error.
113    ///
114    /// # Example
115    ///
116    /// ```rust
117    /// use ccxt_core::error::ConfigValidationError;
118    ///
119    /// let err = ConfigValidationError::too_high("max_retries", 15, 10);
120    /// assert!(err.to_string().contains("max_retries"));
121    /// ```
122    pub fn too_high<V: fmt::Display, M: fmt::Display>(
123        field: &'static str,
124        value: V,
125        max: M,
126    ) -> Self {
127        ConfigValidationError::ValueTooHigh {
128            field,
129            value: value.to_string(),
130            max: max.to_string(),
131        }
132    }
133
134    /// Creates a new `ValueTooLow` error.
135    ///
136    /// # Example
137    ///
138    /// ```rust
139    /// use ccxt_core::error::ConfigValidationError;
140    ///
141    /// let err = ConfigValidationError::too_low("base_delay_ms", 5, 10);
142    /// assert!(err.to_string().contains("base_delay_ms"));
143    /// ```
144    pub fn too_low<V: fmt::Display, M: fmt::Display>(
145        field: &'static str,
146        value: V,
147        min: M,
148    ) -> Self {
149        ConfigValidationError::ValueTooLow {
150            field,
151            value: value.to_string(),
152            min: min.to_string(),
153        }
154    }
155
156    /// Creates a new `ValueInvalid` error.
157    ///
158    /// # Example
159    ///
160    /// ```rust
161    /// use ccxt_core::error::ConfigValidationError;
162    ///
163    /// let err = ConfigValidationError::invalid("capacity", "capacity cannot be zero");
164    /// assert!(err.to_string().contains("capacity"));
165    /// ```
166    pub fn invalid(field: &'static str, reason: impl Into<String>) -> Self {
167        ConfigValidationError::ValueInvalid {
168            field,
169            reason: reason.into(),
170        }
171    }
172
173    /// Creates a new `ValueMissing` error.
174    ///
175    /// # Example
176    ///
177    /// ```rust
178    /// use ccxt_core::error::ConfigValidationError;
179    ///
180    /// let err = ConfigValidationError::missing("api_key");
181    /// assert!(err.to_string().contains("api_key"));
182    /// ```
183    pub fn missing(field: &'static str) -> Self {
184        ConfigValidationError::ValueMissing { field }
185    }
186}
187
188/// Result of a successful configuration validation.
189///
190/// This struct contains any warnings that were generated during validation.
191/// Warnings indicate potential issues that don't prevent the configuration
192/// from being used, but may cause suboptimal behavior.
193///
194/// # Example
195///
196/// ```rust
197/// use ccxt_core::error::ValidationResult;
198///
199/// let mut result = ValidationResult::new();
200/// result.add_warning("refill_period is very short, may cause high CPU usage");
201/// assert!(!result.warnings.is_empty());
202/// ```
203#[derive(Debug, Clone, Default, PartialEq, Eq)]
204pub struct ValidationResult {
205    /// Warnings generated during validation.
206    ///
207    /// These are non-fatal issues that the user should be aware of.
208    pub warnings: Vec<String>,
209}
210
211impl ValidationResult {
212    /// Creates a new empty validation result.
213    ///
214    /// # Example
215    ///
216    /// ```rust
217    /// use ccxt_core::error::ValidationResult;
218    ///
219    /// let result = ValidationResult::new();
220    /// assert!(result.warnings.is_empty());
221    /// ```
222    #[must_use]
223    pub fn new() -> Self {
224        Self {
225            warnings: Vec::new(),
226        }
227    }
228
229    /// Creates a validation result with the given warnings.
230    ///
231    /// # Example
232    ///
233    /// ```rust
234    /// use ccxt_core::error::ValidationResult;
235    ///
236    /// let result = ValidationResult::with_warnings(vec![
237    ///     "Warning 1".to_string(),
238    ///     "Warning 2".to_string(),
239    /// ]);
240    /// assert_eq!(result.warnings.len(), 2);
241    /// ```
242    #[must_use]
243    pub fn with_warnings(warnings: Vec<String>) -> Self {
244        Self { warnings }
245    }
246
247    /// Adds a warning to the validation result.
248    ///
249    /// # Example
250    ///
251    /// ```rust
252    /// use ccxt_core::error::ValidationResult;
253    ///
254    /// let mut result = ValidationResult::new();
255    /// result.add_warning("This is a warning");
256    /// assert_eq!(result.warnings.len(), 1);
257    /// ```
258    pub fn add_warning(&mut self, warning: impl Into<String>) {
259        self.warnings.push(warning.into());
260    }
261
262    /// Returns `true` if there are no warnings.
263    ///
264    /// # Example
265    ///
266    /// ```rust
267    /// use ccxt_core::error::ValidationResult;
268    ///
269    /// let result = ValidationResult::new();
270    /// assert!(result.is_ok());
271    /// ```
272    #[must_use]
273    pub fn is_ok(&self) -> bool {
274        self.warnings.is_empty()
275    }
276
277    /// Returns `true` if there are any warnings.
278    ///
279    /// # Example
280    ///
281    /// ```rust
282    /// use ccxt_core::error::ValidationResult;
283    ///
284    /// let mut result = ValidationResult::new();
285    /// result.add_warning("Warning");
286    /// assert!(result.has_warnings());
287    /// ```
288    #[must_use]
289    pub fn has_warnings(&self) -> bool {
290        !self.warnings.is_empty()
291    }
292
293    /// Merges another validation result into this one.
294    ///
295    /// # Example
296    ///
297    /// ```rust
298    /// use ccxt_core::error::ValidationResult;
299    ///
300    /// let mut result1 = ValidationResult::new();
301    /// result1.add_warning("Warning 1");
302    ///
303    /// let mut result2 = ValidationResult::new();
304    /// result2.add_warning("Warning 2");
305    ///
306    /// result1.merge(result2);
307    /// assert_eq!(result1.warnings.len(), 2);
308    /// ```
309    pub fn merge(&mut self, other: ValidationResult) {
310        self.warnings.extend(other.warnings);
311    }
312}
313
314#[cfg(test)]
315#[allow(clippy::single_char_pattern)] // "5" is acceptable in tests
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_value_too_high_display() {
321        let err = ConfigValidationError::ValueTooHigh {
322            field: "max_retries",
323            value: "15".to_string(),
324            max: "10".to_string(),
325        };
326        let msg = err.to_string();
327        assert!(msg.contains("max_retries"));
328        assert!(msg.contains("15"));
329        assert!(msg.contains("10"));
330    }
331
332    #[test]
333    fn test_value_too_low_display() {
334        let err = ConfigValidationError::ValueTooLow {
335            field: "base_delay_ms",
336            value: "5".to_string(),
337            min: "10".to_string(),
338        };
339        let msg = err.to_string();
340        assert!(msg.contains("base_delay_ms"));
341        assert!(msg.contains("5"));
342        assert!(msg.contains("10"));
343    }
344
345    #[test]
346    fn test_value_invalid_display() {
347        let err = ConfigValidationError::ValueInvalid {
348            field: "capacity",
349            reason: "capacity cannot be zero".to_string(),
350        };
351        let msg = err.to_string();
352        assert!(msg.contains("capacity"));
353        assert!(msg.contains("cannot be zero"));
354    }
355
356    #[test]
357    fn test_value_missing_display() {
358        let err = ConfigValidationError::ValueMissing { field: "api_key" };
359        let msg = err.to_string();
360        assert!(msg.contains("api_key"));
361        assert!(msg.contains("missing"));
362    }
363
364    #[test]
365    fn test_field_name() {
366        let err1 = ConfigValidationError::too_high("max_retries", 15, 10);
367        assert_eq!(err1.field_name(), "max_retries");
368
369        let err2 = ConfigValidationError::too_low("base_delay_ms", 5, 10);
370        assert_eq!(err2.field_name(), "base_delay_ms");
371
372        let err3 = ConfigValidationError::invalid("capacity", "cannot be zero");
373        assert_eq!(err3.field_name(), "capacity");
374
375        let err4 = ConfigValidationError::missing("api_key");
376        assert_eq!(err4.field_name(), "api_key");
377    }
378
379    #[test]
380    fn test_helper_constructors() {
381        let err1 = ConfigValidationError::too_high("field1", 100, 50);
382        assert!(matches!(err1, ConfigValidationError::ValueTooHigh { .. }));
383
384        let err2 = ConfigValidationError::too_low("field2", 5, 10);
385        assert!(matches!(err2, ConfigValidationError::ValueTooLow { .. }));
386
387        let err3 = ConfigValidationError::invalid("field3", "invalid reason");
388        assert!(matches!(err3, ConfigValidationError::ValueInvalid { .. }));
389
390        let err4 = ConfigValidationError::missing("field4");
391        assert!(matches!(err4, ConfigValidationError::ValueMissing { .. }));
392    }
393
394    #[test]
395    fn test_validation_result_new() {
396        let result = ValidationResult::new();
397        assert!(result.warnings.is_empty());
398        assert!(result.is_ok());
399        assert!(!result.has_warnings());
400    }
401
402    #[test]
403    fn test_validation_result_with_warnings() {
404        let result =
405            ValidationResult::with_warnings(vec!["Warning 1".to_string(), "Warning 2".to_string()]);
406        assert_eq!(result.warnings.len(), 2);
407        assert!(!result.is_ok());
408        assert!(result.has_warnings());
409    }
410
411    #[test]
412    fn test_validation_result_add_warning() {
413        let mut result = ValidationResult::new();
414        result.add_warning("Test warning");
415        assert_eq!(result.warnings.len(), 1);
416        assert_eq!(result.warnings[0], "Test warning");
417    }
418
419    #[test]
420    fn test_validation_result_merge() {
421        let mut result1 = ValidationResult::new();
422        result1.add_warning("Warning 1");
423
424        let mut result2 = ValidationResult::new();
425        result2.add_warning("Warning 2");
426        result2.add_warning("Warning 3");
427
428        result1.merge(result2);
429        assert_eq!(result1.warnings.len(), 3);
430    }
431
432    #[test]
433    fn test_config_validation_error_is_error() {
434        // Verify that ConfigValidationError implements std::error::Error
435        fn assert_error<E: std::error::Error>() {}
436        assert_error::<ConfigValidationError>();
437    }
438
439    #[test]
440    fn test_config_validation_error_clone() {
441        let err = ConfigValidationError::too_high("field", 100, 50);
442        let cloned = err.clone();
443        assert_eq!(err, cloned);
444    }
445}