Skip to content

Commit

Permalink
Add cli for user defined rendering templates
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaspustina committed Jan 11, 2020
1 parent b97dfdf commit 3aff77b
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 46 deletions.
4 changes: 2 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
* [X] Commands should allow for arbitrary links for further information or actions
* [X] Command result should store command execution time
* [ ] Rendering should allow for HTML
* [ ] Allow for generic handlebar based rendering via CLI
* [ ] Include failures in report
* [X] Allow for generic handlebar based rendering via CLI
* [ ] Create linux and macos configuration
* macOS
* sw_vers
Expand All @@ -28,6 +29,5 @@
* Linux
* https://www.cyberciti.biz/tips/top-linux-monitoring-tools.html
* http://www.brendangregg.com/USEmethod/use-linux.html
* [ ] Include failures in report
* [ ] Activate deny missing docs and add docs

93 changes: 58 additions & 35 deletions src/bin/usereport.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use exitfailure::ExitFailure;
use failure::{ResultExt, format_err};
use failure::{format_err, ResultExt};
use indicatif::{ProgressBar, ProgressStyle};
use prettytable::{cell, format, row, Cell, Row, Table};
use std::{
Expand All @@ -9,45 +9,59 @@ use std::{
sync::mpsc::{self, Receiver, Sender},
};
use structopt::{clap, StructOpt};
use usereport::{renderer, runner::ThreadRunner, Analysis, AnalysisReport, Config, Renderer};
use usereport::{renderer, runner::ThreadRunner, Analysis, Config, Renderer};

#[derive(Debug, StructOpt)]
#[structopt(name = "usereport", author, about, setting = clap::AppSettings::ColoredHelp)]
struct Opt {
/// Configuration from file, or default if not present
#[structopt(short, long, parse(from_os_str))]
config: Option<PathBuf>,
config: Option<PathBuf>,
/// Output format
#[structopt(short, long, possible_values = & ["json", "markdown"], default_value = "markdown")]
output_type: OutputType,
#[structopt(short, long, possible_values = & ["hbs", "json", "markdown"], default_value = "markdown")]
output_type: OutputType,
/// Set output template if output-type is set to "hbs"
#[structopt(long)]
output_template: Option<String>,
/// Set number of commands to run in parallel; overrides setting from config file
#[structopt(long)]
parallel: Option<usize>,
parallel: Option<usize>,
/// Set number of how many times to run commands in row; overrides setting from config file
#[structopt(long)]
repetitions: Option<usize>,
repetitions: Option<usize>,
/// Show progress bar while waiting for all commands to finish
#[structopt(short = "P", long)]
progress: bool,
progress: bool,
/// Activate debug mode
#[structopt(short, long)]
debug: bool,
debug: bool,
/// Set profile to use
#[structopt(short = "p", long)]
profile: Option<String>,
profile: Option<String>,
/// Show active config
#[structopt(long)]
show_config: bool,
show_config: bool,
/// Show available profiles
#[structopt(long)]
show_profiles: bool,
show_profiles: bool,
/// Show available commands
#[structopt(long)]
show_commands: bool,
show_commands: bool,
}

impl Opt {
pub fn validate(self) -> Result<Self, failure::Error> {
if self.output_type == OutputType::Hbs && self.output_template.is_none() {
return Err(format_err!("Output type hbs requires output template"));
}

Ok(self)
}
}

#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub enum OutputType {
Hbs,
JSON,
Markdown,
}
Expand All @@ -57,9 +71,9 @@ impl FromStr for OutputType {

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_ref() {
"hbs" => Ok(OutputType::Hbs),
"json" => Ok(OutputType::JSON),
"markdown" => Ok(OutputType::Markdown),
"md" => Ok(OutputType::Markdown),
_ => Err(format_err!("failed to parse {} as output type", s)),
}
}
Expand All @@ -69,7 +83,7 @@ fn main() -> Result<(), ExitFailure> {
human_panic::setup_panic!();
env_logger::init();

let opt = Opt::from_args();
let opt = Opt::from_args().validate()?;
let config = opt
.config
.as_ref()
Expand Down Expand Up @@ -137,26 +151,49 @@ fn show_commands(config: &Config) {
}

fn generate_report(opt: &Opt, config: &Config, profile_name: &str) -> Result<(), ExitFailure> {
let parallel = opt.parallel.unwrap_or(config.defaults.max_parallel_commands);
let repetitions = opt.repetitions.unwrap_or(config.defaults.repetitions);
// Create renderer early to detect misconfiguration early
let stdout = std::io::stdout();
let handle = stdout.lock();
let renderer = create_renderer(&opt.output_type, opt.output_template.as_ref())?;

let hostinfo = config.commands_for_hostinfo();
let commands = config
.profile(profile_name)
.and_then(|p| Ok(config.commands_for_profile(p)))?;

let parallel = opt.parallel.unwrap_or(config.defaults.max_parallel_commands);
let repetitions = opt.repetitions.unwrap_or(config.defaults.repetitions);

let number_of_commands = hostinfo.len() + repetitions * commands.len();

let runner = create_runner(opt.progress, number_of_commands);
let analysis = Analysis::new(Box::new(runner), &hostinfo, &commands)
.with_max_parallel_commands(parallel)
.with_repetitions(repetitions);
let report = analysis.run().with_context(|_| "failed to run analysis")?;

render_to_stdout(&report, &opt.output_type).with_context(|_| "failed to render report")?;
renderer
.render(&report, handle)
.with_context(|_| "failed to render report")?;

Ok(())
}

fn create_renderer<W: Write>(
output_type: &OutputType,
output_template: Option<&String>,
) -> Result<Box<dyn Renderer<W>>, ExitFailure> {
let renderer: Box<dyn Renderer<W>> = match output_type {
OutputType::Hbs => {
let template_file = output_template.expect("output type hbs requires output template");
let renderer = renderer::HbsRenderer::from_file(template_file)?;
Box::new(renderer)
}
OutputType::Markdown => Box::new(renderer::HbsRenderer::new(defaults::MD_TEMPLATE)),
OutputType::JSON => Box::new(renderer::JsonRenderer::new()),
};

Ok(renderer)
}

fn create_runner(progress: bool, number_of_commands: usize) -> ThreadRunner {
let mut runner = ThreadRunner::new();
if progress {
Expand All @@ -183,20 +220,6 @@ fn create_progress_bar(expected: usize) -> Sender<usize> {
tx
}

fn render_to_stdout(report: &AnalysisReport, output_type: &OutputType) -> renderer::Result<()> {
let stdout = std::io::stdout();
let handle = stdout.lock();
let r = renderer(output_type);
r.render(report, handle)
}

fn renderer<W: Write>(output_type: &OutputType) -> Box<dyn Renderer<W>> {
match output_type {
OutputType::Markdown => Box::new(renderer::HbsRenderer::new(defaults::MD_TEMPLATE)),
OutputType::JSON => Box::new(renderer::JsonRenderer::new()),
}
}

#[cfg(target_os = "macos")]
mod defaults {
pub(crate) static CONFIG: &str = include_str!("../../contrib/osx.conf");
Expand Down
37 changes: 28 additions & 9 deletions src/renderer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::analysis::AnalysisReport;

use snafu::{ResultExt, Snafu};
use std::io::Write;
use std::{io::Write, path::PathBuf};

/// Error type
#[derive(Debug, Snafu)]
Expand All @@ -13,6 +13,9 @@ pub enum Error {
/// Rendering of report to Json failed
#[snafu(display("failed to render report to Json: {}", source))]
JsonRenderingFailed { source: serde_json::Error },
/// Failed to read handlebars template from file
#[snafu(display("failed to read handlebars template from file '{}': {}", path.display(), source))]
HbsTemplateFileFailed { path: PathBuf, source: std::io::Error },
/// Handlebars template for Markdown is invalid
#[snafu(display("Handlebars template is invalid: {}", source))]
HbsTemplateFailed { source: ::handlebars::TemplateError },
Expand All @@ -23,13 +26,12 @@ pub enum Error {

pub type Result<T, E = Error> = std::result::Result<T, E>;


pub trait Renderer<W: Write> {
fn render(&self, report: &AnalysisReport, w: W) -> Result<()>;
}

pub use json::JsonRenderer;
pub use crate::renderer::handlebars::HbsRenderer;
pub use json::JsonRenderer;

pub mod json {
use super::*;
Expand All @@ -52,23 +54,40 @@ pub mod handlebars {
use super::*;

use ::handlebars::Handlebars;
use std::{fs::File, io::Read, path::Path};

pub struct HbsRenderer<'a> {
template: &'a str,
pub struct HbsRenderer {
template: String,
}

impl<'a> HbsRenderer<'a> {
pub fn new(template: &'a str) -> Self { HbsRenderer { template } }
impl HbsRenderer {
pub fn new<T: Into<String>>(template: T) -> Self {
HbsRenderer {
template: template.into(),
}
}

pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut file = File::open(path.as_ref()).context(HbsTemplateFileFailed {
path: path.as_ref().to_path_buf(),
})?;
let mut template = String::new();
file.read_to_string(&mut template).context(HbsTemplateFileFailed {
path: path.as_ref().to_path_buf(),
})?;

Ok(HbsRenderer { template })
}
}

impl<'a, W: Write> Renderer<W> for HbsRenderer<'a> {
impl<W: Write> Renderer<W> for HbsRenderer {
fn render(&self, report: &AnalysisReport, w: W) -> Result<()> {
let mut handlebars = Handlebars::new();
handlebars.register_helper("inc", Box::new(helpers::inc));
handlebars.register_helper("rfc2822", Box::new(helpers::date_time_2822));
handlebars.register_helper("rfc3339", Box::new(helpers::date_time_3339));
handlebars
.register_template_string("markdown", self.template)
.register_template_string("markdown", &self.template)
.context(HbsTemplateFailed {})?;
handlebars
.render_to_write("markdown", report, w)
Expand Down

0 comments on commit 3aff77b

Please sign in to comment.