aocf/
lib.rs

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/// A cache entry for a single day, containing all data related to that day's problem
47#[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    /// Whether to parse CLI arguments locally
68    #[serde(skip)]
69    parse_cli: bool,
70    /// Input file provided on CLI
71    #[serde(skip)]
72    input_file: Option<PathBuf>,
73    /// Whether the process is piped
74    #[serde(skip)]
75    stream: bool,
76}
77
78impl Aoc {
79    pub fn new() -> Self {
80        Aoc { parse_cli: true, ..Default::default() }
81    }
82
83    /// Set the year
84    pub fn year(mut self, year: Option<i32>) -> Self {
85        self.year = year;
86        self
87    }
88
89    /// Set the day
90    pub fn day(mut self, day: Option<u32>) -> Self {
91        self.day = day;
92        self
93    }
94
95    /// Set cookie string
96    pub fn cookie(mut self, cookie: &str) -> Self {
97        self.cookie = cookie.to_string();
98        self
99    }
100
101    /// Set cookie file
102    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    /// Set the cache path
108//    pub fn cache<P>(&mut self, path: P) -> &mut Self
109//        where P: AsRef<Path> + std::clone::Clone,
110//    {
111    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    /// Enable or disable CLI argument parsing
117    ///
118    /// If enabled, the binary's arguments will be parsed, allowing for
119    /// example, to choose a file to read in as alternative input data,
120    /// rather than using the input data fetched from Advent of Code.
121    pub fn parse_cli(mut self, status: bool) -> Self {
122        self.parse_cli = status;
123        self
124    }
125
126    /// Initialise (finish building)
127    pub fn init(mut self) -> Result<Self, Error> {
128        // Attempt to load cookie data
129        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        // Process CLI args
138        if self.parse_cli {
139            let opt = AocOpts::parse();
140            self.input_file = opt.input;
141        }
142
143        // Process piped status of the process
144        self.stream = is(Stream::Stdin);
145
146        if let Ok(mut aoc) = self.load() {
147            // re-instate fields which will need to be overriden after successful load
148            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    /// Get the problem brief as HTML and sanitise it to markdown
158    #[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    /// Get the input data
170    pub fn get_input(&mut self, force: bool) -> Result<String, Error> {
171        // Input file provided on CLI, read it
172        if let Some(file) = &self.input_file {
173            return Ok(read_to_string(file)?.trim().to_string())
174        }
175
176        // We are piped, read the piped data
177        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        // Get input data from adventofcode.com
191        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    /// Submit the solution
202    #[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(); // Update brief (force) to update stars
208            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    /// get a JSON representation for the AoC problem
225    pub fn to_json(&self) -> Result<String, Error> {
226        Ok(serde_json::to_string_pretty(self)?)
227    }
228
229    /// get an AoC problem from JSON representation
230    pub fn from_json(json: &str) -> Result<Self, Error> {
231        Ok(serde_json::from_str(json)?)
232    }
233
234    /// Save problem to path as JSON
235    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    /// Load the problem from JSON
243    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    /// Write JSON cache
249    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    /// Get time until release
295    pub fn get_time_until_release() {
296
297    }
298}
299
300/// Get an ordered hashmap representation when serialising
301fn 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
318/// Find configuration directory in current directory or its ancestors
319pub 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            /* Very strange result on macos...
355             *
356             * ---- tests::test_find_root stdout ----
357             * thread 'tests::test_find_root' panicked at 'assertion failed: `(left == right)`
358             * left: `"/private/var/folders/24/8k48jl6d249_n_qfxwsl6xvm0000gn/T/.tmpUwUaSn"`,
359             * right: `"/var/folders/24/8k48jl6d249_n_qfxwsl6xvm0000gn/T/.tmpUwUaSn"`', src/lib.rs:292:13
360             * note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
361             *
362            let left: PathBuf = find_root().unwrap().components().skip(5).collect();
363            let right: PathBuf = tmp_path.components().skip(4).collect();
364            assert_eq!(left, right);
365            */
366            assert_eq!(find_root().unwrap(), tmp_path);
367        }
368    }
369}