1#[cfg(feature = "sqlite")]
2#[macro_use] extern crate diesel;
3#[macro_use] extern crate serde_derive;
4
5use std::collections::{HashMap, BTreeMap};
6use std::fmt;
7use std::fs::{File, read_to_string, create_dir_all};
8use std::io::{self, Write, BufRead};
9use std::path::{Path, PathBuf};
10use std::env::current_dir;
11use serde::{Serialize, Serializer};
12use failure::{Error, bail};
13
14mod http;
15#[cfg(feature = "sqlite")]
16pub mod cookie;
17mod cli;
18
19use cli::AocOpts;
20use clap::Parser;
21use atty::{is, Stream};
22
23#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum Level {
26 First,
27 Second,
28}
29
30impl Default for Level {
31 fn default() -> Self {
32 Self::First
33 }
34}
35
36impl fmt::Display for Level {
37 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
38 let s = match self {
39 Level::First => "first",
40 Level::Second => "second",
41 };
42 write!(f, "{}", s)
43 }
44}
45
46#[derive(Debug, Default, Clone, Serialize, Deserialize)]
48pub struct Aoc {
49 pub year: Option<i32>,
50 pub day: Option<u32>,
51 pub level: Level,
52 pub title: Option<String>,
53 pub stars: Option<u8>,
54 pub solution: HashMap<Level, String>,
55
56 input: Option<String>,
57 #[serde(serialize_with = "ordered_map")]
58 brief: HashMap<Level, String>,
59 #[serde(serialize_with = "ordered_map")]
60
61 #[serde(skip)]
62 cookie: String,
63 #[serde(skip)]
64 cache_path: Option<PathBuf>,
65 #[serde(skip)]
66 cookie_path: Option<PathBuf>,
67 #[serde(skip)]
69 parse_cli: bool,
70 #[serde(skip)]
72 input_file: Option<PathBuf>,
73 #[serde(skip)]
75 stream: bool,
76}
77
78impl Aoc {
79 pub fn new() -> Self {
80 Aoc { parse_cli: true, ..Default::default() }
81 }
82
83 pub fn year(mut self, year: Option<i32>) -> Self {
85 self.year = year;
86 self
87 }
88
89 pub fn day(mut self, day: Option<u32>) -> Self {
91 self.day = day;
92 self
93 }
94
95 pub fn cookie(mut self, cookie: &str) -> Self {
97 self.cookie = cookie.to_string();
98 self
99 }
100
101 pub fn cookie_file(mut self, path: impl AsRef<Path>) -> Self {
103 self.cookie_path = Some(path.as_ref().to_path_buf());
104 self
105 }
106
107 pub fn cache<P>(mut self, path: Option<&Path>) -> Self {
112 self.cache_path = path.as_ref().map(PathBuf::from);
113 self
114 }
115
116 pub fn parse_cli(mut self, status: bool) -> Self {
122 self.parse_cli = status;
123 self
124 }
125
126 pub fn init(mut self) -> Result<Self, Error> {
128 if self.cookie.is_empty() {
130 if let Some(p) = &self.cookie_path {
131 self.cookie = read_to_string(p)?.trim().to_string()
132 } else if let Ok(p) = self.get_default_cookie_path() {
133 self.cookie = read_to_string(p)?.trim().to_string()
134 };
135 }
136
137 if self.parse_cli {
139 let opt = AocOpts::parse();
140 self.input_file = opt.input;
141 }
142
143 self.stream = is(Stream::Stdin);
145
146 if let Ok(mut aoc) = self.load() {
147 aoc.cookie = self.cookie;
149 aoc.input_file = self.input_file;
150 aoc.stream = self.stream;
151 Ok(aoc)
152 } else {
153 Ok(self)
154 }
155 }
156
157 #[cfg(feature = "html_parsing")]
159 pub fn get_brief(&mut self, force: bool) -> Result<String, Error> {
160 if self.brief.get(&self.level).is_none() || force {
161 let brief = http::get_brief(self)?;
162 self.title = Some(brief.0);
163 self.brief.insert(self.level, brief.1);
164 };
165 self.write()?;
166 Ok(self.brief.get(&self.level).unwrap().to_string())
167 }
168
169 pub fn get_input(&mut self, force: bool) -> Result<String, Error> {
171 if let Some(file) = &self.input_file {
173 return Ok(read_to_string(file)?.trim().to_string())
174 }
175
176 if !self.stream {
178 let stdin = io::stdin();
179
180 let data = stdin.lock().lines()
181 .flatten()
182 .fold(String::new(), |mut acc, line| {
183 acc.push_str(&format!("{}\n", line));
184 acc
185 });
186
187 return Ok(data);
188 }
189
190 if self.input.is_none() || force {
192 let input = http::get_input(self)?;
193 self.input = Some(input);
194 }
195
196 self.write()?;
197
198 Ok(self.input.clone().unwrap())
199 }
200
201 #[cfg(feature = "html_parsing")]
203 pub fn submit(&mut self, solution: &str) -> Result<String, Error> {
204 let resp = http::submit(self, solution)?;
205 if http::verify(&resp) {
206 self.solution.insert(self.level, solution.to_string());
207 self.get_brief(true).ok(); self.add_star();
209 self.advance().unwrap_or(());
210 }
211 self.write()?;
212 Ok(resp)
213 }
214
215 #[cfg(feature = "html_parsing")]
216 fn add_star(&mut self) {
217 if let Some(ref stars) = self.stars {
218 self.stars = Some(stars + 1);
219 } else {
220 self.stars = Some(1);
221 };
222 }
223
224 pub fn to_json(&self) -> Result<String, Error> {
226 Ok(serde_json::to_string_pretty(self)?)
227 }
228
229 pub fn from_json(json: &str) -> Result<Self, Error> {
231 Ok(serde_json::from_str(json)?)
232 }
233
234 pub fn write_json_to(&self, path: impl AsRef<Path>) -> Result<(), Error> {
236 ensure_parent_dir(path.as_ref())?;
237 let mut file = File::create(path)?;
238 file.write_all(self.to_json()?.as_bytes())?;
239 Ok(())
240 }
241
242 pub fn load_json_from(path: impl AsRef<Path>) -> Result<Self, Error> {
244 let json = read_to_string(path)?;
245 Self::from_json(&json)
246 }
247
248 pub fn write(&self) -> Result<(), Error> {
250 if let Some(ref p) = self.cache_path {
251 self.write_json_to(p)
252 } else {
253 self.write_json_to(self.get_default_cache_path()?)
254 }
255 }
256
257 pub fn advance(&mut self) -> Result<(), Error> {
258 match self.level {
259 Level::First => { self.level = Level::Second; Ok(()) },
260 Level::Second => bail!("already on part 2"),
261 }
262 }
263
264 fn load(&self) -> Result<Self, Error> {
265 if let Some(ref p) = self.cache_path {
266 Self::load_json_from(p)
267 } else {
268 Self::load_json_from(self.get_default_cache_path()?)
269 }
270 }
271
272 fn get_default_cookie_path(&self) -> Result<PathBuf, Error> {
273 let p = PathBuf::from("./.aocf/cookie");
274 if let Ok(r) = find_root() {
275 Ok(r.join(p))
276 } else {
277 Ok(p)
278 }
279 }
280
281 fn get_default_cache_path(&self) -> Result<PathBuf, Error> {
282 if let (Some(y), Some(d)) = (self.year, self.day) {
283 let p = PathBuf::from(&format!("./.aocf/cache/aoc{}_{:02}.json", y, d));
284 if let Ok(r) = find_root() {
285 Ok(r.join(p))
286 } else {
287 Ok(p)
288 }
289 } else {
290 bail!("day or year not set");
291 }
292 }
293
294 pub fn get_time_until_release() {
296
297 }
298}
299
300fn ordered_map<S>(value: &HashMap<Level, String>, serializer: S) -> Result<S::Ok, S::Error>
302where
303 S: Serializer,
304{
305 let ordered: BTreeMap<_, _> = value.iter().collect();
306 ordered.serialize(serializer)
307}
308
309fn ensure_parent_dir(file: impl AsRef<Path>) -> Result<(), Error> {
310 let without_path = file.as_ref().components().count() == 1;
311 match file.as_ref().parent() {
312 Some(dir) if !without_path => create_dir_all(dir)?,
313 _ => (),
314 };
315 Ok(())
316}
317
318pub fn find_root() -> Result<PathBuf, Error> {
320 let cwd = current_dir()?;
321
322 let conf_dir = cwd.ancestors()
323 .find(|dir| dir.join(".aocf").is_dir())
324 .filter(|dir| dir.join(".aocf/config").is_file());
325
326 match conf_dir {
327 Some(dir) => Ok(dir.to_path_buf()),
328 None => bail!("no configuration found, maybe you need to run `aocf init`"),
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use tempfile::tempdir;
336 use std::env;
337 use std::fs;
338
339 #[test]
340 fn test_find_root() {
341 let tmp = tempdir().unwrap();
342 let tmp_path = tmp.path();
343 let tmp_sub = tmp_path.join("im/in/a-subdir");
344 fs::create_dir_all(&tmp_sub).unwrap();
345
346 env::set_current_dir(tmp_path).unwrap();
347 assert!(find_root().is_err());
348 fs::create_dir(tmp_path.join(".aocf")).unwrap();
349 assert!(find_root().is_err());
350 File::create(tmp_path.join(".aocf/config")).unwrap();
351 assert!(find_root().is_ok());
352 env::set_current_dir(tmp_sub).unwrap();
353 if cfg!(linux) || cfg!(windows) {
354 assert_eq!(find_root().unwrap(), tmp_path);
367 }
368 }
369}