use std::collections::VecDeque;
use std::io::{BufRead, Write};
use clap::Parser;
#[derive(Parser)]
#[command(
about = "Buffer a line stream and emit the last N values as a JSON array",
long_about = None,
)]
struct Args {
#[arg(short = 'n', long = "count", default_value_t = 60)]
count: usize,
}
fn parse_line(line: &str) -> serde_json::Value {
serde_json::from_str(line).unwrap_or_else(|_| serde_json::Value::String(line.to_string()))
}
fn push(window: &mut VecDeque<serde_json::Value>, line: &str, count: usize) {
if count == 0 {
return;
}
if window.len() == count {
window.pop_front();
}
window.push_back(parse_line(line));
}
fn window_json(window: &VecDeque<serde_json::Value>) -> String {
serde_json::Value::Array(window.iter().cloned().collect()).to_string()
}
fn run(input: impl BufRead, mut output: impl Write, count: usize) -> std::io::Result<()> {
let mut window: VecDeque<serde_json::Value> = VecDeque::with_capacity(count);
for line in input.lines() {
push(&mut window, &line?, count);
writeln!(output, "{}", window_json(&window))?;
output.flush()?;
}
Ok(())
}
fn main() -> std::io::Result<()> {
let args = Args::parse();
let stdin = std::io::stdin();
let stdout = std::io::stdout();
run(stdin.lock(), stdout.lock(), args.count)
}
#[cfg(test)]
mod tests {
use super::*;
fn accumulate(input: &str, count: usize) -> Vec<String> {
let mut out = Vec::new();
run(input.as_bytes(), &mut out, count).expect("run failed");
String::from_utf8(out)
.expect("output was not utf8")
.lines()
.map(str::to_string)
.collect()
}
#[test]
fn emits_one_array_per_input_line() {
let out = accumulate("1\n2\n3\n", 60);
assert_eq!(out, vec!["[1]", "[1,2]", "[1,2,3]"]);
}
#[test]
fn window_is_oldest_first() {
let out = accumulate("1\n2\n3\n", 60);
assert_eq!(out.last().unwrap(), "[1,2,3]");
}
#[test]
fn evicts_the_oldest_value_once_full() {
let out = accumulate("1\n2\n3\n4\n", 2);
assert_eq!(out, vec!["[1]", "[1,2]", "[2,3]", "[3,4]"]);
}
#[test]
fn emits_a_partial_window_before_it_fills() {
let out = accumulate("1\n", 60);
assert_eq!(out, vec!["[1]"]);
}
#[test]
fn json_lines_are_embedded_as_json() {
let out = accumulate(r#"{"a":1}"#.to_string().as_str(), 60);
assert_eq!(out, vec![r#"[{"a":1}]"#]);
}
#[test]
fn non_json_lines_are_embedded_as_strings() {
let out = accumulate("hello\n", 60);
assert_eq!(out, vec![r#"["hello"]"#]);
}
#[test]
fn bare_numbers_are_not_quoted() {
let out = accumulate("0.41\n", 60);
assert_eq!(out, vec!["[0.41]"]);
}
#[test]
fn json_and_non_json_lines_can_share_a_window() {
let out = accumulate("1\noops\n", 60);
assert_eq!(out.last().unwrap(), r#"[1,"oops"]"#);
}
#[test]
fn a_line_that_is_a_json_string_stays_one_value() {
let out = accumulate(r#""already a string""#, 60);
assert_eq!(out, vec![r#"["already a string"]"#]);
}
#[test]
fn window_never_exceeds_count() {
let input = (0..100)
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join("\n");
let out = accumulate(&input, 3);
let last: serde_json::Value = serde_json::from_str(out.last().unwrap()).unwrap();
assert_eq!(last, serde_json::json!([97, 98, 99]));
}
#[test]
fn empty_input_emits_nothing() {
assert!(accumulate("", 60).is_empty());
}
#[test]
fn a_count_of_zero_emits_empty_windows() {
let out = accumulate("1\n2\n", 0);
assert_eq!(out, vec!["[]", "[]"]);
}
}