use arrow::record_batch::RecordBatch;
use async_trait::async_trait;
use orion_error::conversion::ToStructError;
use orion_error::{OrionError, StructError, UnifiedReason};
use std::error::Error as StdError;
#[derive(Debug, Clone, PartialEq, OrionError)]
pub enum SourceReason {
#[orion_error(message = "end of stream", identity = "sys.wf_connector.eof")]
EOF,
#[orion_error(message = "no data available", identity = "sys.wf_connector.not_data")]
NotData,
#[orion_error(message = "I/O error", identity = "sys.wf_connector.io")]
Io,
#[orion_error(message = "connection error", identity = "sys.wf_connector.connect")]
Connect,
#[orion_error(message = "decode error", identity = "sys.wf_connector.decode")]
Decode,
#[orion_error(
message = "connector not found",
identity = "sys.wf_connector.not_found"
)]
NotFound,
#[orion_error(transparent)]
General(UnifiedReason),
}
impl SourceReason {
pub fn err_detail<S: Into<String>>(self, detail: S) -> SourceError {
self.to_err().with_detail(detail.into())
}
pub fn err_source<E>(self, source: E) -> SourceError
where
E: StdError + Send + Sync + 'static,
{
self.to_err().with_source(source)
}
}
pub type SourceError = StructError<SourceReason>;
pub type SourceResult<T> = Result<T, SourceError>;
#[async_trait]
pub trait BatchSource: Send {
async fn start(&mut self) -> SourceResult<()> {
Ok(())
}
async fn receive_batch(&mut self) -> SourceResult<Vec<RecordBatch>>;
async fn close(&mut self) -> SourceResult<()> {
Ok(())
}
fn identifier(&self) -> &str;
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::{ArrayRef, Int32Array};
use arrow::datatypes::{DataType, Field as ArrowField, Schema};
use orion_error::dev::testing::assert_err_identity;
use orion_error::reason::ErrorCategory;
use std::future::Future;
use std::pin::pin;
use std::sync::Arc;
use std::task::{Context, Poll, Waker};
fn block_on<F: Future>(fut: F) -> F::Output {
let mut fut = pin!(fut);
let mut cx = Context::from_waker(Waker::noop());
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(out) => return out,
Poll::Pending => std::thread::yield_now(),
}
}
}
fn batch(values: Vec<i32>) -> RecordBatch {
let schema = Arc::new(Schema::new(vec![ArrowField::new(
"n",
DataType::Int32,
false,
)]));
let array = Arc::new(Int32Array::from(values)) as ArrayRef;
RecordBatch::try_new(schema, vec![array]).expect("test batch should be valid")
}
#[test]
fn every_leaf_reason_exposes_its_stable_code() {
let cases = [
(SourceReason::EOF, "sys.wf_connector.eof"),
(SourceReason::NotData, "sys.wf_connector.not_data"),
(SourceReason::Io, "sys.wf_connector.io"),
(SourceReason::Connect, "sys.wf_connector.connect"),
(SourceReason::Decode, "sys.wf_connector.decode"),
(SourceReason::NotFound, "sys.wf_connector.not_found"),
];
for (reason, code) in cases {
let err = reason.err_detail("why");
assert_err_identity(&err, code, ErrorCategory::Sys);
}
}
#[test]
fn err_detail_stores_the_detail_and_has_no_source() {
let err = SourceReason::Decode.err_detail("bad frame");
assert_eq!(err.detail().as_deref(), Some("bad frame"));
assert_eq!(err.reason(), &SourceReason::Decode);
assert!(err.source_ref().is_none());
assert!(err.to_string().contains("bad frame"), "display = {err}");
}
#[test]
fn err_source_keeps_the_underlying_error() {
let io = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe closed");
let err = SourceReason::Io.err_source(io);
let source = err.source_ref().expect("the underlying error must be kept");
assert!(
source.to_string().contains("pipe closed"),
"source = {source}"
);
assert!(err.detail().is_none());
}
#[test]
fn helpers_return_the_declared_aliases() {
fn takes_source_error(_: SourceError) {}
fn takes_source_result(_: SourceResult<()>) {}
takes_source_error(SourceReason::Connect.err_detail("refused"));
takes_source_result(Ok(()));
}
#[test]
fn source_error_is_send_sync_and_convertible_to_a_std_error() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<SourceError>();
assert_send_sync::<SourceReason>();
let boxed: Box<dyn StdError + Send + Sync + 'static> = SourceReason::Decode
.err_detail("bad frame")
.into_boxed_std();
assert!(boxed.to_string().contains("bad frame"));
}
struct MinimalSource {
remaining: Vec<RecordBatch>,
}
#[async_trait]
impl BatchSource for MinimalSource {
async fn receive_batch(&mut self) -> SourceResult<Vec<RecordBatch>> {
match self.remaining.pop() {
Some(batch) => Ok(vec![batch]),
None => Err(SourceReason::EOF.err_detail("no more batches")),
}
}
fn identifier(&self) -> &str {
"minimal"
}
}
#[test]
fn lifecycle_defaults_are_no_ops_and_idempotent() {
let mut source = MinimalSource { remaining: vec![] };
block_on(source.start()).expect("default start() must succeed");
block_on(source.close()).expect("default close() must succeed");
block_on(source.close()).expect("close() must stay safe when called repeatedly");
assert_eq!(source.identifier(), "minimal");
}
#[test]
fn batches_pass_through_unchanged() {
let mut source = MinimalSource {
remaining: vec![batch(vec![1, 2, 3])],
};
let got = block_on(source.receive_batch()).expect("the batch should be produced");
assert_eq!(got.len(), 1);
assert_eq!(got[0].num_rows(), 3);
assert_eq!(got[0].num_columns(), 1);
}
#[test]
fn exhausted_source_reports_eof() {
let mut source = MinimalSource { remaining: vec![] };
let err = block_on(source.receive_batch()).expect_err("an exhausted source must error");
assert_eq!(err.reason(), &SourceReason::EOF);
assert_eq!(err.detail().as_deref(), Some("no more batches"));
}
#[test]
fn a_source_may_report_no_data_before_yielding_batches() {
struct PollingSource {
polls: usize,
batch: Option<RecordBatch>,
}
#[async_trait]
impl BatchSource for PollingSource {
async fn receive_batch(&mut self) -> SourceResult<Vec<RecordBatch>> {
self.polls += 1;
match self.polls {
1 => Ok(vec![]),
2 => Ok(vec![self.batch.take().expect("the batch is still pending")]),
_ => Err(SourceReason::EOF.err_detail("stream ended")),
}
}
fn identifier(&self) -> &str {
"polling"
}
}
let mut source = PollingSource {
polls: 0,
batch: Some(batch(vec![9])),
};
assert!(
block_on(source.receive_batch())
.expect("first poll is Ok")
.is_empty(),
"an empty Vec means 'no data right now'"
);
assert_eq!(block_on(source.receive_batch()).expect("data").len(), 1);
assert_eq!(
block_on(source.receive_batch())
.expect_err("then EOF")
.reason(),
&SourceReason::EOF
);
}
#[test]
fn a_source_is_usable_behind_a_trait_object() {
let mut source: Box<dyn BatchSource> = Box::new(MinimalSource {
remaining: vec![batch(vec![7])],
});
let got = block_on(source.receive_batch()).expect("the batch should be produced");
assert_eq!(got[0].num_rows(), 1);
block_on(source.start()).expect("default start() must succeed");
block_on(source.close()).expect("default close() must succeed");
}
}