Skip to main content

feldera_adapterlib/
preprocess.rs

1//! Data preprocessing layer for connectors.
2//!
3//! This module provides a preprocessing framework that allows data transformation
4//! before it reaches the parser.
5//!
6//! The preprocessing layer fits between transport and parsing in the data pipeline:
7//!
8//! ```text
9//! Transport → Preprocessor → Parser → Circuit
10//! ```
11
12use crate::ConnectorMetadata;
13use crate::format::{ParseError, Splitter};
14use feldera_types::preprocess::PreprocessorConfig;
15use std::collections::BTreeMap;
16use std::fmt::{Display, Formatter, Result as FmtResult};
17use std::sync::Arc;
18
19// Errors that can occur during creation of a preprocessor
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum PreprocessorCreateError {
22    /// Preprocessing configuration is invalid.
23    ConfigurationError(String),
24    /// Implementation for factory generating Preprocessor not found
25    FactoryNotFound(String),
26}
27
28impl Display for PreprocessorCreateError {
29    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
30        match self {
31            PreprocessorCreateError::ConfigurationError(msg) => {
32                write!(f, "Configuration error: {}", msg)
33            }
34            PreprocessorCreateError::FactoryNotFound(msg) => {
35                write!(
36                    f,
37                    "Could not locate factory generating preprocessor: {}",
38                    msg
39                )
40            }
41        }
42    }
43}
44
45impl std::error::Error for PreprocessorCreateError {}
46
47/// Trait for preprocessing raw data before parsing.
48pub trait Preprocessor: Send + Sync {
49    /// Process raw input data and return transformed data.
50    ///
51    /// The default implementation forwards to `process_with_metadata` with no
52    /// metadata.
53    ///
54    /// # Arguments
55    /// * `data` - Raw input data bytes
56    ///
57    /// # Returns
58    /// The transformed data and any errors that occurred.
59    fn process(&mut self, data: &[u8]) -> (Vec<u8>, Vec<ParseError>) {
60        self.process_with_metadata(data, None)
61    }
62
63    /// Process raw input data and return transformed data using connector metadata.
64    ///
65    /// Users should implement exactly one of the two processing methods: `process` or
66    /// `process_with_metadata`.
67    ///
68    /// WARNING: a preprocessor that implements neither processing method
69    /// compiles, but will panic at runtime with a stack overflow.
70    ///
71    /// # Arguments
72    /// * `data` - Raw input data bytes
73    /// * `_metadata` - Connector metadata
74    ///
75    /// # Returns
76    /// The transformed data and any errors that occurred.
77    fn process_with_metadata(
78        &mut self,
79        data: &[u8],
80        _metadata: Option<&ConnectorMetadata>,
81    ) -> (Vec<u8>, Vec<ParseError>) {
82        self.process(data)
83    }
84
85    /// Create a new preprocessor with the same configuration as `self`.
86    ///
87    /// Used by multithreaded transport endpoints to create multiple parallel
88    /// input pipelines.
89    fn fork(&self) -> Box<dyn Preprocessor>;
90
91    /// Returns an object that can be used to break a stream of incoming data
92    /// into complete records to pass to [Preprocessor::process].  If the object
93    /// is None, the parser's splitter object will actually be used.
94    fn splitter(&self) -> Option<Box<dyn Splitter>>;
95}
96
97/// A factory that can create a new Preprocessor object.
98pub trait PreprocessorFactory: Send + Sync {
99    /// Create a new preprocessor based on the supplied configuration.
100    ///
101    /// # Arguments
102    ///
103    /// * `config` - Preprocessor-specific configuration.
104    fn create(
105        &self,
106        config: &PreprocessorConfig,
107    ) -> Result<Box<dyn Preprocessor>, PreprocessorCreateError>;
108}
109
110/// A registry where all factories that can create Preprocessors are registered
111#[derive(Default)]
112pub struct PreprocessorRegistry {
113    registered: BTreeMap<&'static str, Arc<dyn PreprocessorFactory>>,
114}
115
116impl PreprocessorRegistry {
117    pub fn new() -> Self {
118        Self {
119            registered: BTreeMap::new(),
120        }
121    }
122
123    /// Register a new factory under the specified name
124    pub fn register(&mut self, name: &'static str, factory: Box<dyn PreprocessorFactory>) {
125        self.registered.insert(name, Arc::from(factory));
126    }
127
128    pub fn get(&self, name: &str) -> Option<Arc<dyn PreprocessorFactory>> {
129        self.registered.get(name).cloned()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use feldera_sqllib::{SqlString, Variant};
137    use serde_json::json;
138
139    /// Preprocessor whose output depends on connector metadata: it prepends
140    /// the value of the `topic` attribute to each record.  Reads the attribute
141    /// through `ConnectorMetadata::get_by_name`, the same path a user-defined
142    /// preprocessor must use.
143    struct TopicPrefixPreprocessor;
144
145    impl Preprocessor for TopicPrefixPreprocessor {
146        fn process_with_metadata(
147            &mut self,
148            data: &[u8],
149            metadata: Option<&ConnectorMetadata>,
150        ) -> (Vec<u8>, Vec<ParseError>) {
151            let Some(metadata) = metadata else {
152                // No metadata attached: pass the record through unchanged.
153                return (data.to_vec(), vec![]);
154            };
155            let mut output = Vec::new();
156            if let Some(Variant::String(topic)) = metadata.get_by_name("topic") {
157                output.extend_from_slice(topic.str().as_bytes());
158                output.push(b':');
159            }
160            output.extend_from_slice(data);
161            (output, vec![])
162        }
163
164        fn fork(&self) -> Box<dyn Preprocessor> {
165            Box::new(TopicPrefixPreprocessor)
166        }
167
168        fn splitter(&self) -> Option<Box<dyn Splitter>> {
169            None
170        }
171    }
172
173    /// Preprocessor with an observable transformation, for registry tests.
174    struct UppercasePreprocessor;
175
176    impl Preprocessor for UppercasePreprocessor {
177        fn process(&mut self, data: &[u8]) -> (Vec<u8>, Vec<ParseError>) {
178            (data.to_ascii_uppercase(), vec![])
179        }
180
181        fn fork(&self) -> Box<dyn Preprocessor> {
182            Box::new(UppercasePreprocessor)
183        }
184
185        fn splitter(&self) -> Option<Box<dyn Splitter>> {
186            None
187        }
188    }
189
190    struct UppercasePreprocessorFactory;
191
192    impl PreprocessorFactory for UppercasePreprocessorFactory {
193        fn create(
194            &self,
195            _config: &PreprocessorConfig,
196        ) -> Result<Box<dyn Preprocessor>, PreprocessorCreateError> {
197            Ok(Box::new(UppercasePreprocessor))
198        }
199    }
200
201    fn make_config(name: &str) -> PreprocessorConfig {
202        PreprocessorConfig {
203            name: name.to_string(),
204            message_oriented: false,
205            config: json!({}),
206        }
207    }
208
209    fn make_metadata() -> ConnectorMetadata {
210        let mut metadata = ConnectorMetadata::new();
211        metadata.insert("topic", Variant::String(SqlString::from("events")));
212        metadata
213    }
214
215    #[test]
216    fn test_process_metadata_transforms_using_metadata() {
217        let mut preprocessor = TopicPrefixPreprocessor;
218
219        // `make_metadata` sets the `topic` attribute to `events`.
220        let metadata = make_metadata();
221        let (output, errors) = preprocessor.process_with_metadata(b"payload", Some(&metadata));
222        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
223        assert_eq!(output, b"events:payload");
224
225        // Without metadata the record passes through unchanged.
226        let (output, errors) = preprocessor.process_with_metadata(b"payload", None);
227        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
228        assert_eq!(output, b"payload");
229    }
230
231    /// A preprocessor that implements only `process_with_metadata` is still
232    /// callable through `process`, whose default implementation forwards with
233    /// no metadata.  This test compiles only because `process` has a default;
234    /// it pins the backward-compatible surface of the trait.
235    #[test]
236    fn test_process_defaults_to_process_with_metadata() {
237        let mut preprocessor = TopicPrefixPreprocessor;
238
239        let (output, errors) = preprocessor.process(b"payload");
240        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
241        assert_eq!(output, b"payload");
242    }
243
244    #[test]
245    fn test_registry_register_and_get() {
246        let mut registry = PreprocessorRegistry::new();
247        registry.register("upper", Box::new(UppercasePreprocessorFactory));
248
249        let factory = registry.get("upper").expect("factory must be registered");
250        let mut preprocessor = factory.create(&make_config("upper")).unwrap();
251        let (output, errors) = preprocessor.process(b"xyz");
252        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
253        assert_eq!(output, b"XYZ");
254
255        assert!(registry.get("missing").is_none());
256    }
257}