1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
//! # Aoc Helpers
//! All functionality requires AoC session cookie, which you can get from you browser after logging in
//!
//! (look in developer tools)
//!
//! The most obvious way to use this library is with the `calculate_and_post` function
//!
//! ```rust
//! use aoc_driver::*;
//!
//! fn solution(i: &str) -> String { unimplemented!() }
//!
//! let session = std::fs::read_to_string(".session.txt").unwrap();
//! calculate_and_post(
//!     session,
//!     2020,
//!     1,
//!     Part1,
//!     Some("inputs/2020/1.txt"),
//!     Some("cache/2022/1.json"),
//!     solution
//! ).unwrap();
//! ```
//!
//! There is an even faster way though using the `aoc_magic` macro
//!
//! ```rust
//! use aoc_driver::*;
//!
//! fn solution(i: &str) -> String { unimplemented!() }
//!
//! let session = std::fs::read_to_string(".session.txt").unwrap();
//! aoc_magic!(session, 2020:1:1, solution).unwrap()
//! ```
//!
//! This macro does the same as the above function call (including creating an `inputs` and `cache` directory), but more concisely

#[cfg(feature = "local_cache")]
mod cache;
pub mod error;

pub use Part::*;

#[cfg(feature = "local_cache")]
use crate::cache::cache_wrapper;

use crate::error::{Error, Result};
use std::{
	fmt::Display,
	fs::File,
	io::{Read, Write},
	path::Path,
};
use ureq::{get, post};

/// Simple way to represent the challenge part
///
/// Converts into `u8`
pub enum Part {
	Part(i32),
	Part1,
	Part2,
}

impl From<Part> for i32 {
	fn from(value: Part) -> Self {
		match value {
			Part::Part(x) => x,
			Part::Part1 => 1,
			Part::Part2 => 2,
		}
	}
}

/// Get some input from the AoC website
pub fn get_input(session: &str, year: impl Into<i32>, day: impl Into<i32>) -> Result<String> {
	let url = format!(
		"https://adventofcode.com/{}/day/{}/input",
		year.into(),
		day.into()
	);
	let cookies = format!("session={}", session);
	let resp = get(&url)
		.set("User-Agent", "rust/aoc_driver")
		.set("Cookie", &cookies)
		.call()
		.map_err(|e| Error::UReq(Some(Box::new(e))))?;

	let mut body = resp.into_string()?;

	// Remove trailing newline if one exists
	if body.ends_with('\n') {
		body.pop();
	}

	Ok(body)
}

/// Gets challenge input - caching at `path` if required
///
/// Checks `path` to see if input has already been downloaded
///
/// If `path` exists will return the contents
///
/// Otherwise download the input for that day and store at `path`
pub fn get_input_or_file(
	session: &str,
	year: impl Into<i32>,
	day: impl Into<i32>,
	path: impl AsRef<Path>,
) -> Result<String> {
	let path = path.as_ref();
	match File::open(path) {
		Ok(mut f) => {
			let mut input = String::new();
			f.read_to_string(&mut input)?;
			Ok(input)
		}
		Err(_) => {
			let input = get_input(session, year, day)?;
			let mut output_file = File::create(path)?;
			output_file.write_all(input.as_bytes())?;
			Ok(input)
		}
	}
}

/// Post an answer to the AoC website.
///
/// Will also cache the result / submission at the given path if provided
///
/// Returns `Ok(())` if answer was correct or has already been given
///
/// Returns `Err(Error::Incorrect)` if the answer was wrong
///
/// Returns `Err(Error::RateLimit(String))` if you are being rate-limited
pub fn post_answer<SolOutput>(
	session: &str,
	year: i32,
	day: i32,
	part: i32,
	#[cfg_attr(not(feature = "local_cache"), allow(unused))] cache_path: Option<impl AsRef<Path>>,
	answer: SolOutput,
) -> Result<()>
where
	SolOutput: Display,
{
	let post_fn = |answer: &str| {
		let url = format!("https://adventofcode.com/{}/day/{}/answer", year, day);
		let cookies = format!("session={}", session);
		let form_level = format!("{}", part);
		let form = [("level", form_level.as_str()), ("answer", answer)];

		let resp = post(&url)
			.set("User-Agent", "rust/aoc_driver")
			.set("Cookie", &cookies)
			.send_form(&form)
			.map_err(|e| Error::UReq(Some(Box::new(e))))?;

		let body = resp.into_string().expect("response was not a string");

		let timeout_msg = "You gave an answer too recently; you have to wait after submitting an answer before trying again.  You have ";
		if let Some(index) = body.find(timeout_msg) {
			let start = index + timeout_msg.len();
			let end = body.find(" left to wait.").unwrap();
			let timeout = String::from(&body[start..end]);
			return Err(Error::RateLimit(timeout));
		}

		let correct = body.contains("That's the right answer!")
			| body.contains("Did you already complete it?");
		match correct {
			true => Ok(()),
			false => Err(Error::Incorrect),
		}
	};

	let answer = answer.to_string();

	#[cfg(feature = "local_cache")]
	return cache_wrapper(cache_path, part, &answer, post_fn);

	#[cfg(not(feature = "local_cache"))]
	return post_fn(&answer);
}

/// Fetches the challenge input, calculate the answer, and post it to the AoC website
///
/// Will cache the input at `path` if provided
///
/// Will also cache the result / submission at the given path if provided
///
/// Returns `Ok(())` if answer was correct or has already been given
///
/// Returns `Err(Error::Incorrect)` if the answer was wrong
///
/// Returns `Err(Error::RateLimit(String))` if you are being rate-limited
pub fn calculate_and_post<SolOutput, SolFn>(
	session: &str,
	year: impl Into<i32>,
	day: impl Into<i32>,
	part: impl Into<i32>,
	input_path: Option<impl AsRef<Path>>,
	cache_path: Option<impl AsRef<Path>>,
	solution: SolFn,
) -> Result<()>
where
	SolOutput: Display,
	SolFn: FnOnce(&str) -> SolOutput,
{
	let year = year.into();
	let day = day.into();
	let part = part.into();

	let input = match input_path {
		Some(path) => get_input_or_file(session, year, day, path),
		None => get_input(session, year, day),
	}?;
	let answer = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| solution(&input)))
		.map_err(|err| Error::Panic(Some(err)))?;
	post_answer(session, year, day, part, cache_path, answer)?;
	Ok(())
}

/// Magic macro to make AoC even easier
///
/// Usage: `aoc_magic!(<session cookie>, <year>:<day>:<part>, <solution function>)`
#[macro_export]
macro_rules! aoc_magic {
	($session:expr, $year:literal : $day:literal : $part:literal, $sol:expr) => {{
		let mut input_path = std::path::PathBuf::from_iter(["inputs", &$year.to_string()]);
		std::fs::create_dir_all(&input_path).unwrap();

		let file_name = format!("{}.txt", $day);
		input_path.push(file_name);

		let mut cache_path = std::path::PathBuf::from_iter(["cache", &$year.to_string()]);
		std::fs::create_dir_all(&cache_path).unwrap();

		let file_name = format!("{}.json", $day);
		cache_path.push(file_name);

		aoc_driver::calculate_and_post(
			$session,
			$year,
			$day,
			$part,
			Some(&input_path),
			Some(&cache_path),
			$sol,
		)
	}};
}