use faucet_common_gcs::GcsCredentials;
use faucet_core::DEFAULT_BATCH_SIZE;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum GcsSinkFormat {
#[default]
JsonLines,
#[cfg(feature = "arrow")]
Parquet,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GcsSinkConfig {
pub bucket: String,
pub prefix: String,
#[serde(default)]
pub format: GcsSinkFormat,
#[serde(default)]
pub auth: GcsCredentials,
#[serde(default = "default_file_extension")]
pub file_extension: String,
pub max_records_per_file: Option<usize>,
#[serde(default = "default_concurrency")]
pub concurrency: usize,
#[serde(default = "default_batch_size")]
pub batch_size: usize,
pub storage_host: Option<String>,
#[cfg(feature = "compression")]
#[serde(default)]
pub compression: faucet_core::CompressionConfig,
}
fn default_file_extension() -> String {
".jsonl".to_string()
}
fn default_batch_size() -> usize {
DEFAULT_BATCH_SIZE
}
fn default_concurrency() -> usize {
10
}
impl GcsSinkConfig {
pub fn new(bucket: impl Into<String>) -> Self {
Self {
bucket: bucket.into(),
prefix: String::new(),
format: GcsSinkFormat::default(),
auth: GcsCredentials::default(),
file_extension: default_file_extension(),
max_records_per_file: None,
concurrency: default_concurrency(),
batch_size: default_batch_size(),
storage_host: None,
#[cfg(feature = "compression")]
compression: faucet_core::CompressionConfig::Auto,
}
}
pub fn prefix(mut self, p: impl Into<String>) -> Self {
self.prefix = p.into();
self
}
pub fn format(mut self, format: GcsSinkFormat) -> Self {
self.format = format;
self
}
pub fn auth(mut self, c: GcsCredentials) -> Self {
self.auth = c;
self
}
pub fn file_extension(mut self, ext: impl Into<String>) -> Self {
self.file_extension = ext.into();
self
}
pub fn max_records_per_file(mut self, n: usize) -> Self {
self.max_records_per_file = Some(n);
self
}
pub fn concurrency(mut self, n: usize) -> Self {
self.concurrency = n;
self
}
pub fn with_batch_size(mut self, n: usize) -> Self {
self.batch_size = n;
self
}
pub fn storage_host(mut self, h: impl Into<String>) -> Self {
self.storage_host = Some(h.into());
self
}
#[cfg(feature = "compression")]
pub fn compression(mut self, c: faucet_core::CompressionConfig) -> Self {
self.compression = c;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults() {
let c = GcsSinkConfig::new("b");
assert_eq!(c.bucket, "b");
assert_eq!(c.prefix, "");
assert!(matches!(c.auth, GcsCredentials::ApplicationDefault));
assert_eq!(c.file_extension, ".jsonl");
assert!(c.max_records_per_file.is_none());
assert_eq!(c.concurrency, 10);
assert_eq!(c.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
assert!(c.storage_host.is_none());
}
#[test]
fn builder_methods() {
let c = GcsSinkConfig::new("b")
.prefix("out/")
.file_extension(".ndjson")
.max_records_per_file(500)
.concurrency(4)
.with_batch_size(0)
.storage_host("http://localhost:4443");
assert_eq!(c.prefix, "out/");
assert_eq!(c.file_extension, ".ndjson");
assert_eq!(c.max_records_per_file, Some(500));
assert_eq!(c.concurrency, 4);
assert_eq!(c.batch_size, 0);
assert_eq!(c.storage_host.as_deref(), Some("http://localhost:4443"));
}
#[test]
fn batch_size_sentinel_accepted_and_above_max_rejected() {
assert!(faucet_core::validate_batch_size(0).is_ok());
assert!(faucet_core::validate_batch_size(faucet_core::MAX_BATCH_SIZE + 1).is_err());
}
#[test]
fn batch_size_defaults_when_omitted_from_json() {
let json = r#"{
"bucket": "b",
"prefix": "p/",
"max_records_per_file": null,
"concurrency": 10,
"storage_host": null
}"#;
let c: GcsSinkConfig = serde_json::from_str(json).unwrap();
assert_eq!(c.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
}
#[cfg(feature = "compression")]
#[test]
fn compression_default_is_auto() {
let cfg = GcsSinkConfig::new("bucket");
assert_eq!(cfg.compression, faucet_core::CompressionConfig::Auto);
}
#[cfg(feature = "compression")]
#[test]
fn compression_config_round_trips() {
let json = r#"{
"bucket": "b",
"prefix": "",
"file_extension": ".jsonl.gz",
"max_records_per_file": null,
"concurrency": 1,
"batch_size": 0,
"storage_host": null,
"compression": "gzip"
}"#;
let cfg: GcsSinkConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.compression, faucet_core::CompressionConfig::Gzip);
}
#[cfg(feature = "arrow")]
#[test]
fn format_defaults_json_lines_and_builder_sets_parquet() {
assert_eq!(GcsSinkConfig::new("b").format, GcsSinkFormat::JsonLines);
let cfg = GcsSinkConfig::new("b").format(GcsSinkFormat::Parquet);
assert_eq!(cfg.format, GcsSinkFormat::Parquet);
}
}