diff --git a/Cargo.toml b/Cargo.toml index 6173b6d..576d2d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "elv" description = "A little CLI helper for Advent of Code. 🎄" -version = "0.13.1" +version = "0.13.2" authors = ["Konrad Pagacz "] edition = "2021" readme = "README.md" diff --git a/README.md b/README.md index 2c3e825..9af6fe3 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ instead of the webpage. So far `elv` supports: - guessing the year and day of a riddle based on the current date - caching `AoC` responses whenever possible, so you minimize your footprint on `AoC`'s servers +- two functions that let you use `elv` as a library in your own + `Rust`-based application or code ## Installation @@ -158,6 +160,29 @@ brew uninstall kpagacz/elv/elv brew autoremove ``` +## Library + +`elv` exposes a supremely small library that you can use in your scripts or +applications. These include: +* `elv::get_input` - a function that downloads the input for a given year and day +* `elv::submit` - a function that submits the solution to a given year and day + +These functions have decent documentation that you can browse +[here](https://docs.rs/elv/latest/elv/). Here is a small example from the docs: + +```rust +// Will succeed if your token is set using another way +get_input(1, 2023, None).unwrap() +submit(20, 2019, "something", 2, Some("Mytoken")).unwrap(); +``` + +You can also use the `Driver` object to perform even more actions, but +this is not recommended as the API is not stable and may change in the +future. The `Driver` struct is also poorly documented. + +Let me know at `konrad.pagacz@gmail.com` or file an issue +if you want to get more functions exposed in the library. + ## Examples You need an Advent of Code session token to interact with its API. `elv` diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..40eb3c0 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,88 @@ +use crate::{domain::riddle_part::RiddlePart, Configuration, Driver}; +use anyhow::Result; + +/// Downloads the input from Advent of Code servers +/// +/// # Arguments +/// +/// * `day` - the day of the challenge. [1 - 25] +/// * `year` - the year of the challenge. E.g. 2023 +/// * `token` - optionally, the token used to authenticate you against AOC servers +/// +/// # Token +/// +/// You need the token to authenticate against the AOC servers. This function will not work +/// without it. You can pass it directly to this function or set it via one of the other methods. +/// See [the README](https://github.com/kpagacz/elv#faq) for more information. +/// +/// If you set the token using the CLI or in the configuration file, this function will reuse +/// it and you will not need to additionally pass the token to the function. +/// +/// # Examples +/// +/// ``` +/// use elv::get_input; +/// fn download_input() -> String { +/// // Will succeed if your token is set using another way +/// get_input(1, 2023, None).unwrap() +/// } +/// fn download_input_with_token() -> String { +/// // No need to set the token in any other way. +/// get_input(1, 2023, Some("123456yourtoken")).unwrap() +/// } +/// ``` +pub fn get_input(day: usize, year: usize, token: Option<&str>) -> Result { + let mut config = Configuration::new(); + if let Some(token) = token { + config.aoc.token = token.to_owned(); + } + + let driver = Driver::new(config); + driver.input(year, day) +} + +/// Submits an answer to Advent of Code servers +/// +/// # Arguments +/// +/// * `day` - the day of the challenge. [1 - 25] +/// * `year` - the year of the challenge. E.g. 2023 +/// * `answer` - the submitted answer +/// * `riddle_part` - either 1 or 2 indicating, respectively, part one and two of the riddle +/// * `token` - optionally, the token used to authenticate you against AOC servers +/// +/// # Examples +/// +/// ``` +/// use elv::submit; +/// fn submit_answer(answer: &str) { +/// // Submits answer `12344` to the first part of thefirst day of the 2023 AOC. +/// // This invocation will not work if you do not supply the token +/// // some other way. +/// submit(1, 2023, "12344", 1, None).unwrap(); +/// // Submits answer `something` to the second part of the 20th day of the 2019 challenge. +/// // This invocation does not need the token set any other way. +/// submit(20, 2019, "something", 2, Some("Mytoken")).unwrap(); +/// } +/// ``` +pub fn submit( + day: usize, + year: usize, + answer: &str, + riddle_part: u8, + token: Option<&str>, +) -> Result<()> { + let mut config = Configuration::new(); + if let Some(token) = token { + config.aoc.token = token.to_owned(); + } + + let driver = Driver::new(config); + let part = match riddle_part { + 1 => RiddlePart::One, + 2 => RiddlePart::Two, + _ => RiddlePart::One, + }; + driver.submit_answer(year, day, part, answer.to_owned())?; + Ok(()) +} diff --git a/src/application/cli.rs b/src/application/cli.rs index aeab472..99d7960 100644 --- a/src/application/cli.rs +++ b/src/application/cli.rs @@ -234,7 +234,7 @@ impl ElvCli { } } - fn determine_date(riddle_args: RiddleArgs) -> Result<(i32, i32), anyhow::Error> { + fn determine_date(riddle_args: RiddleArgs) -> Result<(usize, usize)> { let est_now = chrono::Utc::now() - chrono::Duration::hours(4); let best_guess_date = RiddleDate::best_guess(riddle_args.year, riddle_args.day, est_now)?; diff --git a/src/application/cli/cli_command.rs b/src/application/cli/cli_command.rs index 52ce853..88e40c3 100644 --- a/src/application/cli/cli_command.rs +++ b/src/application/cli/cli_command.rs @@ -13,16 +13,16 @@ pub struct RiddleArgs { /// If you do not supply a year and a day, the current year will be used. /// If you do not supply a year, but supply a day, the previous year /// will be used. - #[arg(short, long, value_parser = clap::value_parser!(i32))] - pub year: Option, + #[arg(short, long, value_parser = clap::value_parser!(usize))] + pub year: Option, /// The day of the challenge /// /// If you do not supply a day, the current day of the month will be used /// (if the current month is December). If the current month is not December, /// the application will not be able to guess the day. - #[arg(short, long, value_parser = clap::value_parser!(i32))] - pub day: Option, + #[arg(short, long, value_parser = clap::value_parser!(usize))] + pub day: Option, } #[derive(Debug, Args)] @@ -200,6 +200,6 @@ pub enum CliCommand { #[command(verbatim_doc_comment, visible_aliases = ["t", "sett", "set-token"])] Token { /// Token to be saved - token: Option - } + token: Option, + }, } diff --git a/src/domain/ports.rs b/src/domain/ports.rs index b4e9802..8997710 100644 --- a/src/domain/ports.rs +++ b/src/domain/ports.rs @@ -4,3 +4,4 @@ pub(crate) mod get_leaderboard; pub(crate) mod get_private_leaderboard; pub(crate) mod get_stars; pub(crate) mod input_cache; +pub(crate) mod get_input; diff --git a/src/domain/ports/aoc_client.rs b/src/domain/ports/aoc_client.rs index 641717d..08c3f6a 100644 --- a/src/domain/ports/aoc_client.rs +++ b/src/domain/ports/aoc_client.rs @@ -1,16 +1,12 @@ -use crate::{ - domain::{ - description::Description, submission::Submission, submission_result::SubmissionResult, - }, - infrastructure::aoc_api::aoc_client_impl::InputResponse, +use crate::domain::{ + description::Description, submission::Submission, submission_result::SubmissionResult, }; use super::errors::AocClientError; pub trait AocClient { fn submit_answer(&self, submission: Submission) -> Result; - fn get_description(&self, year: i32, day: i32) -> Result + fn get_description(&self, year: usize, day: usize) -> Result where Desc: Description + TryFrom; - fn get_input(&self, year: i32, day: i32) -> InputResponse; } diff --git a/src/domain/ports/get_input.rs b/src/domain/ports/get_input.rs new file mode 100644 index 0000000..641ee0e --- /dev/null +++ b/src/domain/ports/get_input.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub trait GetInput { + fn get_input(&self, day: usize, year: usize) -> Result; +} diff --git a/src/domain/ports/input_cache.rs b/src/domain/ports/input_cache.rs index 438488b..aa18a69 100644 --- a/src/domain/ports/input_cache.rs +++ b/src/domain/ports/input_cache.rs @@ -16,7 +16,7 @@ pub enum InputCacheError { } pub trait InputCache { - fn save(input: &str, year: i32, day: i32) -> Result<(), InputCacheError>; - fn load(year: i32, day: i32) -> Result; + fn save(input: &str, year: usize, day: usize) -> Result<(), InputCacheError>; + fn load(year: usize, day: usize) -> Result; fn clear() -> Result<(), InputCacheError>; } diff --git a/src/domain/riddle_date.rs b/src/domain/riddle_date.rs index fb14984..f0acf2a 100644 --- a/src/domain/riddle_date.rs +++ b/src/domain/riddle_date.rs @@ -6,18 +6,18 @@ pub enum RiddleDateError { #[derive(Debug, PartialEq, Eq)] pub struct RiddleDate { - pub year: i32, - pub day: i32, + pub year: usize, + pub day: usize, } impl RiddleDate { - pub fn new(year: i32, day: i32) -> Self { + pub fn new(year: usize, day: usize) -> Self { RiddleDate { year, day } } pub fn best_guess( - year: Option, - day: Option, + year: Option, + day: Option, current_date: Date, ) -> Result { match (year, day) { @@ -32,20 +32,23 @@ impl RiddleDate { current_date: Date, ) -> Result { if current_date.month() == 12 && current_date.day() <= 25 { - Ok(Self::new(current_date.year(), current_date.day() as i32)) + Ok(Self::new( + current_date.year() as usize, + current_date.day() as usize, + )) } else { Err(RiddleDateError::GuessError) } } fn guess_from_day( - day: i32, + day: usize, current_date: Date, ) -> Result { if current_date.month() == 12 { - Ok(Self::new(current_date.year(), day)) + Ok(Self::new(current_date.year() as usize, day)) } else { - Ok(Self::new(current_date.year() - 1, day)) + Ok(Self::new(current_date.year() as usize - 1, day)) } } } diff --git a/src/domain/submission.rs b/src/domain/submission.rs index 47b58cf..77222a5 100644 --- a/src/domain/submission.rs +++ b/src/domain/submission.rs @@ -4,12 +4,12 @@ use super::riddle_part::RiddlePart; pub struct Submission { pub part: RiddlePart, pub answer: String, - pub year: i32, - pub day: i32, + pub year: usize, + pub day: usize, } impl Submission { - pub fn new(part: RiddlePart, answer: String, year: i32, day: i32) -> Self { + pub fn new(part: RiddlePart, answer: String, year: usize, day: usize) -> Self { Submission { part, answer, diff --git a/src/infrastructure/aoc_api.rs b/src/infrastructure/aoc_api.rs index 43795a9..b1bc727 100644 --- a/src/infrastructure/aoc_api.rs +++ b/src/infrastructure/aoc_api.rs @@ -14,3 +14,4 @@ pub mod find_riddle_part_impl; pub mod get_leaderboard_impl; pub mod get_private_leaderboard_impl; pub mod get_stars_impl; +pub mod get_input_impl; diff --git a/src/infrastructure/aoc_api/aoc_client_impl.rs b/src/infrastructure/aoc_api/aoc_client_impl.rs index c91fc33..94208aa 100644 --- a/src/infrastructure/aoc_api/aoc_client_impl.rs +++ b/src/infrastructure/aoc_api/aoc_client_impl.rs @@ -10,45 +10,6 @@ use reqwest::header::{CONTENT_TYPE, ORIGIN}; use std::io::Read; impl AocClient for AocApi { - fn get_input(&self, year: i32, day: i32) -> InputResponse { - let url = match reqwest::Url::parse(&format!("{}/{}/day/{}/input", AOC_URL, year, day)) { - Ok(url) => url, - Err(_) => { - return InputResponse::new( - "Failed to parse the URL. Are you sure your day and year are correct?" - .to_string(), - ResponseStatus::Error, - ) - } - }; - let mut response = match self.http_client.get(url).send() { - Ok(response) => response, - Err(_) => { - return InputResponse::new("Failed to get input".to_string(), ResponseStatus::Error) - } - }; - if response.status() != reqwest::StatusCode::OK { - return InputResponse::new( - "Got a non-200 status code from the server. Is your token up to date?".to_owned(), - ResponseStatus::Error, - ); - } - let mut body = String::new(); - if response.read_to_string(&mut body).is_err() { - return InputResponse::new( - "Failed to read the response body".to_owned(), - ResponseStatus::Error, - ); - } - if body.starts_with("Please don't repeatedly request this") { - return InputResponse::new( - "You have to wait for the input to be available".to_owned(), - ResponseStatus::TooSoon, - ); - } - InputResponse::new(body, ResponseStatus::Ok) - } - fn submit_answer(&self, submission: Submission) -> Result { let url = reqwest::Url::parse(&format!( "{}/{}/day/{}/answer", @@ -118,8 +79,8 @@ impl AocClient for AocApi { /// for a given day and year and returns it as a formatted string. fn get_description>( &self, - year: i32, - day: i32, + year: usize, + day: usize, ) -> Result { let url = reqwest::Url::parse(&format!("{}/{}/day/{}", AOC_URL, year, day))?; self.http_client @@ -130,25 +91,6 @@ impl AocClient for AocApi { } } -#[derive(Debug, PartialEq, Eq)] -pub enum ResponseStatus { - Ok, - TooSoon, - Error, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct InputResponse { - pub body: String, - pub status: ResponseStatus, -} - -impl InputResponse { - pub fn new(body: String, status: ResponseStatus) -> Self { - Self { body, status } - } -} - #[cfg(test)] mod tests { use crate::Configuration; diff --git a/src/infrastructure/aoc_api/find_riddle_part_impl.rs b/src/infrastructure/aoc_api/find_riddle_part_impl.rs index 19d4a23..a44d435 100644 --- a/src/infrastructure/aoc_api/find_riddle_part_impl.rs +++ b/src/infrastructure/aoc_api/find_riddle_part_impl.rs @@ -4,7 +4,7 @@ use crate::infrastructure::{find_riddle_part::FindRiddlePart, http_description:: use super::AocApi; impl FindRiddlePart for AocApi { - fn find_unsolved_part(&self, year: i32, day: i32) -> Result { + fn find_unsolved_part(&self, year: usize, day: usize) -> Result { let description = Self::get_description::(&self, year, day)?; match (description.part_one_answer(), description.part_two_answer()) { (None, _) => Ok(RiddlePart::One), diff --git a/src/infrastructure/aoc_api/get_input_impl.rs b/src/infrastructure/aoc_api/get_input_impl.rs new file mode 100644 index 0000000..ad248d0 --- /dev/null +++ b/src/infrastructure/aoc_api/get_input_impl.rs @@ -0,0 +1,37 @@ +use std::io::Read; + +use super::{AocApi, AOC_URL}; +use crate::domain::ports::get_input::GetInput; +use anyhow::{Context, Result}; + +impl GetInput for AocApi { + fn get_input(&self, day: usize, year: usize) -> Result { + let url = reqwest::Url::parse(&format!("{}/{}/day/{}/input", AOC_URL, year, day)).context( + format!( + "Failed to parse the url: {}/{}/day/{}/input", + AOC_URL, year, day + ), + )?; + self.http_client + .get(url) + .send() + .context( + "Failed to send the request to the AOC server. Is your internet connection OK?", + ) + .and_then(|response| { + response.error_for_status().context(format!( + "Got a non-200 response from a server. Is your token up to date?", + )) + }) + .and_then(|mut ok_response| { + let mut body = String::new(); + ok_response + .read_to_string(&mut body) + .context("Failed to read the response body")?; + if body.starts_with("Please don't repeatedly request this") { + anyhow::bail!("You have to wait for the input to be available"); + } + Ok(body) + }) + } +} diff --git a/src/infrastructure/driver.rs b/src/infrastructure/driver.rs index f3612f7..ab79edc 100644 --- a/src/infrastructure/driver.rs +++ b/src/infrastructure/driver.rs @@ -4,18 +4,15 @@ use anyhow::{Context, Result}; use chrono::TimeZone; use super::{ - aoc_api::{aoc_client_impl::ResponseStatus, AocApi}, - cli_display::CliDisplay, - configuration::Configuration, - find_riddle_part::FindRiddlePart, - http_description::HttpDescription, - input_cache::FileInputCache, - submission_history::SubmissionHistory, + aoc_api::AocApi, cli_display::CliDisplay, configuration::Configuration, + find_riddle_part::FindRiddlePart, http_description::HttpDescription, + input_cache::FileInputCache, submission_history::SubmissionHistory, }; use crate::domain::{ duration_string::DurationString, ports::{ aoc_client::AocClient, + get_input::GetInput, get_leaderboard::GetLeaderboard, get_private_leaderboard::GetPrivateLeaderboard, get_stars::GetStars, @@ -38,7 +35,7 @@ impl Driver { Self { configuration } } - pub fn input(&self, year: i32, day: i32) -> Result { + pub fn input(&self, year: usize, day: usize) -> Result { let is_already_released = self.is_input_released_yet(year, day, &chrono::Utc::now())?; if !is_already_released { anyhow::bail!("The input is not released yet"); @@ -47,43 +44,32 @@ impl Driver { match FileInputCache::load(year, day) { Ok(input) => return Ok(input), Err(e) => match e { - InputCacheError::Empty(_) => { - eprintln!("Attempting to download it from the server..."); - } InputCacheError::Load(_) => { - eprintln!("Cache corrupted. Clear the cache and try again."); + eprintln!("Cache corrupted. Clearing the cache..."); + let _ = self.clear_cache().context("Failed to clear the cache")?; } _ => { - eprintln!("Failed to load the input from the cache: {}", e); + eprintln!("Downloading the input from the server..."); } }, }; let http_client = AocApi::prepare_http_client(&self.configuration); let aoc_api = AocApi::new(http_client, self.configuration.clone()); - let input = aoc_api.get_input(year, day); - if input.status == ResponseStatus::Ok { - if FileInputCache::save(&input.body, year, day).is_err() { - eprintln!("Failed saving the input to the cache"); - } - } else { - anyhow::bail!("{}", input.body); - } - Ok(input.body) + aoc_api.get_input(day, year) } pub fn submit_answer( &self, - year: i32, - day: i32, + year: usize, + day: usize, part: RiddlePart, answer: String, ) -> Result<()> { let http_client = AocApi::prepare_http_client(&self.configuration); let aoc_api = AocApi::new(http_client, self.configuration.clone()); - let mut cache: Option = match SubmissionHistory::from_cache(&year, &day) - { + let mut cache: Option = match SubmissionHistory::from_cache(year, day) { Ok(c) => Some(c), Err(e) => { eprintln!("Error: {}", e); @@ -141,13 +127,6 @@ impl Driver { } /// Clears the cache of the application - /// - /// # Example - /// ``` - /// use elv::Driver; - /// let driver = Driver::default(); - /// driver.clear_cache(); - /// ``` pub fn clear_cache(&self) -> Result<()> { FileInputCache::clear()?; SubmissionHistory::clear()?; @@ -155,7 +134,7 @@ impl Driver { } /// Returns the description of the riddles - pub fn get_description(&self, year: i32, day: i32) -> Result { + pub fn get_description(&self, year: usize, day: usize) -> Result { let http_client = AocApi::prepare_http_client(&self.configuration); let aoc_api = AocApi::new(http_client, self.configuration.clone()); Ok(aoc_api @@ -171,12 +150,6 @@ impl Driver { } /// Lists the directories used by the application - /// # Example - /// ``` - /// use elv::Driver; - /// let driver = Driver::default(); - /// driver.list_app_directories(); - /// ``` pub fn list_app_directories(&self) -> Result> { let mut directories = HashMap::new(); if let Some(config_dir) = Configuration::get_project_directories() @@ -225,7 +198,7 @@ impl Driver { Ok(()) } - pub(crate) fn guess_riddle_part(&self, year: i32, day: i32) -> Result { + pub(crate) fn guess_riddle_part(&self, year: usize, day: usize) -> Result { let http_client = AocApi::prepare_http_client(&self.configuration); let aoc_client = AocApi::new(http_client, self.configuration.clone()); @@ -234,8 +207,8 @@ impl Driver { fn is_input_released_yet( &self, - year: i32, - day: i32, + year: usize, + day: usize, now: &chrono::DateTime, ) -> Result { let input_release_time = match chrono::FixedOffset::west_opt(60 * 60 * 5) diff --git a/src/infrastructure/find_riddle_part.rs b/src/infrastructure/find_riddle_part.rs index bb1eed6..108c3ea 100644 --- a/src/infrastructure/find_riddle_part.rs +++ b/src/infrastructure/find_riddle_part.rs @@ -1,5 +1,5 @@ use crate::domain::riddle_part::RiddlePart; pub trait FindRiddlePart { - fn find_unsolved_part(&self, year: i32, day: i32) -> Result; + fn find_unsolved_part(&self, year: usize, day: usize) -> Result; } diff --git a/src/infrastructure/input_cache.rs b/src/infrastructure/input_cache.rs index 311c507..4e7253e 100644 --- a/src/infrastructure/input_cache.rs +++ b/src/infrastructure/input_cache.rs @@ -4,7 +4,7 @@ use crate::domain::ports::input_cache::{InputCache, InputCacheError}; pub struct FileInputCache; impl FileInputCache { - fn cache_path(year: i32, day: i32) -> std::path::PathBuf { + fn cache_path(year: usize, day: usize) -> std::path::PathBuf { Configuration::get_project_directories() .cache_dir() .join("inputs") @@ -19,7 +19,7 @@ impl From for InputCacheError { } impl InputCache for FileInputCache { - fn save(input: &str, year: i32, day: i32) -> Result<(), InputCacheError> { + fn save(input: &str, year: usize, day: usize) -> Result<(), InputCacheError> { let cache_path = Self::cache_path(year, day); if !cache_path.exists() { std::fs::create_dir_all(cache_path.parent().unwrap())?; @@ -28,7 +28,7 @@ impl InputCache for FileInputCache { Ok(()) } - fn load(year: i32, day: i32) -> Result { + fn load(year: usize, day: usize) -> Result { let cache_path = Self::cache_path(year, day); if !cache_path.exists() { return Err(InputCacheError::Empty(format!( diff --git a/src/infrastructure/submission_history.rs b/src/infrastructure/submission_history.rs index f686740..575aa04 100644 --- a/src/infrastructure/submission_history.rs +++ b/src/infrastructure/submission_history.rs @@ -20,12 +20,12 @@ pub enum SubmissionHistoryError { #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct SubmissionHistory { submissions: Vec, - year: i32, - day: i32, + year: usize, + day: usize, } impl SubmissionHistory { - pub fn new(year: i32, day: i32) -> Self { + pub fn new(year: usize, day: usize) -> Self { SubmissionHistory { submissions: Vec::new(), year, @@ -39,10 +39,10 @@ impl SubmissionHistory { .find(|s| s.submission.part == *part && s.status == SubmissionStatus::Correct) } - pub fn from_cache(year: &i32, day: &i32) -> Result { + pub fn from_cache(year: usize, day: usize) -> Result { let cache_path = Self::cache_path(year, day); if !cache_path.exists() { - Self::new(*year, *day).save_to_cache()?; + Self::new(year, day).save_to_cache()?; } let content = std::fs::read(&cache_path).map_err(|_| { SubmissionHistoryError::Load(format!( @@ -88,7 +88,7 @@ impl SubmissionHistory { } pub fn save_to_cache(&self) -> Result<(), SubmissionHistoryError> { - let cache_path = Self::cache_path(&self.year, &self.day); + let cache_path = Self::cache_path(self.year, self.day); let cache_dir = cache_path.parent().unwrap(); if !cache_path.exists() { std::fs::create_dir_all(cache_dir).map_err(|_| { @@ -129,7 +129,7 @@ impl SubmissionHistory { Ok(()) } - fn cache_path(year: &i32, day: &i32) -> std::path::PathBuf { + fn cache_path(year: usize, day: usize) -> std::path::PathBuf { Configuration::get_project_directories() .cache_dir() .join("submissions") diff --git a/src/lib.rs b/src/lib.rs index 932dbc2..d8d85d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,10 @@ +mod api; mod application; mod domain; mod infrastructure; +pub use crate::api::get_input; +pub use crate::api::submit; pub use crate::application::cli::ElvCli; -pub use crate::infrastructure::configuration::Configuration; -pub use crate::infrastructure::driver::Driver; +use crate::infrastructure::configuration::Configuration; +use crate::infrastructure::driver::Driver;