#![allow(clippy::print_stdout, clippy::print_stderr)]
use serde::Deserialize;
use spate::json::{JsonDeserializerBuilder, JsonFraming, JsonSettings, OnError};
use spate::prelude::*;
use spate::source::LaneId;
use spate_test::{TestEncoder, capture_sink, memory_source};
use std::time::{Duration, Instant};
const CONFIG: &str = r#"
pipeline: { name: json-ndjson-demo, threads: 1 }
checkpoint: { interval: 200ms }
source: { memory: {} }
sink: { capture: {} }
"#;
#[derive(Debug, Deserialize)]
struct Event {
#[expect(dead_code, reason = "decoded but not projected into the sink row")]
id: u64,
name: String,
value: i64,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
spate::telemetry::init(spate::telemetry::LogFormat::Pretty, "info");
let pipeline = Pipeline::from_config(PipelineConfig::from_str(CONFIG)?)?;
let (source, handle) = memory_source();
let (sink, script) = capture_sink(1, 1);
let pool_cfg = {
let mut cfg = SinkPoolConfig::default();
cfg.batch.linger = Duration::from_millis(50); cfg
};
let sink = sink.with_pool_config(pool_cfg);
let runtime = pipeline
.sink(sink)?
.chains(|ctx| {
let chunk_cfg = ctx.chunk();
let deser = JsonDeserializerBuilder::from_settings(JsonSettings {
framing: JsonFraming::Ndjson,
on_error: OnError::Skip,
reject_duplicate_keys: false,
})
.with_metrics(ctx.pipeline.clone(), "main")
.build_serde::<Event>();
chain_owned::<Event, _>(deser)
.with_metrics(ctx.pipeline, "main")
.filter(|e: &Event| e.value >= 0)
.map(|e: Event| format!("{}={}", e.name, e.value).into_bytes())
.sink(
TestEncoder,
KeyHashRouter,
chunk_cfg,
ctx.queues,
ctx.budget,
)
.build()
})
.runtime_options(RuntimeOptions {
handle_signals: false,
..RuntimeOptions::default()
})
.into_runtime(source)?;
let shutdown = runtime.shutdown_handle();
let join = std::thread::spawn(move || runtime.run());
let p0 = PartitionId(0);
handle.assign_lanes(&[(LaneId(0), p0)]);
let payload = concat!(
"{\"id\":1,\"name\":\"alpha\",\"value\":10}\n",
"NOT JSON\n",
"{\"id\":2,\"name\":\"beta\",\"value\":-5}\n",
"{\"id\":3,\"name\":\"gamma\",\"value\":30}"
)
.as_bytes();
let last = handle.push(p0, Some(b"demo"), payload);
let deadline = Instant::now() + Duration::from_secs(10);
while handle.last_committed(p0) != Some(last + 1) {
assert!(Instant::now() < deadline, "commit not observed in time");
std::thread::sleep(Duration::from_millis(20));
}
shutdown.trigger();
let report = join.join().expect("pipeline thread")?;
let rows: Vec<String> = script
.writes()
.iter()
.flat_map(|w| spate_test::decode_rows(&w.payload))
.map(|r| String::from_utf8_lossy(&r).into_owned())
.collect();
println!("\npipeline exit: {:?}", report.state);
println!("final watermarks: {:?}", report.final_watermarks);
println!("rows written ({}): {rows:?}", rows.len());
assert_eq!(
rows.len(),
2,
"4 NDJSON lines: 1 malformed skipped, 1 negative filtered"
);
assert!(rows.contains(&"alpha=10".to_string()));
assert!(rows.contains(&"gamma=30".to_string()));
Ok(())
}