walker_common/report/
mod.rs

1//! Common functionality for creating the reports
2
3mod stats;
4mod summary;
5
6pub use stats::*;
7pub use summary::*;
8
9use std::fmt::Display;
10use std::io::Write;
11use time::macros::format_description;
12
13const BOOTSTRAP_VERSION: &str = "5.3.7";
14const BOOTSTRAP_CSS_SRI: &str =
15    "sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr";
16const BOOTSTRAP_JS_SRI: &str =
17    "sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q";
18
19/// Options for rendering reports.
20#[derive(Clone, Debug, Default)]
21pub struct ReportOptions {
22    pub bootstrap: Bootstrap,
23}
24
25/// Options for the imported bootstrap resources.
26#[derive(Clone, Default, Debug)]
27pub enum Bootstrap {
28    /// Use a default version served from a CDN
29    #[default]
30    Default,
31    /// Use a custom version
32    Custom {
33        /// The location to Bootstrap.
34        ///
35        /// The is considered the base URL, unless `js_location` is present as well. In which case it's considered a full URL.
36        location: String,
37        /// The specific location of the bootstrap JS file.
38        js_location: Option<String>,
39        /// An optional SRI value for the CSS resource
40        css_integrity: Option<String>,
41        /// An optional SRI value for the JS resource
42        js_integrity: Option<String>,
43    },
44}
45
46impl Bootstrap {
47    pub fn css_location(&self) -> String {
48        match self {
49            Self::Default => format!(
50                "https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/css/bootstrap.min.css"
51            ),
52            Self::Custom {
53                location,
54                js_location,
55                ..
56            } => match js_location {
57                Some(_) => location.clone(),
58                None => format!("{location}/css/bootstrap.min.css"),
59            },
60        }
61    }
62
63    pub fn css_integrity(&self) -> Option<String> {
64        match self {
65            Self::Default => Some(BOOTSTRAP_CSS_SRI.into()),
66            Self::Custom { css_integrity, .. } => css_integrity.clone(),
67        }
68    }
69
70    pub fn js_location(&self) -> String {
71        match self {
72            Self::Default => format!(
73                "https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/js/bootstrap.bundle.min.js"
74            ),
75            Self::Custom {
76                location,
77                js_location,
78                ..
79            } => match js_location {
80                Some(js_location) => js_location.clone(),
81                None => format!("{location}/js/bootstrap.bundle.min.js"),
82            },
83        }
84    }
85
86    pub fn js_integrity(&self) -> Option<String> {
87        match self {
88            Self::Default => Some(BOOTSTRAP_JS_SRI.into()),
89            Self::Custom { js_integrity, .. } => js_integrity.clone(),
90        }
91    }
92}
93
94pub fn render(
95    mut write: impl Write,
96    title: impl Display,
97    report: impl Display,
98    options: &ReportOptions,
99) -> anyhow::Result<()> {
100    writeln!(
101        write,
102        r#"<!doctype html>
103<html lang="en">
104  <head>
105    <meta charset="utf-8">
106    <meta name="viewport" content="width=device-width, initial-scale=1">
107    <title>{title}</title>
108    <link href="{css}" rel="stylesheet" {css_integrity} crossorigin="anonymous">
109  </head>
110  <body>
111    <div class="container-fluid">
112      <h1>
113        {title} <span class="badge bg-secondary">{date}</span>
114      </h1>
115      {report}
116    </div>
117    <script src="{js}" {js_integrity} crossorigin="anonymous"></script>
118  </body>
119</html>
120"#,
121        date = time::OffsetDateTime::now_local()
122            .unwrap_or_else(|_| time::OffsetDateTime::now_utc())
123            .format(&format_description!(
124                "[year]-[month padding:zero]-[day padding:zero] [hour repr:24]:[minute padding:zero]:[second padding:zero] [offset_hour sign:mandatory]:[offset_minute]"
125            ))
126            .unwrap_or_else(|_| "Unknown".to_string()),
127        css = html_escape::encode_quoted_attribute(&options.bootstrap.css_location()),
128        js = html_escape::encode_quoted_attribute(&options.bootstrap.js_location()),
129        css_integrity = options
130            .bootstrap
131            .css_integrity()
132            .map(|sri| format!(
133                r#"integrity="{sri}""#,
134                sri = html_escape::encode_quoted_attribute(&sri)
135            ))
136            .unwrap_or_default(),
137        js_integrity = options
138            .bootstrap
139            .js_integrity()
140            .map(|sri| format!(
141                r#"integrity="{sri}""#,
142                sri = html_escape::encode_quoted_attribute(&sri)
143            ))
144            .unwrap_or_default(),
145        title = html_escape::encode_text(&title.to_string()),
146    )?;
147
148    Ok(())
149}