use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use crate::cli::builder::{SharedTemplateEngine, TemplateRef};
use crate::cli::handler::{EmitError, EventSink, OutputKind, RunError, RunErrorKind, StreamSink};
use crate::context::ContextRegistry;
use crate::{ColorPolicy, RenderRequest, Representation, TargetProperties, Theme};
pub(crate) fn event_template(template: &TemplateRef) -> Option<standout_render::TemplateRef> {
match template {
TemplateRef::Named(name) => Some(named_event_template(name)),
TemplateRef::Inline(_) | TemplateRef::Absent(_) => None,
}
}
pub(crate) fn rendered_event_template(
template: &standout_render::TemplateRef,
) -> Option<standout_render::TemplateRef> {
match template {
standout_render::TemplateRef::Named(name) => Some(named_event_template(name)),
standout_render::TemplateRef::Inline(_) | standout_render::TemplateRef::Absent => None,
}
}
fn named_event_template(name: &str) -> standout_render::TemplateRef {
standout_render::TemplateRef::Named(format!("{name}.event"))
}
pub(crate) fn retains_events(representation: Representation) -> bool {
matches!(
representation,
Representation::Json | Representation::Yaml | Representation::Csv
)
}
pub(crate) struct EventContext {
pub command_path: String,
pub template: Option<standout_render::TemplateRef>,
pub theme: Theme,
pub context_registry: ContextRegistry,
pub template_engine: SharedTemplateEngine,
pub template_registry: Option<Rc<crate::TemplateRegistry>>,
pub representation: Representation,
pub color_policy: ColorPolicy,
pub target: TargetProperties,
pub warnings: Option<standout_render::warnings::WarningBuffer>,
pub strict_style_tags: bool,
}
pub(crate) struct EventDestination {
sink: StreamSink,
command_path: String,
representation: Representation,
strict_style_tags: bool,
warnings: Option<standout_render::warnings::WarningBuffer>,
request: Option<RefCell<RenderRequest>>,
failure: RefCell<Option<RunError>>,
retained: Option<RefCell<Vec<serde_json::Value>>>,
}
impl EventDestination {
pub(crate) fn new(sink: StreamSink, context: EventContext) -> Self {
let request = context
.template
.filter(|_| context.representation.is_human())
.map(|template| {
RefCell::new(RenderRequest {
data: serde_json::Value::Null,
template,
theme: context.theme,
format: context.representation,
color_policy: context.color_policy,
target: context.target,
engine: context.template_engine,
registry: context.template_registry,
context_registry: Some(context.context_registry),
csv_projection: None,
extras: HashMap::new(),
warnings: context.warnings.clone(),
})
});
Self {
sink,
command_path: context.command_path,
representation: context.representation,
strict_style_tags: context.strict_style_tags,
warnings: context.warnings,
request,
failure: RefCell::new(None),
retained: retains_events(context.representation).then(|| RefCell::new(Vec::new())),
}
}
pub(crate) fn take_failure(&self) -> Option<RunError> {
self.failure.borrow_mut().take()
}
pub(crate) fn take_document_records(&self) -> Option<Vec<serde_json::Value>> {
self.retained
.as_ref()
.map(|retained| std::mem::take(&mut *retained.borrow_mut()))
}
fn strict_style_tags_error(&self) -> Option<RunError> {
if !self.strict_style_tags {
return None;
}
crate::cli::builder::execution::unresolved_style_tags_error(self.warnings.as_ref())
}
fn render(&self, event: &serde_json::Value) -> Result<String, EmitError> {
let Some(request) = self.request.as_ref() else {
return Err(EmitError::Render {
message: format!(
"command `{}` emitted an event but declares no template to render one; \
an incremental command renders each event from `<name>.event` beside its \
own template",
self.command_path
),
cause: None,
});
};
let mut request = request.borrow_mut();
request.data = serde_json::json!({ "event": event });
let text = standout_render::render_request_split(&request)
.map(|rendered| rendered.formatted)
.map_err(|error| EmitError::Render {
message: error.to_string(),
cause: Some(Arc::new(error)),
})?;
match self.strict_style_tags_error() {
Some(error) => Err(EmitError::Render {
message: error.to_string(),
cause: None,
}),
None => Ok(text),
}
}
fn write(&self, event: &serde_json::Value) -> Result<(), EmitError> {
if let Some(retained) = self.retained.as_ref() {
retained.borrow_mut().push(event.clone());
return Ok(());
}
if self.representation.is_human() {
let text = self.render(event)?;
return Ok(self.sink.write_line(text.as_bytes())?);
}
let line =
standout_render::serialize_document(event, self.representation).map_err(|error| {
EmitError::Render {
message: error.to_string(),
cause: Some(Arc::new(error)),
}
})?;
Ok(self.sink.with_writer(|writer| {
writer.write_all(line.as_bytes())?;
writer.flush()
})?)
}
}
fn emit_failure_error(error: &EmitError) -> RunError {
match error {
EmitError::Serialize(cause) => RunError::render(error.to_string(), cause.clone()),
EmitError::Render { cause, .. } => match cause {
Some(cause) => RunError::render(error.to_string(), cause.clone()),
None => RunError::new(error.to_string(), RunErrorKind::Render),
},
EmitError::Write(io) => {
RunError::final_write(error.to_string(), io.clone(), OutputKind::Text)
}
}
}
impl EventSink for EventDestination {
fn deliver(&self, event: &serde_json::Value) -> Result<(), EmitError> {
let Err(error) = self.write(event) else {
return Ok(());
};
self.record_failure(&error);
Err(error)
}
fn is_open(&self) -> bool {
self.retained.is_some() || self.sink.is_open()
}
fn record_failure(&self, error: &EmitError) {
let mut failure = self.failure.borrow_mut();
if failure.is_none() {
*failure = Some(emit_failure_error(error));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::handler::DiagnosticKind;
#[test]
fn every_emit_failure_keeps_the_phase_it_failed_in() {
let serialize = serde_json::from_str::<serde_json::Value>("{").unwrap_err();
let cases = [
(
EmitError::Serialize(Arc::new(serialize)),
DiagnosticKind::Render,
),
(
EmitError::Render {
message: "no template".into(),
cause: None,
},
DiagnosticKind::Render,
),
(
EmitError::Write(Arc::new(std::io::Error::other("no room"))),
DiagnosticKind::FinalWrite,
),
];
for (error, expected) in cases {
assert_eq!(
DiagnosticKind::from(emit_failure_error(&error).kind()),
expected,
"{error}"
);
}
}
#[test]
fn every_emit_failure_hands_the_run_the_cause_it_carried() {
let serialize = serde_json::from_str::<serde_json::Value>("{").unwrap_err();
let reported = (serialize.line(), serialize.column(), serialize.classify());
let serialized = emit_failure_error(&EmitError::Serialize(Arc::new(serialize)));
let cause = std::error::Error::source(&serialized)
.and_then(|source| source.downcast_ref::<serde_json::Error>())
.expect("the serde error reaches the run");
assert_eq!((cause.line(), cause.column(), cause.classify()), reported);
let rendered = emit_failure_error(&EmitError::Render {
message: "template error: broken".into(),
cause: Some(Arc::new(standout_render::RenderError::TemplateError(
"broken".into(),
))),
});
assert!(matches!(
std::error::Error::source(&rendered)
.and_then(|source| source.downcast_ref::<standout_render::RenderError>()),
Some(standout_render::RenderError::TemplateError(_))
));
let written = emit_failure_error(&EmitError::Write(Arc::new(std::io::Error::from(
std::io::ErrorKind::BrokenPipe,
))));
assert_eq!(
std::error::Error::source(&written)
.and_then(|source| source.downcast_ref::<std::io::Error>())
.map(std::io::Error::kind),
Some(std::io::ErrorKind::BrokenPipe)
);
let causeless = emit_failure_error(&EmitError::Render {
message: "no template".into(),
cause: None,
});
assert!(std::error::Error::source(&causeless).is_none());
assert_eq!(causeless.as_str(), "no template");
}
}