pub mod config;
pub mod convert;
pub mod error;
pub mod ids;
pub mod record;
pub mod runtime;
pub mod sink;
pub mod source;
pub use config::{
ConfigDef, ConfigError, ConfigKey, ConfigKind, ConfigResult, ConnectorConfig,
EnvSecretResolver, FromResolvedValue, RawConfig, ResolveOptions, ResolvedConfig, SecretRef,
SecretResolutionError, SecretResolver, SecretString,
};
pub use convert::{ByteIdentity, Converter, SchemaConverter};
#[cfg(feature = "derive")]
pub use crabka_connect_derive::ConnectorConfig;
pub use error::ConnectError;
pub use ids::{PartitionMap, PositionMap};
pub use record::{ConnectRecord, Header, OffsetMap, OffsetValue, SourceOffset};
pub use runtime::{
CheckpointStore, ConnectorHandle, ConnectorRuntime, HasSink, HasSource,
InMemoryCheckpointStore, NoSink, NoSource, RuntimeState,
};
#[doc(hidden)]
pub use serde_json as __serde_json;
pub use sink::Sink;
pub use source::Source;
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use bytes::Bytes;
use super::*;
struct VecSource {
records: Vec<(Bytes, Bytes)>,
pos: usize,
}
#[async_trait]
impl Source<Bytes, Bytes> for VecSource {
async fn poll(&mut self) -> Result<Option<ConnectRecord<Bytes, Bytes>>, ConnectError> {
let Some((k, v)) = self.records.get(self.pos).cloned() else {
return Ok(None);
};
self.pos += 1;
Ok(Some(ConnectRecord::new(Some(k), Some(v))))
}
fn checkpoint(&self) -> Option<SourceOffset> {
if self.pos == 0 {
return None;
}
let mut position = OffsetMap::new();
#[allow(clippy::cast_possible_wrap)]
position.insert("index".into(), OffsetValue::Long(self.pos as i64));
Some(SourceOffset::new(OffsetMap::new().into(), position.into()))
}
async fn seek(&mut self, offset: SourceOffset) -> Result<(), ConnectError> {
match offset.position.get("index") {
Some(OffsetValue::Long(i)) => {
self.pos = usize::try_from(*i)
.map_err(|_| ConnectError::Offset(format!("negative index {i}")))?;
Ok(())
}
_ => Err(ConnectError::Offset("missing `index` component".into())),
}
}
}
#[derive(Default)]
struct VecSink {
staged: Vec<ConnectRecord<Bytes, Bytes>>,
committed: Vec<ConnectRecord<Bytes, Bytes>>,
}
#[async_trait]
impl Sink<Bytes, Bytes> for VecSink {
async fn put(
&mut self,
records: Vec<ConnectRecord<Bytes, Bytes>>,
) -> Result<(), ConnectError> {
self.staged.extend(records);
Ok(())
}
async fn flush(&mut self) -> Result<(), ConnectError> {
self.committed.append(&mut self.staged);
Ok(())
}
fn supports_transactions(&self) -> bool {
true
}
async fn abort(&mut self) -> Result<(), ConnectError> {
self.staged.clear();
Ok(())
}
}
#[tokio::test]
async fn source_drains_then_checkpoint_and_seek_resume() {
use assert2::check;
let mut src = VecSource {
records: vec![
(Bytes::from_static(b"k0"), Bytes::from_static(b"v0")),
(Bytes::from_static(b"k1"), Bytes::from_static(b"v1")),
],
pos: 0,
};
check!(src.checkpoint().is_none());
let first = src.poll().await.unwrap().unwrap();
check!(first.key == Some(Bytes::from_static(b"k0")));
let cp = src.checkpoint().expect("position after one poll");
let mut resumed = VecSource {
records: src.records.clone(),
pos: 0,
};
resumed.seek(cp).await.unwrap();
let next = resumed.poll().await.unwrap().unwrap();
check!(next.key == Some(Bytes::from_static(b"k1")));
check!(resumed.poll().await.unwrap().is_none());
}
#[tokio::test]
async fn sink_commit_promotes_and_abort_discards() {
use assert2::check;
let mut sink = VecSink::default();
check!(sink.supports_transactions());
sink.begin().await.unwrap();
sink.put(vec![ConnectRecord::new(
None,
Some(Bytes::from_static(b"a")),
)])
.await
.unwrap();
sink.commit().await.unwrap();
check!(sink.committed.len() == 1);
sink.begin().await.unwrap();
sink.put(vec![ConnectRecord::new(
None,
Some(Bytes::from_static(b"b")),
)])
.await
.unwrap();
sink.abort().await.unwrap();
check!(sink.committed.len() == 1);
check!(sink.staged.is_empty());
}
#[tokio::test]
async fn spi_is_object_safe() {
use assert2::check;
let mut src: Box<dyn Source<Bytes, Bytes>> = Box::new(VecSource {
records: vec![(Bytes::from_static(b"k"), Bytes::from_static(b"v"))],
pos: 0,
});
let mut sink: Box<dyn Sink<Bytes, Bytes>> = Box::new(VecSink::default());
while let Some(rec) = src.poll().await.unwrap() {
sink.put(vec![rec]).await.unwrap();
}
sink.flush().await.unwrap();
let conv: &dyn Converter<Bytes> = &ByteIdentity;
let wire = conv.serialize("t", &Bytes::from_static(b"v")).unwrap();
check!(wire == Bytes::from_static(b"v"));
}
#[tokio::test]
async fn source_seek_rejects_unresumable_offset() {
use assert2::check;
let mut src = VecSource {
records: vec![],
pos: 0,
};
check!(src.seek(SourceOffset::default()).await.is_err());
let mut position = OffsetMap::new();
position.insert("index".into(), OffsetValue::Long(-1));
check!(
src.seek(SourceOffset::new(OffsetMap::new().into(), position.into()))
.await
.is_err()
);
}
#[tokio::test]
async fn source_and_sink_close_are_noops() {
use assert2::check;
let mut src = VecSource {
records: vec![],
pos: 0,
};
check!(src.close().await.is_ok());
let mut sink = VecSink::default();
check!(sink.close().await.is_ok());
}
}