github_heatmap/heatmap/
contribution.rs1use colored::{Color, Colorize};
2use scraper::ElementRef;
3use crate::{ColorValues, HeatmapError};
4
5const LEVEL_ATTR: &str = "data-level";
6
7#[derive(Debug, Clone, Eq, PartialEq)]
15pub struct Contribution {
16 pub heat_level: usize
23}
24
25impl Contribution {
26 pub fn from_el(el: &ElementRef) -> Result<Self, HeatmapError> {
35 let heat_level = Self::parse_heat_level(el)?;
36 Ok(Contribution { heat_level })
37 }
38
39 pub fn render(&self, color: &ColorValues) -> String {
46 let intensity = match self.heat_level {
47 0 => 0,
48 1 => 64,
49 2 => 127,
50 3 => 191,
51 _ => 255,
52 };
53
54 let fill = match color {
55 ColorValues::Red => Color::TrueColor { r: intensity, g: 0, b: 0 },
56 ColorValues::Green => Color::TrueColor { r: 0, g: intensity, b: 0 },
57 ColorValues::Blue => Color::TrueColor { r: 0, g: 0, b: intensity },
58 };
59
60 "\u{025A0} ".color(fill).to_string()
61 }
62
63 fn parse_heat_level(el: &ElementRef) -> Result<usize, HeatmapError> {
64 let heat_level = el
65 .value()
66 .attr(LEVEL_ATTR)
67 .ok_or_else(|| HeatmapError::QueryAttribute {
68 attr: LEVEL_ATTR.to_string(),
69 on_alias: "heatmap node".to_string()
70 })?
71 .parse()
72 .map_err(|_| HeatmapError::ParseAttribute {
73 attr: LEVEL_ATTR.to_string(),
74 on_alias: "heatmap node".to_string()
75 })?;
76
77 Ok(heat_level)
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use scraper::{Html, Selector};
85
86 #[test]
87 fn constructs_contribution() {
88 let fragment = Html::parse_fragment("<rect y='15' data-level='3' />");
89 let selector = Selector::parse("rect").unwrap();
90 let rect_el = fragment.select(&selector).next().unwrap();
91 let contribution = Contribution::from_el(&rect_el).unwrap();
92
93 assert_eq!(contribution, Contribution { heat_level: 3 })
94 }
95
96 #[test]
97 fn parses_level_attribute() {
98 let fragment = Html::parse_fragment("<rect y='15' data-level='3' />");
99 let selector = Selector::parse("rect").unwrap();
100 let rect_el = fragment.select(&selector).next().unwrap();
101 let heat_level = Contribution::parse_heat_level(&rect_el).unwrap();
102
103 assert_eq!(heat_level, 3)
104 }
105
106 #[test]
107 fn error_if_no_level_attribute() {
108 let fragment = Html::parse_fragment("<rect y='15' data-heat-level='3' />");
109 let selector = Selector::parse("rect").unwrap();
110 let rect_el = fragment.select(&selector).next().unwrap();
111 let heat_level = Contribution::parse_heat_level(&rect_el);
112
113 assert_eq!(
114 heat_level,
115 Err(HeatmapError::QueryAttribute { attr: LEVEL_ATTR.to_string(), on_alias: "heatmap node".to_string() })
116 )
117 }
118
119 #[test]
120 fn error_if_invalid_level_attribute() {
121 let fragment = Html::parse_fragment("<rect y='15' data-level='three' />");
122 let selector = Selector::parse("rect").unwrap();
123 let rect_el = fragment.select(&selector).next().unwrap();
124 let heat_level = Contribution::parse_heat_level(&rect_el);
125
126 assert_eq!(
127 heat_level,
128 Err(HeatmapError::ParseAttribute { attr: LEVEL_ATTR.to_string(), on_alias: "heatmap node".to_string() })
129 )
130 }
131
132 #[test]
133 fn renders_heatmap_node_unfilled() {
134 let contribution = Contribution { heat_level: 0 };
135 let color = ColorValues::Green;
136 let expected = "\u{025A0} ".color(Color::TrueColor { r: 0, g: 0, b: 0 }).to_string();
137
138 assert_eq!(contribution.render(&color), expected);
139 }
140
141 #[test]
142 fn renders_heatmap_node_red() {
143 let contribution = Contribution { heat_level: 1 };
144 let color = ColorValues::Red;
145 let expected = "\u{025A0} ".color(Color::TrueColor { r: 64, g: 0, b: 0 }).to_string();
146
147 assert_eq!(contribution.render(&color), expected);
148 }
149
150
151 #[test]
152 fn renders_heatmap_node_green() {
153 let contribution = Contribution { heat_level: 2 };
154 let color = ColorValues::Green;
155 let expected = "\u{025A0} ".color(Color::TrueColor { r: 0, g: 127, b: 0 }).to_string();
156
157 assert_eq!(contribution.render(&color), expected);
158 }
159
160 #[test]
161 fn renders_heatmap_node_blue() {
162 let contribution = Contribution { heat_level: 3 };
163 let color = ColorValues::Blue;
164 let expected = "\u{025A0} ".color(Color::TrueColor { r: 0, g: 0, b: 191 }).to_string();
165
166 assert_eq!(contribution.render(&color), expected);
167 }
168}