aoc_driver/
lib.rs

1//! # Aoc Helpers
2//! All functionality requires AoC session cookie, which you can get from you browser after logging in
3//!
4//! (look in developer tools)
5//!
6//! The most obvious way to use this library is with the `calculate_and_post` function
7//!
8//! ```rust
9//! use aoc_driver::*;
10//!
11//! fn solution(i: &str) -> String { unimplemented!() }
12//!
13//! let session = std::fs::read_to_string(".session.txt").unwrap();
14//! calculate_and_post(
15//!     session,
16//!     2020,
17//!     1,
18//!     Part1,
19//!     Some("inputs/2020/1.txt"),
20//!     Some("cache/2022/1.json"),
21//!     solution
22//! ).unwrap();
23//! ```
24//!
25//! There is an even faster way though using the `aoc_magic` macro
26//!
27//! ```rust
28//! use aoc_driver::*;
29//!
30//! fn solution(i: &str) -> String { unimplemented!() }
31//!
32//! let session = std::fs::read_to_string(".session.txt").unwrap();
33//! aoc_magic!(session, 2020:1:1, solution).unwrap()
34//! ```
35//!
36//! This macro does the same as the above function call (including creating an `inputs` and `cache` directory), but more concisely
37
38#[cfg(feature = "local_cache")]
39mod cache;
40pub mod error;
41
42pub use Part::*;
43
44#[cfg(feature = "local_cache")]
45use crate::cache::cache_wrapper;
46
47use crate::error::{Error, Result};
48use std::{
49	fmt::Display,
50	fs::File,
51	io::{Read, Write},
52	path::Path,
53};
54use ureq::{get, post};
55
56/// Simple way to represent the challenge part
57///
58/// Converts into `u8`
59pub enum Part {
60	Part(i32),
61	Part1,
62	Part2,
63}
64
65impl From<Part> for i32 {
66	fn from(value: Part) -> Self {
67		match value {
68			Part::Part(x) => x,
69			Part::Part1 => 1,
70			Part::Part2 => 2,
71		}
72	}
73}
74
75/// Get some input from the AoC website
76pub fn get_input(session: &str, year: impl Into<i32>, day: impl Into<i32>) -> Result<String> {
77	let url = format!(
78		"https://adventofcode.com/{}/day/{}/input",
79		year.into(),
80		day.into()
81	);
82	let cookies = format!("session={}", session);
83	let resp = get(&url)
84		.set("User-Agent", "rust/aoc_driver")
85		.set("Cookie", &cookies)
86		.call()
87		.map_err(|e| Error::UReq(Some(Box::new(e))))?;
88
89	let mut body = resp.into_string()?;
90
91	// Remove trailing newline if one exists
92	if body.ends_with('\n') {
93		body.pop();
94	}
95
96	Ok(body)
97}
98
99/// Gets challenge input - caching at `path` if required
100///
101/// Checks `path` to see if input has already been downloaded
102///
103/// If `path` exists will return the contents
104///
105/// Otherwise download the input for that day and store at `path`
106pub fn get_input_or_file(
107	session: &str,
108	year: impl Into<i32>,
109	day: impl Into<i32>,
110	path: impl AsRef<Path>,
111) -> Result<String> {
112	let path = path.as_ref();
113	match File::open(path) {
114		Ok(mut f) => {
115			let mut input = String::new();
116			f.read_to_string(&mut input)?;
117			Ok(input)
118		}
119		Err(_) => {
120			let input = get_input(session, year, day)?;
121			let mut output_file = File::create(path)?;
122			output_file.write_all(input.as_bytes())?;
123			Ok(input)
124		}
125	}
126}
127
128/// Post an answer to the AoC website.
129///
130/// Will also cache the result / submission at the given path if provided
131///
132/// Returns `Ok(())` if answer was correct or has already been given
133///
134/// Returns `Err(Error::Incorrect)` if the answer was wrong
135///
136/// Returns `Err(Error::RateLimit(String))` if you are being rate-limited
137pub fn post_answer<SolOutput>(
138	session: &str,
139	year: i32,
140	day: i32,
141	part: i32,
142	#[cfg_attr(not(feature = "local_cache"), allow(unused))] cache_path: Option<impl AsRef<Path>>,
143	answer: SolOutput,
144) -> Result<()>
145where
146	SolOutput: Display,
147{
148	let post_fn = |answer: &str| {
149		let url = format!("https://adventofcode.com/{}/day/{}/answer", year, day);
150		let cookies = format!("session={}", session);
151		let form_level = format!("{}", part);
152		let form = [("level", form_level.as_str()), ("answer", answer)];
153
154		let resp = post(&url)
155			.set("User-Agent", "rust/aoc_driver")
156			.set("Cookie", &cookies)
157			.send_form(&form)
158			.map_err(|e| Error::UReq(Some(Box::new(e))))?;
159
160		let body = resp.into_string().expect("response was not a string");
161
162		let timeout_msg = "You gave an answer too recently; you have to wait after submitting an answer before trying again.  You have ";
163		if let Some(index) = body.find(timeout_msg) {
164			let start = index + timeout_msg.len();
165			let end = body.find(" left to wait.").unwrap();
166			let timeout = String::from(&body[start..end]);
167			return Err(Error::RateLimit(timeout));
168		}
169
170		let correct = body.contains("That's the right answer!")
171			| body.contains("Did you already complete it?");
172		match correct {
173			true => Ok(()),
174			false => Err(Error::Incorrect),
175		}
176	};
177
178	let answer = answer.to_string();
179
180	#[cfg(feature = "local_cache")]
181	return cache_wrapper(cache_path, part, &answer, post_fn);
182
183	#[cfg(not(feature = "local_cache"))]
184	return post_fn(&answer);
185}
186
187/// Fetches the challenge input, calculate the answer, and post it to the AoC website
188///
189/// Will cache the input at `path` if provided
190///
191/// Will also cache the result / submission at the given path if provided
192///
193/// Returns `Ok(())` if answer was correct or has already been given
194///
195/// Returns `Err(Error::Incorrect)` if the answer was wrong
196///
197/// Returns `Err(Error::RateLimit(String))` if you are being rate-limited
198pub fn calculate_and_post<SolOutput, SolFn>(
199	session: &str,
200	year: impl Into<i32>,
201	day: impl Into<i32>,
202	part: impl Into<i32>,
203	input_path: Option<impl AsRef<Path>>,
204	cache_path: Option<impl AsRef<Path>>,
205	solution: SolFn,
206) -> Result<()>
207where
208	SolOutput: Display,
209	SolFn: FnOnce(&str) -> SolOutput,
210{
211	let year = year.into();
212	let day = day.into();
213	let part = part.into();
214
215	let input = match input_path {
216		Some(path) => get_input_or_file(session, year, day, path),
217		None => get_input(session, year, day),
218	}?;
219	let answer = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| solution(&input)))
220		.map_err(|err| Error::Panic(Some(err)))?;
221	post_answer(session, year, day, part, cache_path, answer)?;
222	Ok(())
223}
224
225/// Magic macro to make AoC even easier
226///
227/// Usage: `aoc_magic!(<session cookie>, <year>:<day>:<part>, <solution function>)`
228#[macro_export]
229macro_rules! aoc_magic {
230	($session:expr, $year:literal : $day:literal : $part:literal, $sol:expr) => {{
231		let mut input_path = std::path::PathBuf::from_iter(["inputs", &$year.to_string()]);
232		std::fs::create_dir_all(&input_path).unwrap();
233
234		let file_name = format!("{}.txt", $day);
235		input_path.push(file_name);
236
237		let mut cache_path = std::path::PathBuf::from_iter(["cache", &$year.to_string()]);
238		std::fs::create_dir_all(&cache_path).unwrap();
239
240		let file_name = format!("{}.json", $day);
241		cache_path.push(file_name);
242
243		aoc_driver::calculate_and_post(
244			$session,
245			$year,
246			$day,
247			$part,
248			Some(&input_path),
249			Some(&cache_path),
250			$sol,
251		)
252	}};
253}