use crate::deser::{DecoderCore, JsonSerdeDeserializer, JsonValueDeserializer};
use crate::metrics::JsonDeserMetrics;
use serde::Deserialize;
use serde::de::DeserializeOwned;
use spate_core::config::ComponentConfig;
use spate_core::framing::FramingContract;
use spate_core::metrics::{Meter, SharedString};
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum JsonFraming {
#[default]
Single,
Ndjson,
Array,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum OnError {
#[default]
Skip,
Fail,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct JsonSettings {
pub framing: JsonFraming,
pub on_error: OnError,
pub reject_duplicate_keys: bool,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum JsonConfigError {
#[error(transparent)]
Config(#[from] spate_core::config::ConfigError),
#[error(
"the source frames one record per payload, but the JSON deserializer is set \
to `framing: {framing:?}` — remove `framing` (or set it to `single`), or use \
a whole-object source format that lets the deserializer frame"
)]
DoubleFraming {
framing: JsonFraming,
},
}
#[derive(Clone, Debug)]
pub struct JsonDeserializerBuilder {
settings: JsonSettings,
metrics: Option<JsonDeserMetrics>,
}
impl JsonDeserializerBuilder {
pub fn from_component(cfg: &ComponentConfig) -> Result<Self, JsonConfigError> {
let settings: JsonSettings = cfg.deserialize_into()?;
Ok(Self::from_settings(settings))
}
#[must_use]
pub fn from_settings(settings: JsonSettings) -> Self {
JsonDeserializerBuilder {
settings,
metrics: None,
}
}
pub fn for_source_framing(self, contract: FramingContract) -> Result<Self, JsonConfigError> {
match contract {
FramingContract::PerRecord if self.settings.framing != JsonFraming::Single => {
Err(JsonConfigError::DoubleFraming {
framing: self.settings.framing,
})
}
_ => Ok(self),
}
}
#[must_use]
pub fn with_metrics(
mut self,
pipeline: impl Into<SharedString>,
component: impl Into<SharedString>,
) -> Self {
let meter = Meter::with_namespace("json_deser", pipeline, component, "deserializer");
self.metrics = Some(JsonDeserMetrics::new(&meter));
self
}
#[must_use]
pub fn build_serde<T>(&self) -> JsonSerdeDeserializer<T>
where
T: DeserializeOwned + Send + 'static,
{
JsonSerdeDeserializer::new(self.core())
}
#[must_use]
pub fn build_value(&self) -> JsonValueDeserializer {
JsonValueDeserializer::new(self.core())
}
fn core(&self) -> DecoderCore {
DecoderCore {
framing: self.settings.framing,
on_error: self.settings.on_error,
reject_duplicate_keys: self.settings.reject_duplicate_keys,
metrics: self.metrics.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use spate_core::config::{ComponentConfig, YamlValue};
fn component(yaml: &str) -> ComponentConfig {
let raw: YamlValue = serde_yaml::from_str(yaml).unwrap();
ComponentConfig::new("json", raw)
}
#[test]
fn defaults_are_single_skip_no_dedup() {
let s = JsonSettings::default();
assert_eq!(s.framing, JsonFraming::Single);
assert_eq!(s.on_error, OnError::Skip);
assert!(!s.reject_duplicate_keys);
}
#[test]
fn parses_a_full_section() {
let cfg = component("framing: ndjson\non_error: fail\nreject_duplicate_keys: true\n");
let b = JsonDeserializerBuilder::from_component(&cfg).unwrap();
assert_eq!(b.settings.framing, JsonFraming::Ndjson);
assert_eq!(b.settings.on_error, OnError::Fail);
assert!(b.settings.reject_duplicate_keys);
}
#[test]
fn empty_section_uses_defaults() {
let cfg = ComponentConfig::new("json", YamlValue::Null);
let b = JsonDeserializerBuilder::from_component(&cfg).unwrap();
assert_eq!(b.settings.framing, JsonFraming::Single);
}
#[test]
fn unknown_fields_are_rejected() {
let cfg = component("framing: single\nbogus: 1\n");
let err = JsonDeserializerBuilder::from_component(&cfg).unwrap_err();
assert!(err.to_string().contains("bogus"), "{err}");
}
#[test]
fn unknown_framing_is_rejected() {
let cfg = component("framing: yaml\n");
let err = JsonDeserializerBuilder::from_component(&cfg).unwrap_err();
assert!(err.to_string().to_lowercase().contains("framing"), "{err}");
}
#[test]
fn per_record_source_derives_single_and_rejects_double_framing() {
let b =
JsonDeserializerBuilder::from_component(&ComponentConfig::new("json", YamlValue::Null))
.unwrap()
.for_source_framing(FramingContract::PerRecord)
.unwrap();
assert_eq!(b.settings.framing, JsonFraming::Single);
JsonDeserializerBuilder::from_component(&component("framing: single\n"))
.unwrap()
.for_source_framing(FramingContract::PerRecord)
.unwrap();
for framing in ["ndjson", "array"] {
let err = JsonDeserializerBuilder::from_component(&component(&format!(
"framing: {framing}\n"
)))
.unwrap()
.for_source_framing(FramingContract::PerRecord)
.unwrap_err();
assert!(
matches!(err, JsonConfigError::DoubleFraming { .. }),
"{framing}: {err}"
);
}
}
#[test]
fn whole_payload_source_honors_configured_framing() {
for framing in ["single", "ndjson", "array"] {
JsonDeserializerBuilder::from_component(&component(&format!("framing: {framing}\n")))
.unwrap()
.for_source_framing(FramingContract::WholePayload)
.unwrap();
}
}
}