use clap::ArgMatches;
use std::cell::RefCell;
use std::rc::Rc;
use crate::cli::handler::Output as HandlerOutput;
use crate::cli::handler::{CommandContext, ExternalFailure, RunError, RunErrorKind};
use crate::cli::hooks::{ArtifactOutput, HookError, Hooks};
use crate::context::{ContextRegistry, RenderContext};
use crate::StructuredOutputProjection;
use crate::Theme;
use serde::Serialize;
pub use standout_dispatch::{
extract_command_path, get_deepest_matches, has_subcommand, insert_default_command,
};
pub enum DispatchOutput {
Text {
formatted: String,
raw: String,
},
Binary(Vec<u8>, String),
Artifact {
output: ArtifactOutput,
presentation: Box<Presentation>,
},
Silent,
}
pub struct Presentation {
template: String,
theme: Theme,
context_registry: ContextRegistry,
template_engine: Rc<Box<dyn standout_render::template::TemplateEngine>>,
output_mode: crate::OutputMode,
structured_output_projection: Option<StructuredOutputProjection>,
ambiguous_width: crate::AmbiguousWidth,
}
impl Presentation {
pub(crate) fn render(
&self,
json_data: &serde_json::Value,
) -> Result<(String, String), RunError> {
let ambiguous_width =
standout_render::detect_ambiguous_width_override().unwrap_or(self.ambiguous_width);
let render_ctx = RenderContext::with_ambiguous_width(
self.output_mode,
standout_render::detect_terminal_width(),
ambiguous_width,
&self.theme,
json_data,
);
let render_result = match (self.output_mode, self.structured_output_projection.as_ref()) {
(crate::OutputMode::Csv, Some(projection)) => {
standout_render::template::RenderResult::plain(
projection
.csv_projection()
.render(json_data)
.map_err(|e| RunError::new(e.to_string(), RunErrorKind::Render))?,
)
}
_ => standout_render::template::render_auto_with_engine_split(
&**self.template_engine,
&self.template,
json_data,
&self.theme,
self.output_mode,
&self.context_registry,
&render_ctx,
)
.map_err(|e| RunError::new(e.to_string(), RunErrorKind::Render))?,
};
Ok((render_result.formatted, render_result.raw))
}
}
#[derive(Debug)]
struct HandlerErrorSource(Box<dyn std::error::Error + Send + Sync + 'static>);
impl std::fmt::Display for HandlerErrorSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl std::error::Error for HandlerErrorSource {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.0.as_ref())
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn render_handler_output<T: Serialize>(
result: crate::cli::HandlerResult<T>,
matches: &ArgMatches,
ctx: &CommandContext,
hooks: Option<&Hooks>,
template: &str,
theme: &Theme,
context_registry: &ContextRegistry,
template_engine: &Rc<Box<dyn standout_render::template::TemplateEngine>>,
output_mode: crate::OutputMode,
structured_output_projection: Option<&StructuredOutputProjection>,
ambiguous_width: crate::AmbiguousWidth,
) -> Result<DispatchOutput, RunError> {
let output = match result {
Ok(output) => output,
Err(error) => return Err(handler_run_error(error)),
};
let presentation = Presentation {
template: template.to_string(),
theme: theme.clone(),
context_registry: context_registry.clone(),
template_engine: template_engine.clone(),
output_mode,
structured_output_projection: structured_output_projection.cloned(),
ambiguous_width,
};
match output {
HandlerOutput::Render(data) => {
let json_data = serialize_handler_data(&data)?;
let json_data = run_post_dispatch_hooks(json_data, matches, ctx, hooks)?;
let (formatted, raw) = presentation.render(&json_data)?;
Ok(DispatchOutput::Text { formatted, raw })
}
HandlerOutput::Silent => Ok(DispatchOutput::Silent),
HandlerOutput::Binary { data, filename } => Ok(DispatchOutput::Binary(data, filename)),
HandlerOutput::Artifact(artifact) => {
let (bytes, suggested_destination, stdout_allowed, report) = artifact.into_parts();
let report = match report {
Some(report) => {
let json = serialize_handler_data(&report)?;
Some(run_post_dispatch_hooks(json, matches, ctx, hooks)?)
}
None => None,
};
Ok(DispatchOutput::Artifact {
presentation: Box::new(presentation),
output: ArtifactOutput {
bytes,
suggested_destination,
stdout_allowed,
report,
},
})
}
_ => Err(RunError::new(
"Unsupported handler output variant: this standout version cannot present it",
RunErrorKind::Render,
)),
}
}
fn serialize_handler_data<T: Serialize>(data: &T) -> Result<serde_json::Value, RunError> {
serde_json::to_value(data).map_err(|e| {
RunError::new(
format!("Failed to serialize handler result: {}", e),
RunErrorKind::Render,
)
})
}
fn run_post_dispatch_hooks(
json_data: serde_json::Value,
matches: &ArgMatches,
ctx: &CommandContext,
hooks: Option<&Hooks>,
) -> Result<serde_json::Value, RunError> {
match hooks {
Some(hooks) => hooks
.run_post_dispatch(matches, ctx, json_data)
.map_err(|error| hook_run_error(error, crate::cli::HookPhase::PostDispatch)),
None => Ok(json_data),
}
}
pub(crate) fn handler_run_error(error: anyhow::Error) -> RunError {
let error = match error.downcast::<ExternalFailure>() {
Ok(external) => return RunError::from(external),
Err(error) => error,
};
RunError::new(format!("Error: {}", error), RunErrorKind::Handler)
.with_source(HandlerErrorSource(error.into_boxed_dyn_error()))
}
pub(crate) fn hook_run_error(mut error: HookError, phase: crate::cli::HookPhase) -> RunError {
if phase == crate::cli::HookPhase::PreDispatch {
if let Some(source) = error.source.take() {
match source.downcast::<ExternalFailure>() {
Ok(external) => return RunError::from(*external),
Err(source) => error.source = Some(source),
}
}
}
error.phase = phase;
RunError::new(format!("Hook error: {}", error), RunErrorKind::Hook(phase)).with_source(error)
}
pub type DispatchFn = Rc<
RefCell<
dyn FnMut(
&ArgMatches,
&CommandContext,
Option<&Hooks>,
crate::OutputMode,
&crate::Theme,
crate::AmbiguousWidth,
) -> Result<DispatchOutput, RunError>,
>,
>;
pub fn dispatch(
dispatch_fn: &DispatchFn,
matches: &ArgMatches,
ctx: &CommandContext,
hooks: Option<&Hooks>,
output_mode: crate::OutputMode,
theme: &crate::Theme,
ambiguous_width: crate::AmbiguousWidth,
) -> Result<DispatchOutput, RunError> {
(dispatch_fn.borrow_mut())(matches, ctx, hooks, output_mode, theme, ambiguous_width)
}