use serde::Serialize;
use std::any::TypeId;
use std::cell::RefCell;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::rc::Rc;
#[derive(Debug, Serialize)]
pub enum NoEvents {}
pub fn emits_events<E: 'static>() -> bool {
TypeId::of::<E>() != TypeId::of::<NoEvents>()
}
#[derive(Debug, thiserror::Error)]
pub enum EmitError {
#[error("event does not serialize: {0}")]
Serialize(#[from] serde_json::Error),
#[error("{0}")]
Render(String),
#[error("event could not be written: {0}")]
Write(#[from] std::io::Error),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Delivery {
#[default]
Stdout,
File(PathBuf),
Pager(String),
}
impl Delivery {
pub fn path(&self) -> Option<&Path> {
match self {
Delivery::Stdout | Delivery::Pager(_) => None,
Delivery::File(path) => Some(path),
}
}
}
pub trait EventSink {
fn deliver(&self, event: &serde_json::Value) -> Result<(), EmitError>;
fn is_open(&self) -> bool {
true
}
fn record_failure(&self, _error: &EmitError) {}
}
#[derive(Debug)]
struct RunRecord {
records: Vec<serde_json::Value>,
delivery: Delivery,
retain_events: bool,
}
#[derive(Debug, Clone)]
pub struct RunRecorder(Rc<RefCell<RunRecord>>);
impl Default for RunRecorder {
fn default() -> Self {
Self::new()
}
}
impl RunRecorder {
pub fn new() -> Self {
Self::with_event_retention(true)
}
pub fn summary_only() -> Self {
Self::with_event_retention(false)
}
fn with_event_retention(retain_events: bool) -> Self {
Self(Rc::new(RefCell::new(RunRecord {
records: Vec::new(),
delivery: Delivery::default(),
retain_events,
})))
}
pub fn record(&self, value: serde_json::Value) {
self.0.borrow_mut().records.push(value);
}
pub fn retains_events(&self) -> bool {
self.0.borrow().retain_events
}
pub fn set_delivery(&self, delivery: Delivery) {
self.0.borrow_mut().delivery = delivery;
}
pub fn records(&self) -> Vec<serde_json::Value> {
self.0.borrow().records.clone()
}
pub fn delivery(&self) -> Delivery {
self.0.borrow().delivery.clone()
}
}
pub struct Results<E: Serialize> {
recorder: Option<RunRecorder>,
sink: Option<Rc<dyn EventSink>>,
_event: PhantomData<fn(E)>,
}
impl<E: Serialize> std::fmt::Debug for Results<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Results")
.field("recording", &self.recorder.is_some())
.field("writing", &self.sink.is_some())
.finish()
}
}
impl<E: Serialize> Results<E> {
pub fn discarding() -> Self {
Self {
recorder: None,
sink: None,
_event: PhantomData,
}
}
pub fn recording(recorder: RunRecorder) -> Self {
Self {
recorder: Some(recorder),
sink: None,
_event: PhantomData,
}
}
pub fn for_run(recorder: Option<RunRecorder>, sink: Rc<dyn EventSink>) -> Self {
Self {
recorder,
sink: Some(sink),
_event: PhantomData,
}
}
pub fn emit(&mut self, event: E) -> Result<(), EmitError> {
let retaining = self
.recorder
.as_ref()
.filter(|recorder| recorder.retains_events());
let open = self.sink.as_ref().filter(|sink| sink.is_open());
if retaining.is_none() && open.is_none() {
return Ok(());
}
let value = match serde_json::to_value(&event) {
Ok(value) => value,
Err(error) => {
let error = EmitError::from(error);
if let Some(sink) = self.sink.as_ref() {
sink.record_failure(&error);
}
return Err(error);
}
};
if let Some(sink) = open {
sink.deliver(&value)?;
}
if let Some(recorder) = retaining {
recorder.record(value);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct Delivered {
values: RefCell<Vec<serde_json::Value>>,
open: bool,
}
impl EventSink for Delivered {
fn deliver(&self, event: &serde_json::Value) -> Result<(), EmitError> {
self.values.borrow_mut().push(event.clone());
Ok(())
}
fn is_open(&self) -> bool {
self.open
}
}
fn open() -> Rc<Delivered> {
Rc::new(Delivered {
values: RefCell::new(Vec::new()),
open: true,
})
}
#[test]
fn a_recorder_retains_values_in_order() {
let recorder = RunRecorder::new();
recorder.record(serde_json::json!({"n": 1}));
recorder.record(serde_json::json!({"n": 2}));
assert_eq!(
recorder.records(),
vec![serde_json::json!({"n": 1}), serde_json::json!({"n": 2})]
);
}
#[test]
fn a_recorder_carries_the_delivery_decision() {
let recorder = RunRecorder::new();
assert_eq!(recorder.delivery(), Delivery::Stdout);
assert_eq!(recorder.delivery().path(), None);
recorder.set_delivery(Delivery::File(PathBuf::from("out.txt")));
assert_eq!(recorder.delivery().path(), Some(Path::new("out.txt")));
}
#[test]
fn an_emitted_event_is_retained_and_written_in_the_same_call() {
let recorder = RunRecorder::new();
let sink = open();
let mut results = Results::for_run(Some(recorder.clone()), sink.clone());
results
.emit(serde_json::json!({"type": "apply_start"}))
.unwrap();
assert_eq!(recorder.records().len(), 1);
assert_eq!(sink.values.borrow().len(), 1);
}
struct Unserializable;
impl Serialize for Unserializable {
fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
Err(serde::ser::Error::custom("never asked"))
}
}
#[test]
fn a_discarding_channel_keeps_nothing_and_never_serializes() {
Results::discarding().emit(Unserializable).unwrap();
}
#[test]
fn a_summary_only_recorder_writes_every_event_and_retains_none() {
let recorder = RunRecorder::summary_only();
let sink = open();
let mut results = Results::for_run(Some(recorder.clone()), sink.clone());
for n in 0..3 {
results.emit(serde_json::json!({ "n": n })).unwrap();
}
recorder.record(serde_json::json!({"total": 3}));
assert_eq!(sink.values.borrow().len(), 3);
assert_eq!(recorder.records(), vec![serde_json::json!({"total": 3})]);
}
#[test]
fn a_summary_only_recorder_over_a_closed_sink_never_serializes_an_event() {
let closed = Rc::new(Delivered {
values: RefCell::new(Vec::new()),
open: false,
});
let mut results = Results::for_run(Some(RunRecorder::summary_only()), closed);
results.emit(Unserializable).unwrap();
}
#[test]
fn a_closed_sink_still_retains_the_value_and_writes_nothing() {
let recorder = RunRecorder::new();
let closed = Rc::new(Delivered {
values: RefCell::new(Vec::new()),
open: false,
});
let mut results = Results::for_run(Some(recorder.clone()), closed.clone());
results.emit(serde_json::json!({"n": 1})).unwrap();
assert_eq!(recorder.records().len(), 1);
assert!(closed.values.borrow().is_empty());
}
#[test]
fn an_unserializable_event_is_an_emit_error() {
let mut results = Results::recording(RunRecorder::new());
let mut map = std::collections::HashMap::new();
map.insert((1u8, 2u8), 3u8);
let error = results.emit(map).unwrap_err();
assert!(matches!(error, EmitError::Serialize(_)), "{error}");
}
#[test]
fn an_unserializable_event_reaches_the_sink_as_a_recorded_failure() {
#[derive(Default)]
struct Remembers(RefCell<Vec<String>>);
impl EventSink for Remembers {
fn deliver(&self, _: &serde_json::Value) -> Result<(), EmitError> {
Ok(())
}
fn record_failure(&self, error: &EmitError) {
self.0.borrow_mut().push(error.to_string());
}
}
let sink = Rc::new(Remembers::default());
let mut results = Results::for_run(None, sink.clone());
let mut map = std::collections::HashMap::new();
map.insert((1u8, 2u8), 3u8);
results.emit(map).unwrap_err();
assert_eq!(sink.0.borrow().len(), 1, "{:?}", sink.0.borrow());
}
#[test]
fn a_destination_that_refuses_the_bytes_is_an_emit_error() {
struct Refuses;
impl EventSink for Refuses {
fn deliver(&self, _: &serde_json::Value) -> Result<(), EmitError> {
Err(EmitError::Write(std::io::Error::other("no room")))
}
}
let mut results = Results::for_run(None, Rc::new(Refuses));
let error = results.emit(serde_json::json!({"n": 1})).unwrap_err();
assert!(matches!(error, EmitError::Write(_)), "{error}");
}
#[test]
fn an_event_the_destination_refused_is_not_retained() {
struct Refuses;
impl EventSink for Refuses {
fn deliver(&self, _: &serde_json::Value) -> Result<(), EmitError> {
Err(EmitError::Write(std::io::Error::other("no room")))
}
}
let recorder = RunRecorder::new();
let mut results = Results::for_run(Some(recorder.clone()), Rc::new(Refuses));
results.emit(serde_json::json!({"n": 1})).unwrap_err();
assert!(recorder.records().is_empty());
}
}