use anyhow::Result;
use arete_sdk::Frame;
use std::io::{self, BufWriter, Write};
pub enum OutputMode {
Raw,
Merged,
NoDna,
}
pub struct StdoutWriter {
inner: BufWriter<io::Stdout>,
stack: Option<String>,
protocol: Option<String>,
prefix: Option<String>,
}
impl StdoutWriter {
pub fn new() -> Self {
Self {
inner: BufWriter::new(io::stdout()),
stack: None,
protocol: None,
prefix: None,
}
}
pub fn with_stack(mut self, stack: Option<String>) -> Self {
self.stack = stack;
self
}
pub fn with_protocol(mut self, protocol: Option<String>) -> Self {
self.protocol = protocol;
self
}
pub fn with_prefix(mut self, prefix: Option<String>) -> Self {
self.prefix = prefix;
self
}
pub fn writeln(&mut self, line: &str) -> Result<()> {
match &self.prefix {
Some(prefix) => writeln!(self.inner, "{prefix} {line}")?,
None => writeln!(self.inner, "{line}")?,
}
self.inner.flush()?;
Ok(())
}
}
pub fn feed_label(stack: Option<&str>, view: &str) -> String {
match stack {
Some(stack) => format!("{stack} \u{b7} {view}"),
None => view.to_string(),
}
}
impl Drop for StdoutWriter {
fn drop(&mut self) {
let _ = self.inner.flush();
}
}
pub fn print_raw_frame(out: &mut StdoutWriter, frame: &Frame, view: &str) -> Result<()> {
let line = match &out.stack {
Some(stack) => {
let mut value = serde_json::to_value(frame)?;
if let Some(object) = value.as_object_mut() {
if let Some(protocol) = &out.protocol {
object.insert("protocol".into(), serde_json::json!(protocol));
}
object.insert("stack".into(), serde_json::json!(stack));
object.insert("view".into(), serde_json::json!(view));
}
serde_json::to_string(&value)?
}
None => serde_json::to_string(frame)?,
};
out.writeln(&line)
}
fn labelled(out: &StdoutWriter, mut value: serde_json::Value) -> serde_json::Value {
if let Some(object) = value.as_object_mut() {
if let Some(protocol) = &out.protocol {
object.insert("protocol".into(), serde_json::json!(protocol));
}
if let Some(stack) = &out.stack {
object.insert("stack".into(), serde_json::json!(stack));
}
}
value
}
pub fn print_entity_update(
out: &mut StdoutWriter,
view: &str,
key: &str,
op: &str,
data: &serde_json::Value,
) -> Result<()> {
let output = labelled(
out,
serde_json::json!({
"view": view,
"key": key,
"op": op,
"data": data,
}),
);
let line = serde_json::to_string(&output)?;
out.writeln(&line)
}
pub fn print_removal(out: &mut StdoutWriter, view: &str, key: &str, op: &str) -> Result<()> {
let output = labelled(
out,
serde_json::json!({
"view": view,
"key": key,
"op": op,
"data": null,
}),
);
let line = serde_json::to_string(&output)?;
out.writeln(&line)
}
pub fn print_count(count: u64) -> Result<()> {
eprint!("\rUpdates: {} ", count); std::io::stderr().flush()?;
Ok(())
}
pub fn finalize_count() {
eprintln!();
}
pub fn emit_no_dna_event(
out: &mut StdoutWriter,
action: &str,
view: &str,
data: &serde_json::Value,
update_count: u64,
entity_count: u64,
) -> Result<()> {
let output = serde_json::json!({
"schema": "no-dna/v1",
"tool": "a4-stream",
"action": action,
"status": if action == "disconnected" || action == "error" { "done" } else { "streaming" },
"data": labelled(out, serde_json::json!({
"view": view,
"payload": data,
})),
"meta": {
"update_count": update_count,
"entities_tracked": entity_count,
},
});
let line = serde_json::to_string(&output)?;
out.writeln(&line)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn feeds_are_labelled_stack_dot_view() {
assert_eq!(
feed_label(Some("tip-jar"), "ProtocolActivity/list"),
"tip-jar \u{b7} ProtocolActivity/list"
);
assert_eq!(feed_label(None, "Ore/list"), "Ore/list");
}
#[test]
fn a_labelled_writer_adds_the_stack() {
let out = StdoutWriter::new().with_stack(Some("tip-jar".into()));
assert_eq!(
labelled(&out, serde_json::json!({"view": "A/list"})),
serde_json::json!({"view": "A/list", "stack": "tip-jar"})
);
let protocol = StdoutWriter::new()
.with_stack(Some("tip-jar".into()))
.with_protocol(Some("localTipJar".into()));
assert_eq!(
labelled(&protocol, serde_json::json!({"view": "A/list"})),
serde_json::json!({"view": "A/list", "stack": "tip-jar", "protocol": "localTipJar"})
);
let plain = StdoutWriter::new();
assert_eq!(
labelled(&plain, serde_json::json!({"view": "A/list"})),
serde_json::json!({"view": "A/list"})
);
}
}