1pub mod config;
35pub mod convert;
36pub mod error;
37pub mod ids;
38pub mod record;
39pub mod runtime;
40pub mod sink;
41pub mod source;
42
43pub use config::{
44 ConfigDef, ConfigError, ConfigKey, ConfigKind, ConfigResult, ConnectorConfig,
45 EnvSecretResolver, FromResolvedValue, RawConfig, ResolveOptions, ResolvedConfig, SecretRef,
46 SecretResolutionError, SecretResolver, SecretString,
47};
48pub use convert::{ByteIdentity, Converter, SchemaConverter};
49#[cfg(feature = "derive")]
50pub use crabka_connect_derive::ConnectorConfig;
51pub use error::ConnectError;
52pub use ids::{PartitionMap, PositionMap};
53pub use record::{ConnectRecord, Header, OffsetMap, OffsetValue, SourceOffset};
54pub use runtime::{
55 CheckpointStore, ConnectorHandle, ConnectorRuntime, HasSink, HasSource,
56 InMemoryCheckpointStore, NoSink, NoSource, RuntimeState,
57};
58#[doc(hidden)]
59pub use serde_json as __serde_json;
60pub use sink::Sink;
61pub use source::Source;
62
63#[cfg(test)]
64mod tests {
65 use async_trait::async_trait;
69 use bytes::Bytes;
70
71 use super::*;
72
73 struct VecSource {
76 records: Vec<(Bytes, Bytes)>,
77 pos: usize,
78 }
79
80 #[async_trait]
81 impl Source<Bytes, Bytes> for VecSource {
82 async fn poll(&mut self) -> Result<Option<ConnectRecord<Bytes, Bytes>>, ConnectError> {
83 let Some((k, v)) = self.records.get(self.pos).cloned() else {
84 return Ok(None);
85 };
86 self.pos += 1;
87 Ok(Some(ConnectRecord::new(Some(k), Some(v))))
88 }
89
90 fn checkpoint(&self) -> Option<SourceOffset> {
91 if self.pos == 0 {
92 return None;
93 }
94 let mut position = OffsetMap::new();
95 #[allow(clippy::cast_possible_wrap)]
96 position.insert("index".into(), OffsetValue::Long(self.pos as i64));
97 Some(SourceOffset::new(OffsetMap::new().into(), position.into()))
98 }
99
100 async fn seek(&mut self, offset: SourceOffset) -> Result<(), ConnectError> {
101 match offset.position.get("index") {
102 Some(OffsetValue::Long(i)) => {
103 self.pos = usize::try_from(*i)
104 .map_err(|_| ConnectError::Offset(format!("negative index {i}")))?;
105 Ok(())
106 }
107 _ => Err(ConnectError::Offset("missing `index` component".into())),
108 }
109 }
110 }
111
112 #[derive(Default)]
115 struct VecSink {
116 staged: Vec<ConnectRecord<Bytes, Bytes>>,
117 committed: Vec<ConnectRecord<Bytes, Bytes>>,
118 }
119
120 #[async_trait]
121 impl Sink<Bytes, Bytes> for VecSink {
122 async fn put(
123 &mut self,
124 records: Vec<ConnectRecord<Bytes, Bytes>>,
125 ) -> Result<(), ConnectError> {
126 self.staged.extend(records);
127 Ok(())
128 }
129
130 async fn flush(&mut self) -> Result<(), ConnectError> {
131 self.committed.append(&mut self.staged);
132 Ok(())
133 }
134
135 fn supports_transactions(&self) -> bool {
136 true
137 }
138
139 async fn abort(&mut self) -> Result<(), ConnectError> {
140 self.staged.clear();
141 Ok(())
142 }
143 }
144
145 #[tokio::test]
146 async fn source_drains_then_checkpoint_and_seek_resume() {
147 use assert2::check;
148
149 let mut src = VecSource {
150 records: vec![
151 (Bytes::from_static(b"k0"), Bytes::from_static(b"v0")),
152 (Bytes::from_static(b"k1"), Bytes::from_static(b"v1")),
153 ],
154 pos: 0,
155 };
156 check!(src.checkpoint().is_none());
157
158 let first = src.poll().await.unwrap().unwrap();
159 check!(first.key == Some(Bytes::from_static(b"k0")));
160 let cp = src.checkpoint().expect("position after one poll");
161
162 let mut resumed = VecSource {
164 records: src.records.clone(),
165 pos: 0,
166 };
167 resumed.seek(cp).await.unwrap();
168 let next = resumed.poll().await.unwrap().unwrap();
169 check!(next.key == Some(Bytes::from_static(b"k1")));
170 check!(resumed.poll().await.unwrap().is_none());
172 }
173
174 #[tokio::test]
175 async fn sink_commit_promotes_and_abort_discards() {
176 use assert2::check;
177
178 let mut sink = VecSink::default();
179 check!(sink.supports_transactions());
180
181 sink.begin().await.unwrap();
183 sink.put(vec![ConnectRecord::new(
184 None,
185 Some(Bytes::from_static(b"a")),
186 )])
187 .await
188 .unwrap();
189 sink.commit().await.unwrap();
190 check!(sink.committed.len() == 1);
191
192 sink.begin().await.unwrap();
194 sink.put(vec![ConnectRecord::new(
195 None,
196 Some(Bytes::from_static(b"b")),
197 )])
198 .await
199 .unwrap();
200 sink.abort().await.unwrap();
201 check!(sink.committed.len() == 1);
202 check!(sink.staged.is_empty());
203 }
204
205 #[tokio::test]
206 async fn spi_is_object_safe() {
207 use assert2::check;
208
209 let mut src: Box<dyn Source<Bytes, Bytes>> = Box::new(VecSource {
212 records: vec![(Bytes::from_static(b"k"), Bytes::from_static(b"v"))],
213 pos: 0,
214 });
215 let mut sink: Box<dyn Sink<Bytes, Bytes>> = Box::new(VecSink::default());
216
217 while let Some(rec) = src.poll().await.unwrap() {
218 sink.put(vec![rec]).await.unwrap();
219 }
220 sink.flush().await.unwrap();
221
222 let conv: &dyn Converter<Bytes> = &ByteIdentity;
223 let wire = conv.serialize("t", &Bytes::from_static(b"v")).unwrap();
224 check!(wire == Bytes::from_static(b"v"));
225 }
226
227 #[tokio::test]
228 async fn source_seek_rejects_unresumable_offset() {
229 use assert2::check;
230
231 let mut src = VecSource {
232 records: vec![],
233 pos: 0,
234 };
235 check!(src.seek(SourceOffset::default()).await.is_err());
237 let mut position = OffsetMap::new();
239 position.insert("index".into(), OffsetValue::Long(-1));
240 check!(
241 src.seek(SourceOffset::new(OffsetMap::new().into(), position.into()))
242 .await
243 .is_err()
244 );
245 }
246
247 #[tokio::test]
248 async fn source_and_sink_close_are_noops() {
249 use assert2::check;
250
251 let mut src = VecSource {
252 records: vec![],
253 pos: 0,
254 };
255 check!(src.close().await.is_ok());
256 let mut sink = VecSink::default();
257 check!(sink.close().await.is_ok());
258 }
259}