use faucet_common_pubsub::PubsubConnection;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ValueFormat {
#[default]
Json,
String,
Bytes,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OrderingKey {
#[default]
None,
Field {
name: String,
},
Jsonpath {
path: String,
},
}
impl OrderingKey {
pub fn enables_ordering(&self) -> bool {
!matches!(self, OrderingKey::None)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PubsubSinkConfig {
pub topic: String,
#[serde(flatten)]
pub connection: PubsubConnection,
#[serde(default)]
pub value_format: ValueFormat,
#[serde(default)]
pub ordering_key: OrderingKey,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attributes_field: Option<String>,
#[serde(default = "default_batch_size")]
pub batch_size: usize,
#[serde(default = "default_concurrency")]
pub concurrency: usize,
}
pub const MAX_BATCH: usize = 1000;
fn default_batch_size() -> usize {
100
}
fn default_concurrency() -> usize {
4
}
impl PubsubSinkConfig {
pub fn new(topic: impl Into<String>) -> Self {
Self {
topic: topic.into(),
connection: PubsubConnection::default(),
value_format: ValueFormat::default(),
ordering_key: OrderingKey::default(),
attributes_field: None,
batch_size: default_batch_size(),
concurrency: default_concurrency(),
}
}
pub fn validate(&self) -> Result<(), faucet_core::FaucetError> {
use faucet_core::FaucetError;
if self.topic.trim().is_empty() {
return Err(FaucetError::Config(
"pubsub sink: topic must not be empty".into(),
));
}
if self.batch_size == 0 || self.batch_size > MAX_BATCH {
return Err(FaucetError::Config(format!(
"pubsub sink: batch_size must be 1..={MAX_BATCH} (got {})",
self.batch_size
)));
}
if self.concurrency == 0 {
return Err(FaucetError::Config(
"pubsub sink: concurrency must be at least 1".into(),
));
}
match &self.ordering_key {
OrderingKey::Field { name } if name.trim().is_empty() => {
return Err(FaucetError::Config(
"pubsub sink: ordering_key.name must not be empty".into(),
));
}
OrderingKey::Jsonpath { path } if path.trim().is_empty() => {
return Err(FaucetError::Config(
"pubsub sink: ordering_key.path must not be empty".into(),
));
}
_ => {}
}
if let Some(field) = &self.attributes_field
&& field.trim().is_empty()
{
return Err(FaucetError::Config(
"pubsub sink: attributes_field must not be empty when set".into(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_sensible() {
let c = PubsubSinkConfig::new("orders");
c.validate().unwrap();
assert_eq!(c.value_format, ValueFormat::Json);
assert_eq!(c.ordering_key, OrderingKey::None);
assert!(!c.ordering_key.enables_ordering());
assert_eq!(c.batch_size, 100);
assert_eq!(c.concurrency, 4);
}
#[test]
fn ordering_key_enables_flag() {
assert!(OrderingKey::Field { name: "k".into() }.enables_ordering());
assert!(OrderingKey::Jsonpath { path: "a.b".into() }.enables_ordering());
assert!(!OrderingKey::None.enables_ordering());
}
#[test]
fn validation_bounds() {
let mut c = PubsubSinkConfig::new("orders");
c.topic = " ".into();
assert!(c.validate().is_err());
let mut c = PubsubSinkConfig::new("orders");
c.batch_size = 0;
assert!(c.validate().is_err());
c.batch_size = MAX_BATCH + 1;
assert!(c.validate().is_err());
let mut c = PubsubSinkConfig::new("orders");
c.concurrency = 0;
assert!(c.validate().is_err());
let mut c = PubsubSinkConfig::new("orders");
c.ordering_key = OrderingKey::Field { name: " ".into() };
assert!(c.validate().is_err());
c.ordering_key = OrderingKey::Jsonpath { path: "".into() };
assert!(c.validate().is_err());
let mut c = PubsubSinkConfig::new("orders");
c.attributes_field = Some("".into());
assert!(c.validate().is_err());
}
#[test]
fn full_config_parses_from_yaml() {
let yaml = r#"
topic: orders
project_id: my-proj
credentials: { type: application_default }
value_format: json
ordering_key: { type: field, name: customer_id }
attributes_field: __attributes
batch_size: 250
concurrency: 8
"#;
let c: PubsubSinkConfig = serde_yaml::from_str(yaml).unwrap();
c.validate().unwrap();
assert_eq!(
c.ordering_key,
OrderingKey::Field {
name: "customer_id".into()
}
);
assert_eq!(c.attributes_field.as_deref(), Some("__attributes"));
assert_eq!(c.batch_size, 250);
assert_eq!(c.concurrency, 8);
}
}