Skip to content

Commit

Permalink
feat: added getting the leaderboard (#31)
Browse files Browse the repository at this point in the history
* Added a new command to grab the leaderboard of a particular year
* Refactored a bit of AocApi

Closes #31
  • Loading branch information
kpagacz authored Jul 17, 2023
1 parent 2051b9d commit 6c8eeb1
Show file tree
Hide file tree
Showing 21 changed files with 700 additions and 117 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.12.2"
version = "0.12.3"
authors = ["Konrad Pagacz <[email protected]>"]
edition = "2021"
readme = "README.md"
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,47 @@ elv -t <YOUR SESSION TOKEN> -y 2021 -d 1 input
# downloads the input for the riddle published on the 1st of December 2021
```

### Submitting the solution

#### Submitting the solution for today's riddle

This works only while the event is being held, not all the time of the
year. While the event is not held, you need to specify the year and day
of the challenge explicitly using `-y' and`-d' parameters.

```console
elv -t <YOUR SESSION TOKEN> submit one <SOLUTION>
elv -t <YOUR SESSION TOKEN> submit two <SOLUTION>
```

#### Submitting the solution for a particular riddle

You specify the day and the year of the riddle.

```console
elv -t <YOUR SESSION TOKEN> -y 2021 -d 1 submit one <SOLUTION>
```

### Getting the leaderboard

#### Getting the leaderboard for this year

This works only while the event is being held, not all the time of the
year. While the event is not held, you need to specify the year
explicitly using `-y' parameter.

```console
elv -t <YOUR SESSION TOKEN> leaderboard
```

#### Getting the leaderboard for a particular year

You specify the year of the leaderboard.

```console
elv -t <YOUR SESSION TOKEN> -y 2021 -d 1 leaderboard
```

## FAQ

### How can I store the session token?
Expand Down
6 changes: 6 additions & 0 deletions src/application/cli/cli_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ pub enum CliCommand {
answer: String,
},

/// Show the leaderboard
///
/// This command downloads the leaderboard rankings for a particular year.
#[command(visible_aliases = ["l"])]
Leaderboard,

/// 🗑️ Clears the cache
///
/// This command will clear the cache of the application. The cache is used
Expand Down
3 changes: 3 additions & 0 deletions src/domain.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
mod description;
mod duration_string;
pub mod errors;
mod leaderboard;
pub mod ports;
mod riddle_part;
mod submission;
mod submission_result;
mod submission_status;

pub use crate::domain::description::Description;
pub use crate::domain::duration_string::DurationString;
pub use crate::domain::leaderboard::{Leaderboard, LeaderboardEntry};
pub use crate::domain::riddle_part::RiddlePart;
pub use crate::domain::submission::Submission;
pub use crate::domain::submission_result::SubmissionResult;
Expand Down
139 changes: 139 additions & 0 deletions src/domain/leaderboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use error_chain::bail;

use crate::domain::errors::*;

#[derive(PartialEq, Debug)]
pub struct LeaderboardEntry {
pub position: i32,
pub points: i32,
pub username: String,
}

impl TryFrom<&str> for LeaderboardEntry {
type Error = Error;

fn try_from(value: &str) -> Result<Self> {
let values: Vec<&str> = value.split_whitespace().collect();
let (entry_position, entry_points, entry_username);
if let Some(&position) = values.get(0) {
entry_position = position;
} else {
bail!("No leaderboard position");
}
if let Some(&points) = values.get(1) {
entry_points = points;
} else {
bail!("No points in a leaderboard entry");
}
entry_username = values
.iter()
.skip(2)
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(" ");

Ok(Self {
position: entry_position.replace(r")", "").parse().chain_err(|| {
format!("Error parsing a leaderboard position: {}", entry_position)
})?,
points: entry_points
.parse()
.chain_err(|| format!("Error parsing points: {}", entry_points))?,
username: entry_username,
})
}
}

#[derive(PartialEq, Debug)]
pub struct Leaderboard {
pub entries: Vec<LeaderboardEntry>,
}

impl FromIterator<LeaderboardEntry> for Leaderboard {
fn from_iter<T: IntoIterator<Item = LeaderboardEntry>>(iter: T) -> Self {
Self {
entries: iter.into_iter().collect(),
}
}
}

impl TryFrom<Vec<String>> for Leaderboard {
type Error = Error;

fn try_from(value: Vec<String>) -> Result<Self> {
let entries: Result<Vec<LeaderboardEntry>> = value
.iter()
.map(|entry| LeaderboardEntry::try_from(entry.as_ref()))
.collect();
match entries {
Ok(entries) => Ok(Leaderboard::from_iter(entries)),
Err(e) => bail!(e.chain_err(|| "One of the entries failed parsing")),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_from_for_leaderboard_entry() {
let entry = "1) 3693 betaveros";
let expected_entry = {
let username = "betaveros".to_owned();
LeaderboardEntry {
position: 1,
points: 3693,
username,
}
};
let result_entry = LeaderboardEntry::try_from(entry);
match result_entry {
Ok(result) => assert_eq!(expected_entry, result),
Err(e) => panic!("error parsing the entry: {}", e.description()),
}
}

#[test]
fn try_from_for_leaderboard_entry_anonymous_user() {
let entry = "3) 3042 (anonymous user #1510407)";
let expected_entry = {
let username = "(anonymous user #1510407)".to_owned();
LeaderboardEntry {
position: 3,
points: 3042,
username,
}
};
let result_entry = LeaderboardEntry::try_from(entry);
match result_entry {
Ok(result) => assert_eq!(expected_entry, result),
Err(e) => panic!("error parsing the entry: {}", e.description()),
}
}

#[test]
fn try_from_string_vec_for_leaderboard() {
let entries: Vec<String> = vec!["1) 3693 betaveros", "2) 14 me"]
.iter()
.map(|&x| x.to_owned())
.collect();
let expected_leaderboard = Leaderboard {
entries: vec![
LeaderboardEntry {
position: 1,
points: 3693,
username: "betaveros".to_owned(),
},
LeaderboardEntry {
position: 2,
points: 14,
username: "me".to_owned(),
},
],
};
match Leaderboard::try_from(entries) {
Ok(result) => assert_eq!(expected_leaderboard, result),
Err(e) => panic!("Test case failed {}", e.description()),
}
}
}
7 changes: 7 additions & 0 deletions src/domain/ports.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod aoc_client;
mod get_leaderboard;
mod input_cache;

pub use aoc_client::AocClient;
pub use get_leaderboard::GetLeaderboard;
pub use input_cache::InputCache;
12 changes: 12 additions & 0 deletions src/domain/ports/aoc_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use crate::domain::errors::*;
use crate::domain::Description;
use crate::domain::{Submission, SubmissionResult};
use crate::infrastructure::aoc_api::aoc_client_impl::InputResponse;

pub trait AocClient {
fn submit_answer(&self, submission: Submission) -> Result<SubmissionResult>;
fn get_description<Desc>(&self, year: &u16, day: &u8) -> Result<Desc>
where
Desc: Description + TryFrom<reqwest::blocking::Response>;
fn get_input(&self, year: &u16, day: &u8) -> InputResponse;
}
6 changes: 6 additions & 0 deletions src/domain/ports/get_leaderboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use crate::domain::errors::*;
use crate::domain::Leaderboard;

pub trait GetLeaderboard {
fn get_leaderboard(&self, year: u16) -> Result<Leaderboard>;
}
7 changes: 7 additions & 0 deletions src/domain/ports/input_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use crate::domain::errors::*;

pub trait InputCache {
fn save(input: &str, year: u16, day: u8) -> Result<()>;
fn load(year: u16, day: u8) -> Result<String>;
fn clear() -> Result<()>;
}
33 changes: 23 additions & 10 deletions src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ use std::collections::HashMap;
use chrono::TimeZone;
use error_chain::bail;

use crate::aoc_api::{AocApi, ResponseStatus};
use crate::domain::ports::{AocClient, GetLeaderboard, InputCache};
use crate::domain::{errors::*, DurationString, Submission, SubmissionStatus};
use crate::infrastructure::{CliDisplay, Configuration};
use crate::input_cache::InputCache;
use crate::infrastructure::aoc_api::aoc_client_impl::ResponseStatus;
use crate::infrastructure::aoc_api::AocApi;
use crate::infrastructure::{CliDisplay, FileInputCache};
use crate::infrastructure::{Configuration, HttpDescription};
use crate::submission_history::SubmissionHistory;

#[derive(Debug, Default)]
Expand All @@ -31,7 +33,7 @@ impl Driver {
bail!("The input is not released yet");
}

match InputCache::load(year, day) {
match FileInputCache::load(year, day) {
Ok(input) => return Ok(input),
Err(e) => match e {
Error(ErrorKind::NoCacheFound(message), _) => {
Expand All @@ -46,10 +48,11 @@ impl Driver {
},
};

let aoc_api = AocApi::new(&self.configuration);
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 InputCache::cache(&input.body, year, day).is_err() {
if FileInputCache::save(&input.body, year, day).is_err() {
eprintln!("Failed saving the input to the cache");
}
} else {
Expand All @@ -65,7 +68,8 @@ impl Driver {
part: crate::domain::RiddlePart,
answer: String,
) -> Result<()> {
let aoc_api = AocApi::new(&self.configuration);
let http_client = AocApi::prepare_http_client(&self.configuration);
let aoc_api = AocApi::new(http_client, self.configuration.clone());

let mut cache: Option<SubmissionHistory> = match SubmissionHistory::from_cache(&year, &day)
{
Expand Down Expand Up @@ -127,16 +131,17 @@ impl Driver {
}

pub fn clear_cache(&self) -> Result<()> {
InputCache::clear().chain_err(|| "Failed to clear the input cache")?;
FileInputCache::clear().chain_err(|| "Failed to clear the input cache")?;
SubmissionHistory::clear().chain_err(|| "Failed to clear the submission history cache")?;
Ok(())
}

/// Returns the description of the riddles
pub fn get_description(&self, year: u16, day: u8) -> Result<String> {
let aoc_api = AocApi::new(&self.configuration);
let http_client = AocApi::prepare_http_client(&self.configuration);
let aoc_api = AocApi::new(http_client, self.configuration.clone());
Ok(aoc_api
.get_description(&year, &day)?
.get_description::<HttpDescription>(&year, &day)?
.cli_fmt(&self.configuration))
}

Expand Down Expand Up @@ -180,6 +185,14 @@ impl Driver {
}
Ok(directories)
}

pub fn get_leaderboard(&self, year: u16) -> Result<String> {
let http_client = AocApi::prepare_http_client(&self.configuration);
let aoc_client = AocApi::new(http_client, self.configuration.clone());
let leaderboard = aoc_client.get_leaderboard(year)?;

Ok(leaderboard.cli_fmt(&self.configuration))
}
}

#[cfg(test)]
Expand Down
3 changes: 3 additions & 0 deletions src/infrastructure.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
pub mod aoc_api;
mod cli_display;
mod configuration;
mod http_description;
mod input_cache;

pub use crate::infrastructure::cli_display::CliDisplay;
pub use crate::infrastructure::configuration::Configuration;
pub use crate::infrastructure::http_description::HttpDescription;
pub use crate::infrastructure::input_cache::FileInputCache;
13 changes: 13 additions & 0 deletions src/infrastructure/aoc_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use crate::Configuration;

const AOC_URL: &str = "https://adventofcode.com";

#[derive(Debug)]
pub struct AocApi {
http_client: reqwest::blocking::Client,
configuration: Configuration,
}

mod aoc_api_impl;
pub mod aoc_client_impl;
pub mod get_leaderboard_impl;
Loading

0 comments on commit 6c8eeb1

Please sign in to comment.