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}