aoc_helpers/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use anyhow::Result;
4use reqwest::{blocking::Client, cookie::Jar, header::CONTENT_TYPE};
5use std::{collections::BTreeMap, env, fs, sync::Arc};
6
7const AOC_ROOT: &str = "https://adventofcode.com";
8const PROGRESS_FILE: &str = "aoc_progress.json";
9
10type Progress = BTreeMap<u8, (bool, bool)>;
11
12/// Defines solutions & parser for a day
13pub trait Day {
14    /// The day this solution is for
15    const DAY: u8;
16
17    /// Parse the aoc input
18    fn from_input(input: String) -> Self;
19
20    /// The first part of the exercise
21    fn first_part(&mut self) -> String;
22    /// The second part of the exercise
23    fn second_part(&mut self) -> String;
24}
25
26/// Advent of code session, main entry point to the library
27pub struct AocSession {
28    year: u16,
29    client: Client,
30    progress: Progress,
31}
32
33impl AocSession {
34    /// Creates a new session, using the session data from the environment variable `AOC_SESSION`
35    pub fn new(year: u16) -> Result<Self> {
36        let session = env::var("AOC_SESSION")?;
37
38        Self::new_from_session(year, session)
39    }
40
41    /// Creates a new session from a custom session value
42    pub fn new_from_session(year: u16, session: String) -> Result<Self> {
43        let cookies = Jar::default();
44        cookies.add_cookie_str(format!("session={session}").as_str(), &AOC_ROOT.parse()?);
45        let client = Client::builder()
46            .cookie_provider(Arc::new(cookies))
47            .build()?;
48
49        let progress: Progress =
50            if let Ok(Ok(progress)) = fs::read_to_string(PROGRESS_FILE).map(|content | serde_json::from_str(&content)) {
51                progress
52            } else {
53                Default::default()
54            };
55
56        Ok(Self {
57            client,
58            year,
59            progress,
60        })
61    }
62
63    fn save_progress(&mut self, day: u8, second_part: bool) -> Result<()> {
64        if let Some(day_progress) = self.progress.get_mut(&day) {
65            if second_part {
66                day_progress.1 = true
67            } else {
68                day_progress.0 = true
69            }
70        } else {
71            self.progress.insert(
72                day,
73                if second_part {
74                    (false, true)
75                } else {
76                    (true, false)
77                },
78            );
79        }
80
81        Ok(fs::write(
82            PROGRESS_FILE,
83            serde_json::to_string(&self.progress)?,
84        )?)
85    }
86
87    fn submit_part(&mut self, day: u8, part: u8, answer: String) -> Result<bool> {
88        let submission_result = self
89            .client
90            .post(format!("{AOC_ROOT}/{}/day/{}/answer", self.year, day))
91            .body(format!("level={part}&answer={answer}"))
92            .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
93            .send()?
94            .text()?;
95
96        if submission_result.contains("That's not the right answer") {
97            println!("Answer {} is wrong (day {} part {})", answer, day, part);
98
99            return Ok(false);
100        } else if !submission_result.contains("You don't seem to be solving the right level") {
101            println!("Challenge completed successfully #{} {}/2", day, part);
102        }
103
104        self.save_progress(day, part == 2)?;
105
106        Ok(true)
107    }
108
109    /// Sets solution for a day.
110    ///
111    /// The solution is only executed if it hasnt succeded previously.
112    ///
113    /// ## Usage
114    ///
115    /// This method takes a generic [`Day`]. In the [`Day`] implementing struct you can store eg. the parsed input data from AOC
116    pub fn day<D: Day>(mut self) -> Result<Self> {
117        let day = D::DAY;
118
119        let curret_progress = self.progress.get(&day).map(ToOwned::to_owned);
120
121        if matches!(curret_progress, Some((true, true))) {
122            return Ok(self);
123        };
124
125        println!("Running day #{}", day);
126
127        let input = self
128            .client
129            .get(format!("{AOC_ROOT}/{}/day/{}/input", self.year, day))
130            .send()?
131            .text()?;
132
133        let mut parsed = D::from_input(input);
134
135        if matches!(curret_progress, None | Some((false, _))) {
136            self.submit_part(day, 1, parsed.first_part())?;
137        }
138
139        if matches!(curret_progress, None | Some((_, false))) {
140            self.submit_part(day, 2, parsed.second_part())?;
141        }
142
143        Ok(self)
144    }
145}