oar_ocr/core/config/
errors.rs

1//! Configuration error types and validation traits.
2
3use std::path::Path;
4use thiserror::Error;
5
6/// Errors that can occur during configuration validation.
7///
8/// This enum represents various errors that can occur when validating
9/// configuration parameters in the OCR pipeline.
10#[derive(Error, Debug)]
11pub enum ConfigError {
12    /// Error indicating that a batch size is invalid (must be greater than 0).
13    #[error("batch size must be greater than 0")]
14    InvalidBatchSize,
15
16    /// Error indicating that a model path does not exist.
17    #[error("model path does not exist: {path}")]
18    ModelPathNotFound { path: std::path::PathBuf },
19
20    /// Error indicating that a configuration is invalid.
21    #[error("invalid configuration: {message}")]
22    InvalidConfig { message: String },
23
24    /// Error indicating that validation failed.
25    #[error("validation failed: {message}")]
26    ValidationFailed { message: String },
27
28    /// Error indicating that a resource limit has been exceeded.
29    #[error("resource limit exceeded: {message}")]
30    ResourceLimitExceeded { message: String },
31}
32
33/// A trait for configuration types that can provide recommended defaults.
34///
35/// This trait complements ConfigValidator::get_defaults, allowing generic
36/// code to talk about defaults without depending on validation details.
37pub trait ConfigDefaults: Sized {
38    /// Return the recommended defaults for this configuration type.
39    fn defaults() -> Self;
40}
41
42// Blanket implementation: any ConfigValidator can provide defaults via get_defaults
43impl<T: ConfigValidator> ConfigDefaults for T {
44    fn defaults() -> Self {
45        T::get_defaults()
46    }
47}
48
49/// A trait for validating configuration parameters.
50///
51/// This trait provides methods for validating various configuration parameters
52/// used in the OCR pipeline, such as batch sizes, model paths, and image dimensions.
53pub trait ConfigValidator {
54    /// Validates the configuration.
55    ///
56    /// This method should be implemented by types that need to validate their configuration.
57    ///
58    /// # Returns
59    ///
60    /// A Result indicating success or a ConfigError if validation fails.
61    fn validate(&self) -> Result<(), ConfigError>;
62
63    /// Returns the default configuration.
64    ///
65    /// This method should be implemented by types that have default configuration values.
66    ///
67    /// # Returns
68    ///
69    /// The default configuration.
70    fn get_defaults() -> Self
71    where
72        Self: Sized;
73
74    /// Validates a batch size.
75    ///
76    /// This method checks that the batch size is greater than 0.
77    ///
78    /// # Arguments
79    ///
80    /// * `batch_size` - The batch size to validate.
81    ///
82    /// # Returns
83    ///
84    /// A Result indicating success or a ConfigError if validation fails.
85    fn validate_batch_size(&self, batch_size: usize) -> Result<(), ConfigError> {
86        if batch_size == 0 {
87            Err(ConfigError::InvalidBatchSize)
88        } else {
89            Ok(())
90        }
91    }
92
93    /// Validates a batch size against limits.
94    ///
95    /// This method checks that the batch size is greater than 0 and does not exceed
96    /// the maximum allowed batch size.
97    ///
98    /// # Arguments
99    ///
100    /// * `batch_size` - The batch size to validate.
101    /// * `max_batch_size` - The maximum allowed batch size.
102    ///
103    /// # Returns
104    ///
105    /// A Result indicating success or a ConfigError if validation fails.
106    fn validate_batch_size_with_limits(
107        &self,
108        batch_size: usize,
109        max_batch_size: usize,
110    ) -> Result<(), ConfigError> {
111        if batch_size == 0 {
112            return Err(ConfigError::InvalidBatchSize);
113        }
114        if batch_size > max_batch_size {
115            return Err(ConfigError::ResourceLimitExceeded {
116                message: format!(
117                    "Batch size {} exceeds maximum allowed batch size {}",
118                    batch_size, max_batch_size
119                ),
120            });
121        }
122        Ok(())
123    }
124
125    /// Validates a model path.
126    ///
127    /// This method checks that the model path exists and is a file.
128    ///
129    /// # Arguments
130    ///
131    /// * `path` - The path to validate.
132    ///
133    /// # Returns
134    ///
135    /// A Result indicating success or a ConfigError if validation fails.
136    fn validate_model_path(&self, path: &Path) -> Result<(), ConfigError> {
137        if !path.exists() {
138            Err(ConfigError::ModelPathNotFound {
139                path: path.to_path_buf(),
140            })
141        } else if !path.is_file() {
142            Err(ConfigError::InvalidConfig {
143                message: format!("Model path is not a file: {}", path.display()),
144            })
145        } else {
146            Ok(())
147        }
148    }
149
150    /// Validates image dimensions.
151    ///
152    /// This method checks that image dimensions are positive.
153    ///
154    /// # Arguments
155    ///
156    /// * `width` - The width to validate.
157    /// * `height` - The height to validate.
158    ///
159    /// # Returns
160    ///
161    /// A Result indicating success or a ConfigError if validation fails.
162    fn validate_image_dimensions(&self, width: u32, height: u32) -> Result<(), ConfigError> {
163        if width == 0 || height == 0 {
164            Err(ConfigError::InvalidConfig {
165                message: "Image dimensions must be positive".to_string(),
166            })
167        } else {
168            Ok(())
169        }
170    }
171
172    /// Validates a confidence threshold.
173    ///
174    /// This method checks that the confidence threshold is between 0.0 and 1.0.
175    ///
176    /// # Arguments
177    ///
178    /// * `threshold` - The threshold to validate.
179    ///
180    /// # Returns
181    ///
182    /// A Result indicating success or a ConfigError if validation fails.
183    fn validate_confidence_threshold(&self, threshold: f32) -> Result<(), ConfigError> {
184        if !(0.0..=1.0).contains(&threshold) {
185            Err(ConfigError::InvalidConfig {
186                message: format!(
187                    "Confidence threshold must be between 0.0 and 1.0, got {}",
188                    threshold
189                ),
190            })
191        } else {
192            Ok(())
193        }
194    }
195
196    /// Validates a memory limit.
197    ///
198    /// This method checks that the memory limit is reasonable.
199    ///
200    /// # Arguments
201    ///
202    /// * `limit_mb` - The memory limit in megabytes to validate.
203    ///
204    /// # Returns
205    ///
206    /// A Result indicating success or a ConfigError if validation fails.
207    fn validate_memory_limit(&self, limit_mb: usize) -> Result<(), ConfigError> {
208        const MAX_REASONABLE_MEMORY_MB: usize = 32 * 1024; // 32 GB
209
210        if limit_mb > MAX_REASONABLE_MEMORY_MB {
211            Err(ConfigError::ResourceLimitExceeded {
212                message: format!(
213                    "Memory limit {} MB exceeds reasonable maximum of {} MB",
214                    limit_mb, MAX_REASONABLE_MEMORY_MB
215                ),
216            })
217        } else {
218            Ok(())
219        }
220    }
221
222    /// Validates thread count.
223    ///
224    /// This method checks that the thread count is reasonable.
225    ///
226    /// # Arguments
227    ///
228    /// * `thread_count` - The thread count to validate.
229    ///
230    /// # Returns
231    ///
232    /// A Result indicating success or a ConfigError if validation fails.
233    fn validate_thread_count(&self, thread_count: usize) -> Result<(), ConfigError> {
234        const MAX_REASONABLE_THREADS: usize = 256;
235
236        if thread_count == 0 {
237            Err(ConfigError::InvalidConfig {
238                message: "Thread count must be greater than 0".to_string(),
239            })
240        } else if thread_count > MAX_REASONABLE_THREADS {
241            Err(ConfigError::ResourceLimitExceeded {
242                message: format!(
243                    "Thread count {} exceeds reasonable maximum of {}",
244                    thread_count, MAX_REASONABLE_THREADS
245                ),
246            })
247        } else {
248            Ok(())
249        }
250    }
251
252    /// Validates a float value is within a specified range.
253    ///
254    /// # Arguments
255    ///
256    /// * `value` - The value to validate.
257    /// * `min` - The minimum allowed value (inclusive).
258    /// * `max` - The maximum allowed value (inclusive).
259    /// * `field_name` - The name of the field being validated.
260    ///
261    /// # Returns
262    ///
263    /// A Result indicating success or a ConfigError if validation fails.
264    fn validate_f32_range(
265        &self,
266        value: f32,
267        min: f32,
268        max: f32,
269        field_name: &str,
270    ) -> Result<(), ConfigError> {
271        if value < min || value > max {
272            Err(ConfigError::InvalidConfig {
273                message: format!(
274                    "{} must be between {} and {}, got {}",
275                    field_name, min, max, value
276                ),
277            })
278        } else {
279            Ok(())
280        }
281    }
282
283    /// Validates a float value is positive.
284    ///
285    /// # Arguments
286    ///
287    /// * `value` - The value to validate.
288    /// * `field_name` - The name of the field being validated.
289    ///
290    /// # Returns
291    ///
292    /// A Result indicating success or a ConfigError if validation fails.
293    fn validate_positive_f32(&self, value: f32, field_name: &str) -> Result<(), ConfigError> {
294        if value <= 0.0 {
295            Err(ConfigError::InvalidConfig {
296                message: format!("{} must be greater than 0, got {}", field_name, value),
297            })
298        } else {
299            Ok(())
300        }
301    }
302
303    /// Validates a usize value is positive.
304    ///
305    /// # Arguments
306    ///
307    /// * `value` - The value to validate.
308    /// * `field_name` - The name of the field being validated.
309    ///
310    /// # Returns
311    ///
312    /// A Result indicating success or a ConfigError if validation fails.
313    fn validate_positive_usize(&self, value: usize, field_name: &str) -> Result<(), ConfigError> {
314        if value == 0 {
315            Err(ConfigError::InvalidConfig {
316                message: format!("{} must be greater than 0, got {}", field_name, value),
317            })
318        } else {
319            Ok(())
320        }
321    }
322}
323
324/// Provides a default implementation of ConfigValidator for any type.
325///
326/// This implementation provides basic validation methods that can be used
327/// by any type that implements ConfigValidator.
328pub struct DefaultValidator;
329
330impl ConfigValidator for DefaultValidator {
331    fn validate(&self) -> Result<(), ConfigError> {
332        Ok(())
333    }
334
335    fn get_defaults() -> Self {
336        DefaultValidator
337    }
338}
339
340/// Extension trait for ConfigValidator that provides error wrapping utilities.
341///
342/// This trait extends ConfigValidator to provide convenient methods for wrapping
343/// validation errors into OCRError types, reducing duplication across the codebase.
344pub trait ConfigValidatorExt: ConfigValidator {
345    /// Validates configuration and wraps any errors into OCRError::ConfigError.
346    ///
347    /// This method provides a convenient way to validate configuration and
348    /// automatically wrap any validation errors into the appropriate OCRError type.
349    /// This eliminates the repeated `config.validate().map_err(|e| OCRError::ConfigError { message: e.to_string() })`
350    /// pattern found throughout the codebase.
351    ///
352    /// # Returns
353    ///
354    /// A Result indicating success or an OCRError if validation fails.
355    fn validate_and_wrap_ocr_error(self) -> Result<Self, super::super::errors::OCRError>
356    where
357        Self: Sized,
358    {
359        self.validate()
360            .map_err(|e| super::super::errors::OCRError::ConfigError {
361                message: e.to_string(),
362            })?;
363        Ok(self)
364    }
365
366    /// Validates configuration and wraps any errors into a generic error.
367    ///
368    /// This method provides a convenient way to validate configuration when
369    /// working with generic error types.
370    ///
371    /// # Returns
372    ///
373    /// A Result indicating success or a wrapped error if validation fails.
374    fn validate_and_wrap_generic(self) -> Result<Self, Box<dyn std::error::Error + Send + Sync>>
375    where
376        Self: Sized,
377    {
378        self.validate()?;
379        Ok(self)
380    }
381}
382
383// Blanket implementation for all ConfigValidator types
384impl<T: ConfigValidator> ConfigValidatorExt for T {}
385
386impl From<ConfigError> for String {
387    /// Converts a ConfigError to a String.
388    ///
389    /// This allows ConfigError to be converted to a String representation.
390    fn from(error: ConfigError) -> Self {
391        error.to_string()
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    struct TestValidator;
400    impl ConfigValidator for TestValidator {
401        fn validate(&self) -> Result<(), ConfigError> {
402            Ok(())
403        }
404
405        fn get_defaults() -> Self {
406            TestValidator
407        }
408    }
409
410    #[test]
411    fn test_validate_batch_size() {
412        let validator = TestValidator;
413        assert!(validator.validate_batch_size(1).is_ok());
414        assert!(validator.validate_batch_size(10).is_ok());
415        assert!(validator.validate_batch_size(0).is_err());
416    }
417
418    #[test]
419    fn test_validate_image_dimensions() {
420        let validator = TestValidator;
421        assert!(validator.validate_image_dimensions(100, 100).is_ok());
422        assert!(validator.validate_image_dimensions(1, 1).is_ok());
423        assert!(validator.validate_image_dimensions(0, 100).is_err());
424        assert!(validator.validate_image_dimensions(100, 0).is_err());
425        assert!(validator.validate_image_dimensions(0, 0).is_err());
426    }
427
428    #[test]
429    fn test_validate_confidence_threshold() {
430        let validator = TestValidator;
431        assert!(validator.validate_confidence_threshold(0.0).is_ok());
432        assert!(validator.validate_confidence_threshold(0.5).is_ok());
433        assert!(validator.validate_confidence_threshold(1.0).is_ok());
434        assert!(validator.validate_confidence_threshold(-0.1).is_err());
435        assert!(validator.validate_confidence_threshold(1.1).is_err());
436    }
437
438    #[test]
439    fn test_validate_memory_limit() {
440        let validator = TestValidator;
441        assert!(validator.validate_memory_limit(1024).is_ok());
442        assert!(validator.validate_memory_limit(16 * 1024).is_ok());
443        assert!(validator.validate_memory_limit(64 * 1024).is_err());
444    }
445
446    #[test]
447    fn test_validate_thread_count() {
448        let validator = TestValidator;
449        assert!(validator.validate_thread_count(1).is_ok());
450        assert!(validator.validate_thread_count(8).is_ok());
451        assert!(validator.validate_thread_count(64).is_ok());
452        assert!(validator.validate_thread_count(0).is_err());
453        assert!(validator.validate_thread_count(512).is_err());
454    }
455
456    #[test]
457    fn test_config_error_to_string() {
458        let error = ConfigError::InvalidBatchSize;
459        let error_string: String = error.into();
460        assert_eq!(error_string, "batch size must be greater than 0");
461    }
462}