elv/infrastructure/
http_description.rs1use crate::{domain::description::Description, Configuration};
2
3use super::cli_display::CliDisplay;
4
5pub struct HttpDescription {
6    year: u16,
7    day: u8,
8    body: String,
9}
10
11impl HttpDescription {
12    pub fn part_one(&self) -> Option<String> {
13        let part_one_selector = scraper::Selector::parse(".day-desc").unwrap();
14        let binding = scraper::Html::parse_document(&self.body);
15        let select = binding.select(&part_one_selector);
16        select.map(|e| e.inner_html()).next()
17    }
18
19    pub fn part_one_answer(&self) -> Option<String> {
20        let part_one_answer_selector = scraper::Selector::parse("main > p").unwrap();
21        let binding = scraper::Html::parse_document(&self.body);
22        binding
23            .select(&part_one_answer_selector)
24            .map(|e| e.inner_html())
25            .find(|html| html.starts_with("Your puzzle answer was"))
26    }
27
28    pub fn part_two(&self) -> Option<String> {
29        let part_one_selector = scraper::Selector::parse(".day-desc").unwrap();
30        let binding = scraper::Html::parse_document(&self.body);
31        let select = binding.select(&part_one_selector);
32        select.map(|e| e.inner_html()).skip(1).next()
33    }
34
35    pub fn part_two_answer(&self) -> Option<String> {
36        let part_one_answer_selector = scraper::Selector::parse("main > p").unwrap();
37        let binding = scraper::Html::parse_document(&self.body);
38        binding
39            .select(&part_one_answer_selector)
40            .map(|e| e.inner_html())
41            .filter(|html| html.starts_with("Your puzzle answer was"))
42            .skip(1)
43            .next()
44    }
45}
46
47impl TryFrom<reqwest::blocking::Response> for HttpDescription {
48    type Error = anyhow::Error;
49
50    fn try_from(
51        http_response: reqwest::blocking::Response,
52    ) -> Result<HttpDescription, anyhow::Error> {
53        if http_response.status().is_success() == false {
54            anyhow::bail!("AoC server responded with an error".to_owned());
55        }
56
57        let mut year = String::new();
58        let mut day = String::new();
59        let year_and_day_regex =
60            regex::Regex::new(r".+\.com/([[:alnum:]]+)/day/([[:alnum:]]+)$").unwrap();
61        match year_and_day_regex.captures(http_response.url().as_str()) {
62            Some(captures) => {
63                captures.expand("1", &mut year);
64                captures.expand("2", &mut day);
65            }
66            None => {
67                anyhow::bail!("Cannot extract year and day from the url to construct a Description")
68            }
69        }
70
71        Ok(HttpDescription {
72            year: year.parse()?,
73            day: day.parse()?,
74            body: http_response.text()?,
75        })
76    }
77}
78
79impl Description for HttpDescription {
80    fn year(&self) -> u16 {
81        self.year
82    }
83
84    fn day(&self) -> u8 {
85        self.day
86    }
87}
88
89impl std::fmt::Display for HttpDescription {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        let description = [
92            self.part_one(),
93            self.part_one_answer(),
94            self.part_two(),
95            self.part_two_answer(),
96        ]
97        .iter()
98        .filter(|part| part.is_some())
99        .map(|part| part.as_deref().unwrap())
100        .collect::<Vec<_>>()
101        .join("\n");
102
103        f.write_str(&html2text::from_read_with_decorator(
104            description.as_bytes(),
105            200,
106            html2text::render::text_renderer::TrivialDecorator::new(),
107        ))
108    }
109}
110
111impl CliDisplay for HttpDescription {
112    fn cli_fmt(&self, configuration: &Configuration) -> String {
113        let description = [
114            self.part_one(),
115            self.part_one_answer(),
116            self.part_two(),
117            self.part_two_answer(),
118        ]
119        .iter()
120        .filter(|part| part.is_some())
121        .map(|part| part.as_deref().unwrap())
122        .collect::<Vec<_>>()
123        .join("\n");
124        html2text::from_read_with_decorator(
125            description.as_bytes(),
126            configuration.cli.output_width as usize,
127            html2text::render::text_renderer::TrivialDecorator::new(),
128        )
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use std::path::PathBuf;
135
136    use super::*;
137
138    fn get_resource_file(file: &str) -> String {
139        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
140        d.push(format!("tests/resources/{}", file));
141
142        std::fs::read_to_string(d.as_path()).unwrap()
143    }
144
145    #[test]
146    fn no_part_done() {
147        let description = HttpDescription {
148            year: 2022,
149            day: 1,
150            body: get_resource_file("riddle-description-no-part-done.html"),
151        };
152
153        assert!(description.part_one().is_some());
154        assert!(description.part_two().is_none());
155        assert!(description.part_one_answer().is_none());
156        assert!(description.part_two_answer().is_none());
157    }
158
159    #[test]
160    fn first_part_done() {
161        let description = HttpDescription {
162            year: 2022,
163            day: 1,
164            body: get_resource_file("riddle-description-first-part-done.html"),
165        };
166
167        assert!(description.part_one().is_some());
168        assert!(description.part_one_answer().is_some());
169        assert!(description.part_two().is_some());
170        assert!(description.part_two_answer().is_none());
171    }
172
173    #[test]
174    fn second_part_done() {
175        let description = HttpDescription {
176            year: 2022,
177            day: 1,
178            body: get_resource_file("riddle-description-both-parts-done.html"),
179        };
180
181        assert!(description.part_one().is_some());
182        assert!(description.part_one_answer().is_some());
183        assert!(description.part_two().is_some());
184        assert!(description.part_two_answer().is_some());
185    }
186}