use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const DEFAULT_BATCH_SIZE: usize = faucet_core::DEFAULT_BATCH_SIZE;
pub const DEFAULT_CONCURRENCY: usize = 4;
fn default_batch_size() -> usize {
DEFAULT_BATCH_SIZE
}
fn default_concurrency() -> usize {
DEFAULT_CONCURRENCY
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ParquetSourceConfig {
pub source: ParquetLocation,
#[serde(default = "default_batch_size")]
pub batch_size: usize,
#[serde(default)]
pub columns: Option<Vec<String>>,
#[serde(default = "default_concurrency")]
pub concurrency: usize,
}
impl ParquetSourceConfig {
pub fn new(source: ParquetLocation) -> Self {
Self {
source,
batch_size: DEFAULT_BATCH_SIZE,
columns: None,
concurrency: DEFAULT_CONCURRENCY,
}
}
pub fn local(path: impl Into<String>) -> Self {
Self::new(ParquetLocation::LocalPath { path: path.into() })
}
pub fn glob(pattern: impl Into<String>) -> Self {
Self::new(ParquetLocation::Glob {
pattern: pattern.into(),
})
}
pub fn s3(s3: ParquetS3Config) -> Self {
Self::new(ParquetLocation::S3(s3))
}
pub fn batch_size(mut self, size: usize) -> Self {
self.batch_size = size;
self
}
pub fn with_batch_size(self, batch_size: usize) -> Self {
self.batch_size(batch_size)
}
pub fn columns<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.columns = Some(columns.into_iter().map(Into::into).collect());
self
}
pub fn concurrency(mut self, concurrency: usize) -> Self {
self.concurrency = concurrency;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ParquetLocation {
LocalPath { path: String },
Glob { pattern: String },
S3(ParquetS3Config),
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ParquetS3Config {
pub bucket: String,
#[serde(default)]
pub key: Option<String>,
#[serde(default)]
pub prefix: Option<String>,
#[serde(default)]
pub region: Option<String>,
#[serde(default)]
pub endpoint_url: Option<String>,
}
impl ParquetS3Config {
pub fn object(bucket: impl Into<String>, key: impl Into<String>) -> Self {
Self {
bucket: bucket.into(),
key: Some(key.into()),
prefix: None,
region: None,
endpoint_url: None,
}
}
pub fn prefix(bucket: impl Into<String>, prefix: impl Into<String>) -> Self {
Self {
bucket: bucket.into(),
key: None,
prefix: Some(prefix.into()),
region: None,
endpoint_url: None,
}
}
pub fn region(mut self, region: impl Into<String>) -> Self {
self.region = Some(region.into());
self
}
pub fn endpoint_url(mut self, url: impl Into<String>) -> Self {
self.endpoint_url = Some(url.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_sensible() {
let cfg = ParquetSourceConfig::local("/tmp/data.parquet");
assert_eq!(cfg.batch_size, DEFAULT_BATCH_SIZE);
assert_eq!(cfg.concurrency, DEFAULT_CONCURRENCY);
assert!(cfg.columns.is_none());
assert!(matches!(cfg.source, ParquetLocation::LocalPath { .. }));
}
#[test]
fn builder_methods_compose() {
let cfg = ParquetSourceConfig::glob("/tmp/*.parquet")
.batch_size(2048)
.concurrency(8)
.columns(["id", "name"]);
assert_eq!(cfg.batch_size, 2048);
assert_eq!(cfg.concurrency, 8);
assert_eq!(
cfg.columns.as_deref(),
Some(&["id".to_string(), "name".to_string()][..])
);
}
#[test]
fn s3_object_and_prefix_variants() {
let by_key = ParquetS3Config::object("my-bucket", "events/2024/data.parquet");
assert_eq!(by_key.key.as_deref(), Some("events/2024/data.parquet"));
assert!(by_key.prefix.is_none());
let by_prefix = ParquetS3Config::prefix("my-bucket", "events/2024/")
.region("us-east-1")
.endpoint_url("http://localhost:9000");
assert!(by_prefix.key.is_none());
assert_eq!(by_prefix.prefix.as_deref(), Some("events/2024/"));
assert_eq!(by_prefix.region.as_deref(), Some("us-east-1"));
assert_eq!(
by_prefix.endpoint_url.as_deref(),
Some("http://localhost:9000")
);
}
#[test]
fn batch_size_default_via_serde() {
let cfg: ParquetSourceConfig = serde_json::from_value(serde_json::json!({
"source": { "type": "local_path", "path": "/tmp/x.parquet" }
}))
.unwrap();
assert_eq!(cfg.batch_size, DEFAULT_BATCH_SIZE);
assert_eq!(cfg.concurrency, DEFAULT_CONCURRENCY);
assert!(cfg.columns.is_none());
}
#[test]
fn schema_generates_without_panicking() {
let _ = faucet_core::schema_for!(ParquetSourceConfig);
}
#[test]
fn batch_size_defaults_to_faucet_core_default_batch_size() {
let cfg = ParquetSourceConfig::local("/tmp/x.parquet");
assert_eq!(cfg.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
}
#[test]
fn with_batch_size_overrides_default() {
let cfg = ParquetSourceConfig::local("/tmp/x.parquet").with_batch_size(500);
assert_eq!(cfg.batch_size, 500);
}
#[test]
fn batch_size_zero_is_accepted_as_no_batching_sentinel() {
let cfg = ParquetSourceConfig::local("/tmp/x.parquet").with_batch_size(0);
assert_eq!(cfg.batch_size, 0);
assert!(faucet_core::validate_batch_size(cfg.batch_size).is_ok());
}
#[test]
fn batch_size_above_max_is_rejected_by_validate_batch_size() {
let cfg = ParquetSourceConfig::local("/tmp/x.parquet")
.with_batch_size(faucet_core::MAX_BATCH_SIZE + 1);
assert!(faucet_core::validate_batch_size(cfg.batch_size).is_err());
}
#[test]
fn batch_size_deserializes_from_json() {
let json = r#"{
"source": { "type": "local_path", "path": "/tmp/x.parquet" },
"batch_size": 250
}"#;
let cfg: ParquetSourceConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.batch_size, 250);
}
#[test]
fn batch_size_zero_deserializes_from_json() {
let json = r#"{
"source": { "type": "local_path", "path": "/tmp/x.parquet" },
"batch_size": 0
}"#;
let cfg: ParquetSourceConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.batch_size, 0);
}
}