use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
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(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
)));
};
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(error.to_string()))?;
match self.strict_style_tags_error() {
Some(error) => Err(EmitError::Render(error.to_string())),
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(error.to_string()))?;
Ok(self.sink.with_writer(|writer| {
writer.write_all(line.as_bytes())?;
writer.flush()
})?)
}
}
fn failure_kind(error: &EmitError) -> RunErrorKind {
match error {
EmitError::Serialize(_) | EmitError::Render(_) => RunErrorKind::Render,
EmitError::Write(_) => RunErrorKind::FinalWrite(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(RunError::new(error.to_string(), failure_kind(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(serialize), DiagnosticKind::Render),
(
EmitError::Render("no template".into()),
DiagnosticKind::Render,
),
(
EmitError::Write(std::io::Error::other("no room")),
DiagnosticKind::FinalWrite,
),
];
for (error, expected) in cases {
assert_eq!(
DiagnosticKind::from(failure_kind(&error)),
expected,
"{error}"
);
}
}
}