1use crate::analysis::AnalysisReport;
2
3use snafu::{ResultExt, Snafu};
4use std::{fmt::Debug, io::Write, path::PathBuf};
5
6#[derive(Debug, Snafu)]
8#[allow(missing_docs)]
9pub enum Error {
10 #[snafu(display("failed to render report to Json: {}", source))]
12 RenderJsonFailed { source: serde_json::Error },
13 #[snafu(display("failed to read handlebars template from file '{}': {}", path.display(), source))]
15 ReadHbsTemplateFileFailed { path: PathBuf, source: std::io::Error },
16 #[snafu(display("Handlebars template is invalid: {}", source))]
18 InvalidHbsTemplate { source: ::handlebars::TemplateError },
19 #[snafu(display("failed to render handlebars template: {}", source))]
21 RenderHbsTemplateFailed { source: ::handlebars::RenderError },
22}
23
24pub type Result<T, E = Error> = std::result::Result<T, E>;
25
26pub trait Renderer<W: Write>: Debug {
27 fn render(&self, report: &AnalysisReport, w: W) -> Result<()>;
28}
29
30pub use crate::renderer::handlebars::HbsRenderer;
31pub use json::JsonRenderer;
32
33pub mod json {
34 use super::*;
35
36 #[derive(Default, Debug, Eq, PartialEq, Clone)]
37 pub struct JsonRenderer {}
38
39 impl JsonRenderer {
40 pub fn new() -> Self { JsonRenderer {} }
41 }
42
43 impl<W: Write> Renderer<W> for JsonRenderer {
44 fn render(&self, report: &AnalysisReport, w: W) -> Result<()> {
45 serde_json::to_writer(w, report).context(RenderJsonFailed {})
46 }
47 }
48}
49
50pub mod handlebars {
51 use super::*;
52
53 use ::handlebars::Handlebars;
54 use std::{fs::File, io::Read, path::Path};
55
56 #[derive(Default, Debug, Eq, PartialEq, Clone)]
57 pub struct HbsRenderer {
58 template: String,
59 }
60
61 impl HbsRenderer {
62 pub fn new<T: Into<String>>(template: T) -> Self {
63 HbsRenderer {
64 template: template.into(),
65 }
66 }
67
68 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
69 let mut file = File::open(path.as_ref()).context(ReadHbsTemplateFileFailed {
70 path: path.as_ref().to_path_buf(),
71 })?;
72 let mut template = String::new();
73 file.read_to_string(&mut template).context(ReadHbsTemplateFileFailed {
74 path: path.as_ref().to_path_buf(),
75 })?;
76
77 Ok(HbsRenderer { template })
78 }
79 }
80
81 impl<W: Write> Renderer<W> for HbsRenderer {
82 fn render(&self, report: &AnalysisReport, w: W) -> Result<()> {
83 let mut handlebars = Handlebars::new();
84 handlebars.register_helper("inc", Box::new(helpers::inc));
85 handlebars.register_helper("rfc2822", Box::new(helpers::date_time_2822));
86 handlebars.register_helper("rfc3339", Box::new(helpers::date_time_3339));
87 handlebars.register_helper("escape", Box::new(helpers::escape));
88 handlebars
89 .register_template_string("template", &self.template)
90 .context(InvalidHbsTemplate {})?;
91 handlebars
92 .render_to_write("template", report, w)
93 .context(RenderHbsTemplateFailed {})
94 }
95 }
96
97 mod helpers {
98 use chrono::{DateTime, Local};
99 use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError};
100
101 pub(crate) fn inc(
102 h: &Helper,
103 _: &Handlebars,
104 _: &Context,
105 _: &mut RenderContext,
106 out: &mut dyn Output,
107 ) -> HelperResult {
108 let value = h
109 .param(0)
110 .ok_or_else(|| RenderError::new("no such parameter"))?
111 .value()
112 .as_i64()
113 .ok_or_else(|| RenderError::new("parameter is not a number"))?;
114 let inc = format!("{}", value + 1);
115 out.write(&inc).map_err(RenderError::with)
116 }
117
118 pub(crate) fn date_time_2822(
119 h: &Helper,
120 _: &Handlebars,
121 _: &Context,
122 _: &mut RenderContext,
123 out: &mut dyn Output,
124 ) -> HelperResult {
125 let dt = date_param(h)?;
126 out.write(&dt.to_rfc2822()).map_err(RenderError::with)
127 }
128
129 pub(crate) fn date_time_3339(
130 h: &Helper,
131 _: &Handlebars,
132 _: &Context,
133 _: &mut RenderContext,
134 out: &mut dyn Output,
135 ) -> HelperResult {
136 let dt = date_param(h)?;
137 out.write(&dt.to_rfc3339()).map_err(RenderError::with)
138 }
139
140 fn date_param(h: &Helper) -> ::std::result::Result<DateTime<Local>, RenderError> {
141 let dt_str = h
142 .param(0)
143 .ok_or_else(|| RenderError::new("no such parameter"))?
144 .value()
145 .as_str()
146 .ok_or_else(|| RenderError::new("parameter is not a string"))?;
147 dt_str.parse::<DateTime<Local>>().map_err(RenderError::with)
148 }
149
150 pub(crate) fn escape(
151 h: &Helper,
152 _: &Handlebars,
153 _: &Context,
154 _: &mut RenderContext,
155 out: &mut dyn Output,
156 ) -> HelperResult {
157 let from = h
158 .param(0)
159 .ok_or_else(|| RenderError::new("no 'from' parameter"))?
160 .value()
161 .as_str()
162 .ok_or_else(|| RenderError::new("'from' parameter is not a string"))?;
163
164 let to = h
165 .param(1)
166 .ok_or_else(|| RenderError::new("no 'to' parameter"))?
167 .value()
168 .as_str()
169 .ok_or_else(|| RenderError::new("'to' parameter is not a string"))?;
170
171 let data = h
172 .param(2)
173 .ok_or_else(|| RenderError::new("no 'data' parameter"))?
174 .value()
175 .as_str()
176 .ok_or_else(|| RenderError::new("'data' parameter is not a string"))?;
177
178 let escaped = data.replace(from, to);
179 out.write(&escaped).map_err(RenderError::with)
180 }
181 }
182}