Skip to content

Commit

Permalink
feat: added functions that can be used in Rust code without the execu…
Browse files Browse the repository at this point in the history
…table (#57)

* Added a function that downloads input and a function that submits answers.

Closes #12
  • Loading branch information
kpagacz authored Oct 26, 2023
1 parent 566578a commit 2250e7e
Show file tree
Hide file tree
Showing 20 changed files with 220 additions and 146 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>"]
edition = "2021"
readme = "README.md"
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 `[email protected]` 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`
Expand Down
88 changes: 88 additions & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
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(())
}
2 changes: 1 addition & 1 deletion src/application/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
12 changes: 6 additions & 6 deletions src/application/cli/cli_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i32>,
#[arg(short, long, value_parser = clap::value_parser!(usize))]
pub year: Option<usize>,

/// 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<i32>,
#[arg(short, long, value_parser = clap::value_parser!(usize))]
pub day: Option<usize>,
}

#[derive(Debug, Args)]
Expand Down Expand Up @@ -200,6 +200,6 @@ pub enum CliCommand {
#[command(verbatim_doc_comment, visible_aliases = ["t", "sett", "set-token"])]
Token {
/// Token to be saved
token: Option<String>
}
token: Option<String>,
},
}
1 change: 1 addition & 0 deletions src/domain/ports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
10 changes: 3 additions & 7 deletions src/domain/ports/aoc_client.rs
Original file line number Diff line number Diff line change
@@ -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<SubmissionResult, AocClientError>;
fn get_description<Desc>(&self, year: i32, day: i32) -> Result<Desc, AocClientError>
fn get_description<Desc>(&self, year: usize, day: usize) -> Result<Desc, AocClientError>
where
Desc: Description + TryFrom<reqwest::blocking::Response>;
fn get_input(&self, year: i32, day: i32) -> InputResponse;
}
5 changes: 5 additions & 0 deletions src/domain/ports/get_input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use anyhow::Result;

pub trait GetInput {
fn get_input(&self, day: usize, year: usize) -> Result<String>;
}
4 changes: 2 additions & 2 deletions src/domain/ports/input_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, InputCacheError>;
fn save(input: &str, year: usize, day: usize) -> Result<(), InputCacheError>;
fn load(year: usize, day: usize) -> Result<String, InputCacheError>;
fn clear() -> Result<(), InputCacheError>;
}
21 changes: 12 additions & 9 deletions src/domain/riddle_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Date: chrono::Datelike>(
year: Option<i32>,
day: Option<i32>,
year: Option<usize>,
day: Option<usize>,
current_date: Date,
) -> Result<Self, RiddleDateError> {
match (year, day) {
Expand All @@ -32,20 +32,23 @@ impl RiddleDate {
current_date: Date,
) -> Result<Self, RiddleDateError> {
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<Date: chrono::Datelike>(
day: i32,
day: usize,
current_date: Date,
) -> Result<Self, RiddleDateError> {
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))
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/domain/submission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/infrastructure/aoc_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
62 changes: 2 additions & 60 deletions src/infrastructure/aoc_api/aoc_client_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SubmissionResult, AocClientError> {
let url = reqwest::Url::parse(&format!(
"{}/{}/day/{}/answer",
Expand Down Expand Up @@ -118,8 +79,8 @@ impl AocClient for AocApi {
/// for a given day and year and returns it as a formatted string.
fn get_description<HttpDescription: std::convert::TryFrom<reqwest::blocking::Response>>(
&self,
year: i32,
day: i32,
year: usize,
day: usize,
) -> Result<HttpDescription, AocClientError> {
let url = reqwest::Url::parse(&format!("{}/{}/day/{}", AOC_URL, year, day))?;
self.http_client
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/infrastructure/aoc_api/find_riddle_part_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RiddlePart, anyhow::Error> {
fn find_unsolved_part(&self, year: usize, day: usize) -> Result<RiddlePart, anyhow::Error> {
let description = Self::get_description::<HttpDescription>(&self, year, day)?;
match (description.part_one_answer(), description.part_two_answer()) {
(None, _) => Ok(RiddlePart::One),
Expand Down
Loading

0 comments on commit 2250e7e

Please sign in to comment.