1use crate::deser::{DecoderCore, JsonSerdeDeserializer, JsonValueDeserializer};
16use crate::metrics::JsonDeserMetrics;
17use serde::Deserialize;
18use serde::de::DeserializeOwned;
19use spate_core::config::ComponentConfig;
20use spate_core::framing::FramingContract;
21use spate_core::metrics::{Meter, SharedString};
22
23#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
25#[serde(rename_all = "snake_case")]
26pub enum JsonFraming {
27 #[default]
30 Single,
31 Ndjson,
34 Array,
37}
38
39#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
41#[serde(rename_all = "snake_case")]
42pub enum OnError {
43 #[default]
47 Skip,
48 Fail,
55}
56
57#[derive(Clone, Debug, Default, Deserialize)]
59#[serde(deny_unknown_fields, default)]
60pub struct JsonSettings {
61 pub framing: JsonFraming,
63 pub on_error: OnError,
65 pub reject_duplicate_keys: bool,
69}
70
71#[derive(Debug, thiserror::Error)]
73#[non_exhaustive]
74pub enum JsonConfigError {
75 #[error(transparent)]
77 Config(#[from] spate_core::config::ConfigError),
78 #[error(
81 "the source frames one record per payload, but the JSON deserializer is set \
82 to `framing: {framing:?}` — remove `framing` (or set it to `single`), or use \
83 a whole-object source format that lets the deserializer frame"
84 )]
85 DoubleFraming {
86 framing: JsonFraming,
88 },
89}
90
91#[derive(Clone, Debug)]
94pub struct JsonDeserializerBuilder {
95 settings: JsonSettings,
96 metrics: Option<JsonDeserMetrics>,
97}
98
99impl JsonDeserializerBuilder {
100 pub fn from_component(cfg: &ComponentConfig) -> Result<Self, JsonConfigError> {
102 let settings: JsonSettings = cfg.deserialize_into()?;
103 Ok(Self::from_settings(settings))
104 }
105
106 #[must_use]
108 pub fn from_settings(settings: JsonSettings) -> Self {
109 JsonDeserializerBuilder {
110 settings,
111 metrics: None,
112 }
113 }
114
115 pub fn for_source_framing(self, contract: FramingContract) -> Result<Self, JsonConfigError> {
129 match contract {
130 FramingContract::PerRecord if self.settings.framing != JsonFraming::Single => {
131 Err(JsonConfigError::DoubleFraming {
132 framing: self.settings.framing,
133 })
134 }
135 _ => Ok(self),
136 }
137 }
138
139 #[must_use]
144 pub fn with_metrics(
145 mut self,
146 pipeline: impl Into<SharedString>,
147 component: impl Into<SharedString>,
148 ) -> Self {
149 let meter = Meter::with_namespace("json_deser", pipeline, component, "deserializer");
150 self.metrics = Some(JsonDeserMetrics::new(&meter));
151 self
152 }
153
154 #[must_use]
156 pub fn build_serde<T>(&self) -> JsonSerdeDeserializer<T>
157 where
158 T: DeserializeOwned + Send + 'static,
159 {
160 JsonSerdeDeserializer::new(self.core())
161 }
162
163 #[must_use]
165 pub fn build_value(&self) -> JsonValueDeserializer {
166 JsonValueDeserializer::new(self.core())
167 }
168
169 fn core(&self) -> DecoderCore {
170 DecoderCore {
171 framing: self.settings.framing,
172 on_error: self.settings.on_error,
173 reject_duplicate_keys: self.settings.reject_duplicate_keys,
174 metrics: self.metrics.clone(),
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use spate_core::config::{ComponentConfig, YamlValue};
183
184 fn component(yaml: &str) -> ComponentConfig {
185 let raw: YamlValue = serde_yaml::from_str(yaml).unwrap();
186 ComponentConfig::new("json", raw)
187 }
188
189 #[test]
190 fn defaults_are_single_skip_no_dedup() {
191 let s = JsonSettings::default();
192 assert_eq!(s.framing, JsonFraming::Single);
193 assert_eq!(s.on_error, OnError::Skip);
194 assert!(!s.reject_duplicate_keys);
195 }
196
197 #[test]
198 fn parses_a_full_section() {
199 let cfg = component("framing: ndjson\non_error: fail\nreject_duplicate_keys: true\n");
200 let b = JsonDeserializerBuilder::from_component(&cfg).unwrap();
201 assert_eq!(b.settings.framing, JsonFraming::Ndjson);
202 assert_eq!(b.settings.on_error, OnError::Fail);
203 assert!(b.settings.reject_duplicate_keys);
204 }
205
206 #[test]
207 fn empty_section_uses_defaults() {
208 let cfg = ComponentConfig::new("json", YamlValue::Null);
210 let b = JsonDeserializerBuilder::from_component(&cfg).unwrap();
211 assert_eq!(b.settings.framing, JsonFraming::Single);
212 }
213
214 #[test]
215 fn unknown_fields_are_rejected() {
216 let cfg = component("framing: single\nbogus: 1\n");
217 let err = JsonDeserializerBuilder::from_component(&cfg).unwrap_err();
218 assert!(err.to_string().contains("bogus"), "{err}");
219 }
220
221 #[test]
222 fn unknown_framing_is_rejected() {
223 let cfg = component("framing: yaml\n");
224 let err = JsonDeserializerBuilder::from_component(&cfg).unwrap_err();
225 assert!(err.to_string().to_lowercase().contains("framing"), "{err}");
226 }
227
228 #[test]
229 fn per_record_source_derives_single_and_rejects_double_framing() {
230 let b =
232 JsonDeserializerBuilder::from_component(&ComponentConfig::new("json", YamlValue::Null))
233 .unwrap()
234 .for_source_framing(FramingContract::PerRecord)
235 .unwrap();
236 assert_eq!(b.settings.framing, JsonFraming::Single);
237
238 JsonDeserializerBuilder::from_component(&component("framing: single\n"))
240 .unwrap()
241 .for_source_framing(FramingContract::PerRecord)
242 .unwrap();
243
244 for framing in ["ndjson", "array"] {
246 let err = JsonDeserializerBuilder::from_component(&component(&format!(
247 "framing: {framing}\n"
248 )))
249 .unwrap()
250 .for_source_framing(FramingContract::PerRecord)
251 .unwrap_err();
252 assert!(
253 matches!(err, JsonConfigError::DoubleFraming { .. }),
254 "{framing}: {err}"
255 );
256 }
257 }
258
259 #[test]
260 fn whole_payload_source_honors_configured_framing() {
261 for framing in ["single", "ndjson", "array"] {
262 JsonDeserializerBuilder::from_component(&component(&format!("framing: {framing}\n")))
263 .unwrap()
264 .for_source_framing(FramingContract::WholePayload)
265 .unwrap();
266 }
267 }
268}