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}