use arrow::datatypes::Schema;
use arrow::record_batch::RecordBatch;
use async_trait::async_trait;
use std::sync::Arc;
use wf_connector_api::{BatchSource, SourceError, SourceReason, SourceResult};
use wp_connector_api::{DataSource, SourceBatch, SourceError as WpError, SourceReason as WpReason};
use super::ndjson::ndjson_to_record_batch;
use super::payload::payload_to_string;
pub struct FileBatchSource {
key: String,
inner: Box<dyn DataSource>,
schema: Arc<Schema>,
}
impl FileBatchSource {
pub fn new(
key: impl Into<String>,
source: Box<dyn DataSource>,
schema: Arc<Schema>,
) -> Self {
Self { key: key.into(), inner: source, schema }
}
pub async fn from_path(
key: impl Into<String>,
path: impl AsRef<std::path::Path>,
schema: Arc<Schema>,
) -> std::io::Result<Self> {
let source = SimpleFileSource::open(path).await?;
Ok(Self::new(key, Box::new(source), schema))
}
fn convert_batch(&self, events: SourceBatch) -> SourceResult<Vec<RecordBatch>> {
if events.is_empty() {
return Ok(vec![]);
}
let lines: Vec<String> = events.iter().map(|e| payload_to_string(&e.payload)).collect();
match ndjson_to_record_batch(&lines, &self.schema) {
Ok(Some(batch)) => Ok(vec![batch]),
Ok(None) => Ok(vec![]),
Err(e) => Err(SourceReason::Decode.err_detail(e)),
}
}
fn wp_error_to_wf(err: WpError) -> SourceError {
super::error::wp_error_to_wf(err)
}
}
#[async_trait]
impl BatchSource for FileBatchSource {
async fn start(&mut self) -> SourceResult<()> {
self.inner.close().await.ok(); Ok(())
}
async fn receive_batch(&mut self) -> SourceResult<Vec<RecordBatch>> {
match self.inner.receive().await {
Ok(batch) => self.convert_batch(batch),
Err(e) => Err(Self::wp_error_to_wf(e)),
}
}
async fn close(&mut self) -> SourceResult<()> {
self.inner.close().await.ok();
Ok(())
}
fn identifier(&self) -> &str { &self.key }
}
use std::io;
use std::path::Path;
use tokio::io::{AsyncBufReadExt, BufReader};
use wp_connector_api::{SourceEvent, Tags};
use wp_model_core::raw::RawData;
pub struct SimpleFileSource {
lines: tokio::io::Lines<BufReader<tokio::fs::File>>,
key: String,
eof: bool,
}
impl SimpleFileSource {
pub async fn open(path: impl AsRef<Path>) -> io::Result<Self> {
let p = path.as_ref();
let file = tokio::fs::File::open(p).await?;
let reader = BufReader::new(file);
Ok(Self { lines: reader.lines(), key: p.display().to_string(), eof: false })
}
}
#[async_trait]
impl DataSource for SimpleFileSource {
async fn receive(&mut self) -> Result<SourceBatch, WpError> {
if self.eof {
return Err(WpError::from(WpReason::EOF));
}
let mut batch = Vec::new();
for _ in 0..128 {
match self.lines.next_line().await {
Ok(Some(line)) => {
batch.push(SourceEvent::new(
0, &self.key, RawData::from_string(line),
Arc::new(Tags::new()),
));
}
Ok(None) => { self.eof = true; break; }
Err(e) => {
return Err(WpReason::Other.err_detail(e.to_string()));
}
}
}
if batch.is_empty() && self.eof {
return Err(WpError::from(WpReason::EOF));
}
Ok(batch)
}
fn try_receive(&mut self) -> Option<SourceBatch> { None }
fn identifier(&self) -> String { self.key.clone() }
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::datatypes::{DataType, Field};
use std::io::Write;
use tempfile::NamedTempFile;
#[tokio::test]
async fn file_batch_source_identifier() {
let schema = Arc::new(Schema::new(vec![Field::new("msg", DataType::Utf8, true)]));
let src = FileBatchSource::new("test_key",
Box::new(SimpleFileSource::open("Cargo.toml").await.unwrap()), schema);
assert_eq!(src.identifier(), "test_key");
}
#[tokio::test]
async fn file_batch_source_lifecycle() {
let mut tmp = NamedTempFile::new().unwrap();
writeln!(tmp, r#"{{"msg":"hello"}}"#).unwrap();
writeln!(tmp, r#"{{"msg":"world"}}"#).unwrap();
let path = tmp.path().to_path_buf();
let schema = Arc::new(Schema::new(vec![Field::new("msg", DataType::Utf8, true)]));
let mut src = FileBatchSource::new("test",
Box::new(SimpleFileSource::open(&path).await.unwrap()), schema);
src.start().await.unwrap();
let batches = src.receive_batch().await.unwrap();
assert_eq!(batches.len(), 1);
assert_eq!(batches[0].num_rows(), 2);
let result = src.receive_batch().await;
assert!(result.is_err()); src.close().await.unwrap();
src.close().await.unwrap(); }
#[tokio::test]
async fn file_batch_source_empty_file() {
let tmp = NamedTempFile::new().unwrap();
let schema = Arc::new(Schema::new(vec![Field::new("msg", DataType::Utf8, true)]));
let mut src = FileBatchSource::new("empty",
Box::new(SimpleFileSource::open(tmp.path()).await.unwrap()), schema);
src.start().await.unwrap();
assert!(src.receive_batch().await.is_err());
}
#[tokio::test]
async fn from_path_constructor() {
let mut tmp = NamedTempFile::new().unwrap();
writeln!(tmp, r#"{{"msg":"hi"}}"#).unwrap();
let schema = Arc::new(Schema::new(vec![Field::new("msg", DataType::Utf8, true)]));
let mut src = FileBatchSource::from_path("fp", tmp.path(), schema).await.unwrap();
src.start().await.unwrap();
let batches = src.receive_batch().await.unwrap();
assert_eq!(batches[0].num_rows(), 1);
}
}