#[cfg(feature = "sqlite")]
#[macro_use] extern crate diesel;
#[macro_use] extern crate serde_derive;
use std::collections::{HashMap, BTreeMap};
use std::fmt;
use std::fs::{File, read_to_string, create_dir_all};
use std::io::{self, Write, BufRead};
use std::path::{Path, PathBuf};
use std::env::current_dir;
use serde::{Serialize, Serializer};
use failure::{Error, bail};
mod http;
#[cfg(feature = "sqlite")]
pub mod cookie;
mod cli;
use cli::AocOpts;
use clap::Parser;
use atty::{is, Stream};
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Level {
First,
Second,
}
impl Default for Level {
fn default() -> Self {
Self::First
}
}
impl fmt::Display for Level {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Level::First => "first",
Level::Second => "second",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Aoc {
pub year: Option<i32>,
pub day: Option<u32>,
pub level: Level,
pub title: Option<String>,
pub stars: Option<u8>,
pub solution: HashMap<Level, String>,
input: Option<String>,
#[serde(serialize_with = "ordered_map")]
brief: HashMap<Level, String>,
#[serde(serialize_with = "ordered_map")]
#[serde(skip)]
cookie: String,
#[serde(skip)]
cache_path: Option<PathBuf>,
#[serde(skip)]
cookie_path: Option<PathBuf>,
#[serde(skip)]
parse_cli: bool,
#[serde(skip)]
input_file: Option<PathBuf>,
#[serde(skip)]
stream: bool,
}
impl Aoc {
pub fn new() -> Self {
Aoc { parse_cli: true, ..Default::default() }
}
pub fn year(mut self, year: Option<i32>) -> Self {
self.year = year;
self
}
pub fn day(mut self, day: Option<u32>) -> Self {
self.day = day;
self
}
pub fn cookie(mut self, cookie: &str) -> Self {
self.cookie = cookie.to_string();
self
}
pub fn cookie_file(mut self, path: impl AsRef<Path>) -> Self {
self.cookie_path = Some(path.as_ref().to_path_buf());
self
}
pub fn cache<P>(mut self, path: Option<&Path>) -> Self {
self.cache_path = path.as_ref().map(PathBuf::from);
self
}
pub fn parse_cli(mut self, status: bool) -> Self {
self.parse_cli = status;
self
}
pub fn init(mut self) -> Result<Self, Error> {
if self.cookie.is_empty() {
if let Some(p) = &self.cookie_path {
self.cookie = read_to_string(p)?.trim().to_string()
} else if let Ok(p) = self.get_default_cookie_path() {
self.cookie = read_to_string(p)?.trim().to_string()
};
}
if self.parse_cli {
let opt = AocOpts::parse();
self.input_file = opt.input;
}
self.stream = is(Stream::Stdin);
if let Ok(mut aoc) = self.load() {
aoc.cookie = self.cookie;
aoc.input_file = self.input_file;
aoc.stream = self.stream;
Ok(aoc)
} else {
Ok(self)
}
}
#[cfg(feature = "html_parsing")]
pub fn get_brief(&mut self, force: bool) -> Result<String, Error> {
if self.brief.get(&self.level).is_none() || force {
let brief = http::get_brief(self)?;
self.title = Some(brief.0);
self.brief.insert(self.level, brief.1);
};
self.write()?;
Ok(self.brief.get(&self.level).unwrap().to_string())
}
pub fn get_input(&mut self, force: bool) -> Result<String, Error> {
if let Some(file) = &self.input_file {
return Ok(read_to_string(file)?.trim().to_string())
}
if !self.stream {
let stdin = io::stdin();
let data = stdin.lock().lines()
.flatten()
.fold(String::new(), |mut acc, line| {
acc.push_str(&format!("{}\n", line));
acc
});
return Ok(data);
}
if self.input.is_none() || force {
let input = http::get_input(self)?;
self.input = Some(input);
}
self.write()?;
Ok(self.input.clone().unwrap())
}
#[cfg(feature = "html_parsing")]
pub fn submit(&mut self, solution: &str) -> Result<String, Error> {
let resp = http::submit(self, solution)?;
if http::verify(&resp) {
self.solution.insert(self.level, solution.to_string());
self.get_brief(true).ok(); self.add_star();
self.advance().unwrap_or(());
}
self.write()?;
Ok(resp)
}
#[cfg(feature = "html_parsing")]
fn add_star(&mut self) {
if let Some(ref stars) = self.stars {
self.stars = Some(stars + 1);
} else {
self.stars = Some(1);
};
}
pub fn to_json(&self) -> Result<String, Error> {
Ok(serde_json::to_string_pretty(self)?)
}
pub fn from_json(json: &str) -> Result<Self, Error> {
Ok(serde_json::from_str(json)?)
}
pub fn write_json_to(&self, path: impl AsRef<Path>) -> Result<(), Error> {
ensure_parent_dir(path.as_ref())?;
let mut file = File::create(path)?;
file.write_all(self.to_json()?.as_bytes())?;
Ok(())
}
pub fn load_json_from(path: impl AsRef<Path>) -> Result<Self, Error> {
let json = read_to_string(path)?;
Self::from_json(&json)
}
pub fn write(&self) -> Result<(), Error> {
if let Some(ref p) = self.cache_path {
self.write_json_to(p)
} else {
self.write_json_to(self.get_default_cache_path()?)
}
}
pub fn advance(&mut self) -> Result<(), Error> {
match self.level {
Level::First => { self.level = Level::Second; Ok(()) },
Level::Second => bail!("already on part 2"),
}
}
fn load(&self) -> Result<Self, Error> {
if let Some(ref p) = self.cache_path {
Self::load_json_from(p)
} else {
Self::load_json_from(self.get_default_cache_path()?)
}
}
fn get_default_cookie_path(&self) -> Result<PathBuf, Error> {
let p = PathBuf::from("./.aocf/cookie");
if let Ok(r) = find_root() {
Ok(r.join(p))
} else {
Ok(p)
}
}
fn get_default_cache_path(&self) -> Result<PathBuf, Error> {
if let (Some(y), Some(d)) = (self.year, self.day) {
let p = PathBuf::from(&format!("./.aocf/cache/aoc{}_{:02}.json", y, d));
if let Ok(r) = find_root() {
Ok(r.join(p))
} else {
Ok(p)
}
} else {
bail!("day or year not set");
}
}
pub fn get_time_until_release() {
}
}
fn ordered_map<S>(value: &HashMap<Level, String>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let ordered: BTreeMap<_, _> = value.iter().collect();
ordered.serialize(serializer)
}
fn ensure_parent_dir(file: impl AsRef<Path>) -> Result<(), Error> {
let without_path = file.as_ref().components().count() == 1;
match file.as_ref().parent() {
Some(dir) if !without_path => create_dir_all(dir)?,
_ => (),
};
Ok(())
}
pub fn find_root() -> Result<PathBuf, Error> {
let cwd = current_dir()?;
let conf_dir = cwd.ancestors()
.find(|dir| dir.join(".aocf").is_dir())
.filter(|dir| dir.join(".aocf/config").is_file());
match conf_dir {
Some(dir) => Ok(dir.to_path_buf()),
None => bail!("no configuration found, maybe you need to run `aocf init`"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::env;
use std::fs;
#[test]
fn test_find_root() {
let tmp = tempdir().unwrap();
let tmp_path = tmp.path();
let tmp_sub = tmp_path.join("im/in/a-subdir");
fs::create_dir_all(&tmp_sub).unwrap();
env::set_current_dir(tmp_path).unwrap();
assert!(find_root().is_err());
fs::create_dir(tmp_path.join(".aocf")).unwrap();
assert!(find_root().is_err());
File::create(tmp_path.join(".aocf/config")).unwrap();
assert!(find_root().is_ok());
env::set_current_dir(tmp_sub).unwrap();
if cfg!(linux) || cfg!(windows) {
assert_eq!(find_root().unwrap(), tmp_path);
}
}
}