Skip to main content

exif_oxide/processor_registry/
capability.rs

1//! ProcessorCapability assessment system
2//!
3//! This module provides the capability assessment framework that enables the
4//! registry to select the most appropriate processor for given data and context.
5
6/// Assessment of a processor's capability to handle specific data
7///
8/// This enum provides a nuanced way for processors to indicate how well they
9/// can handle particular data, enabling the registry to make optimal selections
10/// while maintaining fallback options.
11///
12/// ## ExifTool Reference
13///
14/// ExifTool uses conditional expressions to determine processor applicability:
15/// ```perl
16/// {
17///     Condition => '$$self{Model} =~ /EOS R5/',  # Perfect match
18///     SubDirectory => { ProcessProc => \&ProcessCanonSerialDataMkII }
19/// },
20/// {
21///     Condition => '$$self{Make} eq "Canon"',    # Good match
22///     SubDirectory => { ProcessProc => \&ProcessCanonSerialData }
23/// }
24/// ```
25///
26/// This enum captures these preference levels explicitly.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum ProcessorCapability {
29    /// Exact match for this data - highest priority
30    ///
31    /// Use this when the processor is specifically designed for the exact
32    /// combination of manufacturer, model, format version, etc.
33    ///
34    /// Example: Canon EOS R5 serial data processor when processing EOS R5 data
35    Perfect,
36
37    /// Compatible and good choice - second priority
38    ///
39    /// Use this when the processor is designed for this manufacturer/format
40    /// but may not be the most specific option available.
41    ///
42    /// Example: Generic Canon processor when processing Canon data
43    Good,
44
45    /// Can handle but not optimal - third priority
46    ///
47    /// Use this when the processor can handle the data but it's not its
48    /// primary purpose. Often used by generic processors as fallbacks.
49    ///
50    /// Example: Generic EXIF processor when processing manufacturer data
51    Fallback,
52
53    /// Cannot process this data - will be filtered out
54    ///
55    /// Use this when the processor cannot handle the data due to format
56    /// incompatibility, missing required context, etc.
57    Incompatible,
58}
59
60impl PartialOrd for ProcessorCapability {
61    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
62        Some(self.cmp(other))
63    }
64}
65
66impl Ord for ProcessorCapability {
67    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
68        // Higher priority values are "greater"
69        self.priority_score().cmp(&other.priority_score())
70    }
71}
72
73impl ProcessorCapability {
74    /// Check if this capability indicates the processor can handle the data
75    pub fn is_compatible(&self) -> bool {
76        match self {
77            ProcessorCapability::Perfect => true,
78            ProcessorCapability::Good => true,
79            ProcessorCapability::Fallback => true,
80            ProcessorCapability::Incompatible => false,
81        }
82    }
83
84    /// Get priority score for sorting (higher is better)
85    pub fn priority_score(&self) -> u8 {
86        match self {
87            ProcessorCapability::Perfect => 100,
88            ProcessorCapability::Good => 75,
89            ProcessorCapability::Fallback => 25,
90            ProcessorCapability::Incompatible => 0,
91        }
92    }
93
94    /// Get human-readable description
95    pub fn description(&self) -> &'static str {
96        match self {
97            ProcessorCapability::Perfect => "Perfect match - optimal processor for this data",
98            ProcessorCapability::Good => "Good match - compatible and recommended",
99            ProcessorCapability::Fallback => "Fallback option - can process but not optimal",
100            ProcessorCapability::Incompatible => "Incompatible - cannot process this data",
101        }
102    }
103
104    /// Combine multiple capabilities to get overall assessment
105    ///
106    /// This is useful when a processor evaluates multiple criteria and needs
107    /// to provide an overall capability assessment.
108    pub fn combine(capabilities: &[ProcessorCapability]) -> ProcessorCapability {
109        if capabilities.is_empty() {
110            return ProcessorCapability::Incompatible;
111        }
112
113        // If any are incompatible, the whole assessment is incompatible
114        if capabilities.contains(&ProcessorCapability::Incompatible) {
115            return ProcessorCapability::Incompatible;
116        }
117
118        // Return the lowest (worst) capability that's still compatible
119        capabilities
120            .iter()
121            .min_by_key(|c| c.priority_score())
122            .cloned()
123            .unwrap_or(ProcessorCapability::Incompatible)
124    }
125
126    /// Create capability based on boolean conditions
127    ///
128    /// Helper for simple processors that just need to check if they're compatible.
129    pub fn from_boolean(is_compatible: bool) -> ProcessorCapability {
130        if is_compatible {
131            ProcessorCapability::Good
132        } else {
133            ProcessorCapability::Incompatible
134        }
135    }
136
137    /// Create capability with manufacturer and model specificity
138    ///
139    /// Helper for manufacturer-specific processors that want to indicate
140    /// different capability levels based on specificity.
141    pub fn from_specificity(
142        manufacturer_matches: bool,
143        model_matches: bool,
144        is_primary_purpose: bool,
145    ) -> ProcessorCapability {
146        if !manufacturer_matches {
147            return ProcessorCapability::Incompatible;
148        }
149
150        match (model_matches, is_primary_purpose) {
151            (true, true) => ProcessorCapability::Perfect,
152            (true, false) => ProcessorCapability::Good,
153            (false, true) => ProcessorCapability::Good,
154            (false, false) => ProcessorCapability::Fallback,
155        }
156    }
157}
158
159/// Assessment details for debugging and analysis
160///
161/// This structure provides detailed information about why a processor
162/// returned a particular capability assessment, useful for debugging
163/// processor selection decisions.
164#[derive(Debug, Clone)]
165pub struct CapabilityAssessment {
166    /// The capability level assessed
167    pub capability: ProcessorCapability,
168
169    /// Explanation of why this capability was assigned
170    pub reason: String,
171
172    /// Factors that contributed to the assessment
173    pub factors: Vec<CapabilityFactor>,
174
175    /// Missing requirements that prevented higher capability
176    pub missing_requirements: Vec<String>,
177}
178
179impl CapabilityAssessment {
180    /// Create a new capability assessment
181    pub fn new(capability: ProcessorCapability, reason: String) -> Self {
182        Self {
183            capability,
184            reason,
185            factors: Vec::new(),
186            missing_requirements: Vec::new(),
187        }
188    }
189
190    /// Add a contributing factor
191    pub fn add_factor(mut self, factor: CapabilityFactor) -> Self {
192        self.factors.push(factor);
193        self
194    }
195
196    /// Add a missing requirement
197    pub fn add_missing_requirement(mut self, requirement: String) -> Self {
198        self.missing_requirements.push(requirement);
199        self
200    }
201
202    /// Get a detailed explanation including all factors
203    pub fn detailed_explanation(&self) -> String {
204        let mut explanation = format!("{}: {}", self.capability.description(), self.reason);
205
206        if !self.factors.is_empty() {
207            explanation.push_str("\nFactors:");
208            for factor in &self.factors {
209                explanation.push_str(&format!("\n  - {}", factor.description()));
210            }
211        }
212
213        if !self.missing_requirements.is_empty() {
214            explanation.push_str("\nMissing requirements:");
215            for req in &self.missing_requirements {
216                explanation.push_str(&format!("\n  - {req}"));
217            }
218        }
219
220        explanation
221    }
222}
223
224/// Individual factors that contribute to capability assessment
225///
226/// These represent specific checks or conditions that a processor
227/// evaluates when determining its capability level.
228#[derive(Debug, Clone)]
229pub enum CapabilityFactor {
230    /// Manufacturer matches processor's target
231    ManufacturerMatch(String),
232
233    /// Model matches processor's target
234    ModelMatch(String),
235
236    /// Format version is supported
237    FormatVersionSupported(String),
238
239    /// Required context field is available
240    RequiredContextAvailable(String),
241
242    /// Table name matches processor's scope
243    TableNameMatch(String),
244
245    /// Data pattern matches expected format
246    DataPatternMatch(String),
247
248    /// Custom condition result
249    CustomCondition(String, bool),
250}
251
252impl CapabilityFactor {
253    /// Get human-readable description of this factor
254    pub fn description(&self) -> String {
255        match self {
256            CapabilityFactor::ManufacturerMatch(manufacturer) => {
257                format!("Manufacturer '{manufacturer}' matches processor target")
258            }
259            CapabilityFactor::ModelMatch(model) => {
260                format!("Model '{model}' matches processor target")
261            }
262            CapabilityFactor::FormatVersionSupported(version) => {
263                format!("Format version '{version}' is supported")
264            }
265            CapabilityFactor::RequiredContextAvailable(field) => {
266                format!("Required context field '{field}' is available")
267            }
268            CapabilityFactor::TableNameMatch(table) => {
269                format!("Table name '{table}' matches processor scope")
270            }
271            CapabilityFactor::DataPatternMatch(pattern) => {
272                format!("Data matches expected pattern '{pattern}'")
273            }
274            CapabilityFactor::CustomCondition(condition, result) => {
275                format!(
276                    "Custom condition '{}': {}",
277                    condition,
278                    if *result { "passed" } else { "failed" }
279                )
280            }
281        }
282    }
283
284    /// Get the impact this factor has on capability (positive or negative)
285    pub fn impact(&self) -> i8 {
286        match self {
287            CapabilityFactor::ManufacturerMatch(_) => 20,
288            CapabilityFactor::ModelMatch(_) => 25,
289            CapabilityFactor::FormatVersionSupported(_) => 15,
290            CapabilityFactor::RequiredContextAvailable(_) => 10,
291            CapabilityFactor::TableNameMatch(_) => 15,
292            CapabilityFactor::DataPatternMatch(_) => 30,
293            CapabilityFactor::CustomCondition(_, true) => 10,
294            CapabilityFactor::CustomCondition(_, false) => -20,
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_capability_ordering() {
305        assert!(ProcessorCapability::Perfect > ProcessorCapability::Good);
306        assert!(ProcessorCapability::Good > ProcessorCapability::Fallback);
307        assert!(ProcessorCapability::Fallback > ProcessorCapability::Incompatible);
308    }
309
310    #[test]
311    fn test_capability_priority_scores() {
312        assert_eq!(ProcessorCapability::Perfect.priority_score(), 100);
313        assert_eq!(ProcessorCapability::Good.priority_score(), 75);
314        assert_eq!(ProcessorCapability::Fallback.priority_score(), 25);
315        assert_eq!(ProcessorCapability::Incompatible.priority_score(), 0);
316    }
317
318    #[test]
319    fn test_capability_compatibility() {
320        assert!(ProcessorCapability::Perfect.is_compatible());
321        assert!(ProcessorCapability::Good.is_compatible());
322        assert!(ProcessorCapability::Fallback.is_compatible());
323        assert!(!ProcessorCapability::Incompatible.is_compatible());
324    }
325
326    #[test]
327    fn test_capability_combination() {
328        let capabilities = vec![
329            ProcessorCapability::Perfect,
330            ProcessorCapability::Good,
331            ProcessorCapability::Fallback,
332        ];
333        assert_eq!(
334            ProcessorCapability::combine(&capabilities),
335            ProcessorCapability::Fallback
336        );
337
338        let capabilities = vec![
339            ProcessorCapability::Perfect,
340            ProcessorCapability::Incompatible,
341        ];
342        assert_eq!(
343            ProcessorCapability::combine(&capabilities),
344            ProcessorCapability::Incompatible
345        );
346
347        let capabilities = vec![ProcessorCapability::Perfect, ProcessorCapability::Good];
348        assert_eq!(
349            ProcessorCapability::combine(&capabilities),
350            ProcessorCapability::Good
351        );
352    }
353
354    #[test]
355    fn test_capability_from_specificity() {
356        assert_eq!(
357            ProcessorCapability::from_specificity(false, false, false),
358            ProcessorCapability::Incompatible
359        );
360        assert_eq!(
361            ProcessorCapability::from_specificity(true, true, true),
362            ProcessorCapability::Perfect
363        );
364        assert_eq!(
365            ProcessorCapability::from_specificity(true, false, true),
366            ProcessorCapability::Good
367        );
368        assert_eq!(
369            ProcessorCapability::from_specificity(true, false, false),
370            ProcessorCapability::Fallback
371        );
372    }
373
374    #[test]
375    fn test_capability_assessment() {
376        let assessment = CapabilityAssessment::new(
377            ProcessorCapability::Good,
378            "Manufacturer matches".to_string(),
379        )
380        .add_factor(CapabilityFactor::ManufacturerMatch("Canon".to_string()))
381        .add_missing_requirement("Model information".to_string());
382
383        assert_eq!(assessment.capability, ProcessorCapability::Good);
384        assert_eq!(assessment.factors.len(), 1);
385        assert_eq!(assessment.missing_requirements.len(), 1);
386
387        let explanation = assessment.detailed_explanation();
388        assert!(explanation.contains("Canon"));
389        assert!(explanation.contains("Model information"));
390    }
391
392    #[test]
393    fn test_capability_factor_impact() {
394        let factor = CapabilityFactor::ManufacturerMatch("Canon".to_string());
395        assert_eq!(factor.impact(), 20);
396
397        let factor = CapabilityFactor::CustomCondition("test".to_string(), false);
398        assert_eq!(factor.impact(), -20);
399    }
400}