aocli 0.0.9

Advent of Code helper CLI
use regex::Regex;

use crate::error::{AocError, Context, Result, ToErr};

pub fn get_input(year: &str, day: &str, session: &str) -> Result<String> {
    let day = &day.parse::<u8>().unwrap().to_string();
    let url = format!("https://adventofcode.com/{year}/day/{day}/input");
    let response = ureq::get(&url).set("cookie", session).call();
    match response {
        Ok(response) => {
            let text = response.into_string().context(AocError::Response)?;
            Ok(text.trim_end().to_string())
        }
        Err(e) => Err(match e {
            ureq::Error::Status(_, response) => {
                let text = response.into_string().context(AocError::Response)?;
                let re = Regex::new(r"Puzzle inputs differ by user").unwrap();
                if re.is_match(&text) {
                    AocError::Session
                } else {
                    AocError::PageAvailable
                }
                .into()
            }
            ureq::Error::Transport(transport) => {
                transport.to_string().error().context(AocError::Network)
            }
        }),
    }
}

pub struct Progress {
    pub part_1: Option<String>,
    pub part_2: Option<String>,
    pub next: Option<String>,
}

pub fn get_progress(year: &str, day: &str, session: &str) -> Result<Progress> {
    let day = &day.parse::<u8>().unwrap().to_string();
    let url = format!("https://adventofcode.com/{year}/day/{day}");
    let response = ureq::get(&url).set("cookie", session).call();
    let text = match response {
        Ok(response) => {
            let text = response.into_string().context(AocError::Response)?;
            let re =
                Regex::new(r"To play, please identify yourself via one of these services").unwrap();
            if re.is_match(&text) {
                return AocError::Session.err();
            }
            text
        }
        Err(e) => {
            return match e {
                ureq::Error::Status(_, _) => AocError::PageAvailable.err(),
                ureq::Error::Transport(transport) => {
                    transport.to_string().err().context(AocError::Network)
                }
            }
        }
    };
    let re = Regex::new(r"Your puzzle answer was <code>([^<]+)</code>").unwrap();
    let caps: Vec<_> = re.captures_iter(&text).take(2).collect();
    let (part_1, part_2) = match caps.len() {
        0 => (None, None),
        1 => (Some(caps[0][1].to_string()), None),
        2 => (Some(caps[0][1].to_string()), Some(caps[1][1].to_string())),
        _ => unreachable!(),
    };
    let next = (caps.len() <= (day != "25") as usize).then_some((caps.len() as u8 + 1).to_string());
    Ok(Progress {
        part_1,
        part_2,
        next,
    })
}

pub enum SubmissionResult {
    Correct,
    Incorrect,
    Wait,
}

pub fn submit(
    year: &str,
    day: &str,
    part: &str,
    answer: &str,
    session: &str,
) -> Result<SubmissionResult> {
    let day = &day.parse::<u8>().unwrap().to_string();
    let url = format!("https://adventofcode.com/{year}/day/{day}/answer");
    let params = &[("level", part), ("answer", answer)];
    let response = ureq::post(&url).set("cookie", session).send_form(params);
    match response {
        Ok(response) => {
            let text = response.into_string().context(AocError::Response)?;
            let re = Regex::new(r"That's the right answer").unwrap();
            if re.is_match(&text) {
                return Ok(SubmissionResult::Correct);
            }
            let re = Regex::new(r"That's not the right answer").unwrap();
            if re.is_match(&text) {
                return Ok(SubmissionResult::Incorrect);
            }
            let re = Regex::new(r"You gave an answer too recently").unwrap();
            if re.is_match(&text) {
                return Ok(SubmissionResult::Wait);
            }
            AocError::Response.err()
        }
        Err(e) => match e {
            ureq::Error::Status(_, _) => AocError::PageAvailable.err(),
            ureq::Error::Transport(transport) => {
                transport.to_string().err().context(AocError::Network)
            }
        },
    }
}

pub struct YearCompletion {
    pub days: [DayCompletion; 25],
    pub total: u8,
}

#[derive(Clone, Copy)]
pub enum DayCompletion {
    None,
    Partial,
    Full,
}

pub fn get_year_completion(year: &str, session: &str) -> Result<YearCompletion> {
    let url = format!("https://adventofcode.com/{year}");
    let response = ureq::get(&url).set("cookie", session).call();
    let text = match response {
        Ok(response) => {
            let text = response.into_string().context(AocError::Response)?;
            let re = Regex::new(r"\[Log In\]").unwrap();
            if re.is_match(&text) {
                return AocError::Session.err();
            }
            text
        }
        Err(e) => {
            return match e {
                ureq::Error::Status(_, _) => AocError::PageAvailable.err(),
                ureq::Error::Transport(transport) => {
                    transport.to_string().err().context(AocError::Network)
                }
            }
        }
    };

    let mut days = [DayCompletion::None; 25];
    let mut total = 0;

    for day in 1..=25 {
        let re = Regex::new(&format!("\"Day {day}, two stars\"")).unwrap();
        if re.is_match(&text) {
            days[day - 1] = DayCompletion::Full;
            total += 2;
            continue;
        }
        let re = Regex::new(&format!("\"Day {day}, one star\"")).unwrap();
        if re.is_match(&text) {
            days[day - 1] = DayCompletion::Partial;
            total += 1;
        }
    }

    Ok(YearCompletion { days, total })
}