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.3";
14const BOOTSTRAP_CSS_SRI: &str =
15    "sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH";
16const BOOTSTRAP_JS_SRI: &str =
17    "sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz";
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!("https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/css/bootstrap.min.css"),
50            Self::Custom {location, js_location, ..} => match js_location {
51                Some(_) => location.clone(),
52                None => format!("{location}/css/bootstrap.min.css" ),
53            },
54        }
55    }
56
57    pub fn css_integrity(&self) -> Option<String> {
58        match self {
59            Self::Default => Some(BOOTSTRAP_CSS_SRI.into()),
60            Self::Custom { css_integrity, .. } => css_integrity.clone(),
61        }
62    }
63
64    pub fn js_location(&self) -> String {
65        match self {
66            Self::Default => format!("https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/js/bootstrap.bundle.min.js"),
67            Self::Custom {location, js_location, ..} => {
68                match js_location {
69                    Some(js_location) => js_location.clone(),
70                    None => format!("{location}/js/bootstrap.bundle.min.js")
71                }
72            }
73        }
74    }
75
76    pub fn js_integrity(&self) -> Option<String> {
77        match self {
78            Self::Default => Some(BOOTSTRAP_JS_SRI.into()),
79            Self::Custom { js_integrity, .. } => js_integrity.clone(),
80        }
81    }
82}
83
84pub fn render(
85    mut write: impl Write,
86    title: impl Display,
87    report: impl Display,
88    options: &ReportOptions,
89) -> anyhow::Result<()> {
90    writeln!(
91        write,
92        r#"<!doctype html>
93<html lang="en">
94  <head>
95    <meta charset="utf-8">
96    <meta name="viewport" content="width=device-width, initial-scale=1">
97    <title>{title}</title>
98    <link href="{css}" rel="stylesheet" {css_integrity} crossorigin="anonymous">
99  </head>
100  <body>
101    <div class="container-fluid">
102      <h1>
103        {title} <span class="badge bg-secondary">{date}</span>
104      </h1>
105      {report}
106    </div>
107    <script src="{js}" {js_integrity} crossorigin="anonymous"></script>
108  </body>
109</html>
110"#,
111        date = time::OffsetDateTime::now_local()
112            .unwrap_or_else(|_| time::OffsetDateTime::now_utc())
113            .format(&format_description!(
114                "[year]-[month padding:zero]-[day padding:zero] [hour repr:24]:[minute padding:zero]:[second padding:zero] [offset_hour sign:mandatory]:[offset_minute]"
115            ))
116            .unwrap_or_else(|_| "Unknown".to_string()),
117        css = html_escape::encode_quoted_attribute(&options.bootstrap.css_location()),
118        js = html_escape::encode_quoted_attribute(&options.bootstrap.js_location()),
119        css_integrity = options
120            .bootstrap
121            .css_integrity()
122            .map(|sri| format!(
123                r#"integrity="{sri}""#,
124                sri = html_escape::encode_quoted_attribute(&sri)
125            ))
126            .unwrap_or_default(),
127        js_integrity = options
128            .bootstrap
129            .js_integrity()
130            .map(|sri| format!(
131                r#"integrity="{sri}""#,
132                sri = html_escape::encode_quoted_attribute(&sri)
133            ))
134            .unwrap_or_default(),
135        title = html_escape::encode_text(&title.to_string()),
136    )?;
137
138    Ok(())
139}