Skip to main content

exif_oxide/processor_registry/
traits.rs

1//! Core trait definitions for the enhanced processor architecture
2//!
3//! This module defines the fundamental traits that enable ExifTool's sophisticated
4//! processor dispatch system while maintaining Rust's type safety and performance.
5
6use crate::types::{Result, TagValue};
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use super::{ProcessorCapability, ProcessorContext};
11
12/// Core trait for binary data processors
13///
14/// This trait abstracts ExifTool's various processing functions (ProcessBinaryData,
15/// ProcessNikonEncrypted, ProcessSerialData, etc.) into a unified interface that
16/// supports capability assessment and rich context passing.
17///
18/// ## ExifTool Reference
19///
20/// ExifTool processors have the signature: `sub ProcessorName($$$) { my ($et, $dirInfo, $tagTablePtr) = @_; }`
21/// This trait provides equivalent functionality with Rust type safety.
22pub trait BinaryDataProcessor: Send + Sync {
23    /// Assess this processor's capability to handle the given context
24    ///
25    /// Returns a capability assessment that helps the registry select the most
26    /// appropriate processor for the current data and context.
27    ///
28    /// ## ExifTool Reference
29    ///
30    /// This implements the conditional logic found in ExifTool's Condition expressions:
31    /// ```perl
32    /// {
33    ///     Condition => '$$self{Model} =~ /EOS R5/',
34    ///     SubDirectory => { ProcessProc => \&ProcessCanonSerialData }
35    /// }
36    /// ```
37    fn can_process(&self, context: &ProcessorContext) -> ProcessorCapability;
38
39    /// Process binary data and extract tags
40    ///
41    /// This is the core processing function that extracts metadata from binary data
42    /// using the provided context. Returns extracted tags, warnings, and potential
43    /// nested processors for recursive processing.
44    ///
45    /// ## ExifTool Reference
46    ///
47    /// Equivalent to ExifTool's main processor functions like ProcessBinaryData,
48    /// ProcessNikonEncrypted, etc.
49    fn process_data(&self, data: &[u8], context: &ProcessorContext) -> Result<ProcessorResult>;
50
51    /// Get metadata about this processor
52    ///
53    /// Returns descriptive metadata for debugging, documentation, and introspection.
54    /// This helps with understanding processor selection decisions and capabilities.
55    fn get_metadata(&self) -> ProcessorMetadata;
56}
57
58/// Result returned by processor after processing binary data
59///
60/// This structure encapsulates everything a processor produces, including extracted
61/// tags, warnings, and instructions for further processing.
62///
63/// ## ExifTool Reference
64///
65/// ExifTool processors modify the ExifTool object state and may trigger recursive
66/// processing. This structure captures those side effects explicitly.
67#[derive(Debug)]
68pub struct ProcessorResult {
69    /// Tags extracted by the processor
70    /// Maps tag names to their extracted values
71    pub extracted_tags: HashMap<String, TagValue>,
72
73    /// Warnings generated during processing
74    /// Used for non-fatal issues like data corruption or unexpected formats
75    pub warnings: Vec<String>,
76
77    /// Additional processors to run for nested processing
78    /// Each entry specifies a processor and the context it should use
79    /// This enables ExifTool's recursive SubDirectory processing
80    pub next_processors: Vec<(ProcessorKey, ProcessorContext)>,
81}
82
83impl ProcessorResult {
84    /// Create a new empty processor result
85    pub fn new() -> Self {
86        Self {
87            extracted_tags: HashMap::new(),
88            warnings: Vec::new(),
89            next_processors: Vec::new(),
90        }
91    }
92
93    /// Create processor result with extracted tags
94    pub fn with_tags(extracted_tags: HashMap<String, TagValue>) -> Self {
95        Self {
96            extracted_tags,
97            warnings: Vec::new(),
98            next_processors: Vec::new(),
99        }
100    }
101
102    /// Add a warning to the result
103    pub fn add_warning(&mut self, warning: String) {
104        self.warnings.push(warning);
105    }
106
107    /// Add a tag to the extracted results
108    pub fn add_tag(&mut self, name: String, value: TagValue) {
109        self.extracted_tags.insert(name, value);
110    }
111
112    /// Add a nested processor for recursive processing
113    pub fn add_nested_processor(&mut self, key: ProcessorKey, context: ProcessorContext) {
114        self.next_processors.push((key, context));
115    }
116}
117
118impl Default for ProcessorResult {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124/// Metadata about a processor for introspection and debugging
125///
126/// This provides human-readable information about what a processor does,
127/// what it supports, and what context it requires.
128#[derive(Debug, Clone)]
129pub struct ProcessorMetadata {
130    /// Human-readable name of the processor
131    pub name: String,
132
133    /// Description of what this processor handles
134    pub description: String,
135
136    /// Manufacturers this processor supports (e.g., ["Canon", "Nikon"])
137    pub supported_manufacturers: Vec<String>,
138
139    /// Required context fields for this processor to function
140    /// Used for validation and error reporting
141    pub required_context: Vec<String>,
142
143    /// Optional context fields that enhance processing
144    pub optional_context: Vec<String>,
145
146    /// Example conditions where this processor would be selected
147    pub example_conditions: Vec<String>,
148}
149
150impl ProcessorMetadata {
151    /// Create new processor metadata
152    pub fn new(name: String, description: String) -> Self {
153        Self {
154            name,
155            description,
156            supported_manufacturers: Vec::new(),
157            required_context: Vec::new(),
158            optional_context: Vec::new(),
159            example_conditions: Vec::new(),
160        }
161    }
162
163    /// Add supported manufacturer
164    pub fn with_manufacturer(mut self, manufacturer: String) -> Self {
165        self.supported_manufacturers.push(manufacturer);
166        self
167    }
168
169    /// Add required context field
170    pub fn with_required_context(mut self, field: String) -> Self {
171        self.required_context.push(field);
172        self
173    }
174
175    /// Add optional context field
176    pub fn with_optional_context(mut self, field: String) -> Self {
177        self.optional_context.push(field);
178        self
179    }
180
181    /// Add example condition
182    pub fn with_example_condition(mut self, condition: String) -> Self {
183        self.example_conditions.push(condition);
184        self
185    }
186}
187
188/// Unique identifier for a processor in the registry
189///
190/// This provides a hierarchical naming scheme that mirrors ExifTool's processor
191/// organization: namespace (manufacturer) + processor name + optional variant.
192///
193/// ## ExifTool Reference
194///
195/// ExifTool processors are referenced by function names like:
196/// - `ProcessBinaryData` (generic)
197/// - `ProcessNikonEncrypted` (manufacturer-specific)
198/// - `ProcessCanonSerialData` (manufacturer + type specific)
199#[derive(Debug, Clone, PartialEq, Eq, Hash)]
200pub struct ProcessorKey {
201    /// Namespace (typically manufacturer: "Canon", "Nikon", "EXIF", etc.)
202    pub namespace: String,
203
204    /// Processor name within the namespace ("SerialData", "AFInfo", "Encrypted", etc.)
205    pub processor_name: String,
206
207    /// Optional variant for model-specific processors ("MkII", "Z9", etc.)
208    pub variant: Option<String>,
209}
210
211impl ProcessorKey {
212    /// Create a new processor key
213    pub fn new(namespace: String, processor_name: String) -> Self {
214        Self {
215            namespace,
216            processor_name,
217            variant: None,
218        }
219    }
220
221    /// Create a processor key with variant
222    pub fn with_variant(namespace: String, processor_name: String, variant: String) -> Self {
223        Self {
224            namespace,
225            processor_name,
226            variant: Some(variant),
227        }
228    }
229}
230
231impl std::fmt::Display for ProcessorKey {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        match &self.variant {
234            Some(variant) => write!(
235                f,
236                "{}::{}::{}",
237                self.namespace, self.processor_name, variant
238            ),
239            None => write!(f, "{}::{}", self.namespace, self.processor_name),
240        }
241    }
242}
243
244/// Arc-wrapped processor for efficient sharing in the registry
245pub type SharedProcessor = Arc<dyn BinaryDataProcessor>;
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_processor_key_formatting() {
253        let key = ProcessorKey::new("Canon".to_string(), "SerialData".to_string());
254        assert_eq!(format!("{key}"), "Canon::SerialData");
255
256        let key_with_variant = ProcessorKey::with_variant(
257            "Canon".to_string(),
258            "SerialData".to_string(),
259            "MkII".to_string(),
260        );
261        assert_eq!(format!("{key_with_variant}"), "Canon::SerialData::MkII");
262    }
263
264    #[test]
265    fn test_processor_result_creation() {
266        let mut result = ProcessorResult::new();
267        result.add_tag("TestTag".to_string(), TagValue::String("test".to_string()));
268        result.add_warning("Test warning".to_string());
269
270        assert_eq!(result.extracted_tags.len(), 1);
271        assert_eq!(result.warnings.len(), 1);
272        assert_eq!(result.next_processors.len(), 0);
273    }
274
275    #[test]
276    fn test_processor_metadata_builder() {
277        let metadata =
278            ProcessorMetadata::new("Test Processor".to_string(), "A test processor".to_string())
279                .with_manufacturer("Canon".to_string())
280                .with_required_context("manufacturer".to_string())
281                .with_example_condition("$model =~ /EOS R5/".to_string());
282
283        assert_eq!(metadata.supported_manufacturers, vec!["Canon"]);
284        assert_eq!(metadata.required_context, vec!["manufacturer"]);
285        assert_eq!(metadata.example_conditions, vec!["$model =~ /EOS R5/"]);
286    }
287}