From 076867c6a23e7f30898bd7ee423038cc495a3d2a Mon Sep 17 00:00:00 2001 From: Konrad Pagacz Date: Sat, 2 Dec 2023 11:31:04 +0100 Subject: [PATCH] fix: fixed showing stars page for 2023 * Fixed a bug where the stars page for 2023 wouldn't show. * Refactored the logic responsible for parsing the stars page to be hopefully more resilient. Closes #58 --- src/application/cli.rs | 2 +- src/domain/ports/errors.rs | 3 - src/domain/ports/get_stars.rs | 4 +- src/infrastructure/aoc_api/get_stars_impl.rs | 67 +++++++++++++------- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/application/cli.rs b/src/application/cli.rs index 99d7960..66f453b 100644 --- a/src/application/cli.rs +++ b/src/application/cli.rs @@ -203,7 +203,7 @@ impl ElvCli { let driver = get_driver(None, None); match driver.get_stars(year.unwrap_or_else(determine_year)) { Ok(stars) => println!("{}", stars), - Err(e) => eprintln!("❌ Failure: {}", e.to_string()), + Err(e) => eprintln!("❌ Failure: {:#}", e), } } diff --git a/src/domain/ports/errors.rs b/src/domain/ports/errors.rs index 6feadd7..b0c378a 100644 --- a/src/domain/ports/errors.rs +++ b/src/domain/ports/errors.rs @@ -24,9 +24,6 @@ pub enum AocClientError { #[error("Network error")] NetworkError(#[from] reqwest::Error), - - #[error("Failed to get stars page")] - GetStarsError, } impl From for AocClientError { diff --git a/src/domain/ports/get_stars.rs b/src/domain/ports/get_stars.rs index 5f6f03c..a015f9f 100644 --- a/src/domain/ports/get_stars.rs +++ b/src/domain/ports/get_stars.rs @@ -1,5 +1,5 @@ -use super::{super::stars::Stars, errors::AocClientError}; +use super::super::stars::Stars; pub trait GetStars { - fn get_stars(&self, year: i32) -> Result; + fn get_stars(&self, year: i32) -> anyhow::Result; } diff --git a/src/infrastructure/aoc_api/get_stars_impl.rs b/src/infrastructure/aoc_api/get_stars_impl.rs index b25dbe5..2915f33 100644 --- a/src/infrastructure/aoc_api/get_stars_impl.rs +++ b/src/infrastructure/aoc_api/get_stars_impl.rs @@ -1,16 +1,21 @@ +use anyhow::Context; + use super::AOC_URL; -use crate::domain::{ - ports::{errors::AocClientError, get_stars::GetStars}, - solved_parts::SolvedParts, - stars::Stars, -}; +use crate::domain::{ports::get_stars::GetStars, solved_parts::SolvedParts, stars::Stars}; use super::AocApi; impl GetStars for AocApi { - fn get_stars(&self, year: i32) -> Result { - let url = reqwest::Url::parse(&format!("{}/{}", AOC_URL, year))?; - Stars::from_readable(self.http_client.get(url).send()?.error_for_status()?) + fn get_stars(&self, year: i32) -> anyhow::Result { + let url = reqwest::Url::parse(&format!("{}/{}", AOC_URL, year)) + .expect("Creating a static URL should not fail"); + Stars::from_readable( + self.http_client + .get(url) + .send()? + .error_for_status() + .context("GET request for the stars page failed")?, + ) } } @@ -20,21 +25,25 @@ impl GetStars for AocApi { // of this trait in the standard library which conflicts // with the one above. So this is one workaround... impl Stars { - pub fn from_readable(mut readable: T) -> Result { + pub fn from_readable(mut readable: T) -> anyhow::Result { let mut body = String::new(); readable .read_to_string(&mut body) - .map_err(|_| AocClientError::GetStarsError)?; + .context("Reading the stars page response body to string failed")?; - parse_http_response(body) + parse_http_response(body).context("Parsing the stars page body to stars failed") } } -fn parse_http_response(calendar_http_body: String) -> Result { +fn parse_http_response(calendar_http_body: String) -> anyhow::Result { let document = scraper::Html::parse_document(&calendar_http_body); - let calendar_entries_selector = - scraper::Selector::parse("[class^='calendar-day']:not(.calendar-day)").unwrap(); - let solved_parts = document + let calendar_entries_selector = scraper::Selector::parse( + "pre[class=\"calendar\"] > span, \ + pre[class=\"calendar\"] > a, pre[class=\"calendar calendar-perfect\"] > span, \ + pre[class=\"calendar calendar-perfect\"] > a", + ) + .expect("Parsing a static CSS selector should not fail"); + let solved_statuses = document .select(&calendar_entries_selector) .map(|day| { match ( @@ -55,25 +64,37 @@ fn parse_http_response(calendar_http_body: String) -> Result>(); - let entries_without_stars = std::iter::zip(solved_parts.clone(), calendar_entries) + let ascii_art = std::iter::zip(&solved_statuses, calendar_entries) .map(|(solved_part, entry)| { - let text = entry.text().collect::>(); + let text: Vec<_> = entry + .children() + .filter_map(|node| match node.value() { + scraper::Node::Text(text) => Some(String::from(&text[..])), + scraper::Node::Element(el) => { + if el.name() != "script" { + let el_ref = scraper::ElementRef::wrap(node).unwrap(); + Some(el_ref.text().collect::>().join("")) + } else { + None + } + } + _ => None, + }) + .collect(); Ok(match solved_part { SolvedParts::Both => text.join(""), SolvedParts::One => text .join("") .strip_suffix("*") - .ok_or_else(|| AocClientError::GetStarsError)? - .to_owned(), + .map_or_else(|| text.join(""), |stripped| String::from(stripped)), SolvedParts::None => text .join("") .strip_suffix("**") - .ok_or_else(|| AocClientError::GetStarsError)? - .to_owned(), + .map_or_else(|| text.join(""), |stripped| String::from(stripped)), }) }) - .collect::, AocClientError>>()?; - Ok(Stars::new(solved_parts, entries_without_stars)) + .collect::>>()?; + Ok(Stars::new(solved_statuses, ascii_art)) } #[cfg(test)]