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}