Skip to content

Commit 6c8eeb1

Browse files
authored
feat: added getting the leaderboard (#31)
* Added a new command to grab the leaderboard of a particular year * Refactored a bit of AocApi Closes #31
1 parent 2051b9d commit 6c8eeb1

21 files changed

+700
-117
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
[package]
33
name = "elv"
44
description = "A little CLI helper for Advent of Code. 🎄"
5-
version = "0.12.2"
5+
version = "0.12.3"
66
authors = ["Konrad Pagacz <[email protected]>"]
77
edition = "2021"
88
readme = "README.md"

README.md

+41
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,47 @@ elv -t <YOUR SESSION TOKEN> -y 2021 -d 1 input
215215
# downloads the input for the riddle published on the 1st of December 2021
216216
```
217217

218+
### Submitting the solution
219+
220+
#### Submitting the solution for today's riddle
221+
222+
This works only while the event is being held, not all the time of the
223+
year. While the event is not held, you need to specify the year and day
224+
of the challenge explicitly using `-y' and`-d' parameters.
225+
226+
```console
227+
elv -t <YOUR SESSION TOKEN> submit one <SOLUTION>
228+
elv -t <YOUR SESSION TOKEN> submit two <SOLUTION>
229+
```
230+
231+
#### Submitting the solution for a particular riddle
232+
233+
You specify the day and the year of the riddle.
234+
235+
```console
236+
elv -t <YOUR SESSION TOKEN> -y 2021 -d 1 submit one <SOLUTION>
237+
```
238+
239+
### Getting the leaderboard
240+
241+
#### Getting the leaderboard for this year
242+
243+
This works only while the event is being held, not all the time of the
244+
year. While the event is not held, you need to specify the year
245+
explicitly using `-y' parameter.
246+
247+
```console
248+
elv -t <YOUR SESSION TOKEN> leaderboard
249+
```
250+
251+
#### Getting the leaderboard for a particular year
252+
253+
You specify the year of the leaderboard.
254+
255+
```console
256+
elv -t <YOUR SESSION TOKEN> -y 2021 -d 1 leaderboard
257+
```
258+
218259
## FAQ
219260

220261
### How can I store the session token?

src/application/cli/cli_command.rs

+6
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ pub enum CliCommand {
5959
answer: String,
6060
},
6161

62+
/// Show the leaderboard
63+
///
64+
/// This command downloads the leaderboard rankings for a particular year.
65+
#[command(visible_aliases = ["l"])]
66+
Leaderboard,
67+
6268
/// 🗑️ Clears the cache
6369
///
6470
/// This command will clear the cache of the application. The cache is used

src/domain.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
mod description;
22
mod duration_string;
33
pub mod errors;
4+
mod leaderboard;
5+
pub mod ports;
46
mod riddle_part;
57
mod submission;
68
mod submission_result;
79
mod submission_status;
810

911
pub use crate::domain::description::Description;
1012
pub use crate::domain::duration_string::DurationString;
13+
pub use crate::domain::leaderboard::{Leaderboard, LeaderboardEntry};
1114
pub use crate::domain::riddle_part::RiddlePart;
1215
pub use crate::domain::submission::Submission;
1316
pub use crate::domain::submission_result::SubmissionResult;

src/domain/leaderboard.rs

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use error_chain::bail;
2+
3+
use crate::domain::errors::*;
4+
5+
#[derive(PartialEq, Debug)]
6+
pub struct LeaderboardEntry {
7+
pub position: i32,
8+
pub points: i32,
9+
pub username: String,
10+
}
11+
12+
impl TryFrom<&str> for LeaderboardEntry {
13+
type Error = Error;
14+
15+
fn try_from(value: &str) -> Result<Self> {
16+
let values: Vec<&str> = value.split_whitespace().collect();
17+
let (entry_position, entry_points, entry_username);
18+
if let Some(&position) = values.get(0) {
19+
entry_position = position;
20+
} else {
21+
bail!("No leaderboard position");
22+
}
23+
if let Some(&points) = values.get(1) {
24+
entry_points = points;
25+
} else {
26+
bail!("No points in a leaderboard entry");
27+
}
28+
entry_username = values
29+
.iter()
30+
.skip(2)
31+
.map(|x| x.to_string())
32+
.collect::<Vec<_>>()
33+
.join(" ");
34+
35+
Ok(Self {
36+
position: entry_position.replace(r")", "").parse().chain_err(|| {
37+
format!("Error parsing a leaderboard position: {}", entry_position)
38+
})?,
39+
points: entry_points
40+
.parse()
41+
.chain_err(|| format!("Error parsing points: {}", entry_points))?,
42+
username: entry_username,
43+
})
44+
}
45+
}
46+
47+
#[derive(PartialEq, Debug)]
48+
pub struct Leaderboard {
49+
pub entries: Vec<LeaderboardEntry>,
50+
}
51+
52+
impl FromIterator<LeaderboardEntry> for Leaderboard {
53+
fn from_iter<T: IntoIterator<Item = LeaderboardEntry>>(iter: T) -> Self {
54+
Self {
55+
entries: iter.into_iter().collect(),
56+
}
57+
}
58+
}
59+
60+
impl TryFrom<Vec<String>> for Leaderboard {
61+
type Error = Error;
62+
63+
fn try_from(value: Vec<String>) -> Result<Self> {
64+
let entries: Result<Vec<LeaderboardEntry>> = value
65+
.iter()
66+
.map(|entry| LeaderboardEntry::try_from(entry.as_ref()))
67+
.collect();
68+
match entries {
69+
Ok(entries) => Ok(Leaderboard::from_iter(entries)),
70+
Err(e) => bail!(e.chain_err(|| "One of the entries failed parsing")),
71+
}
72+
}
73+
}
74+
75+
#[cfg(test)]
76+
mod tests {
77+
use super::*;
78+
#[test]
79+
fn try_from_for_leaderboard_entry() {
80+
let entry = "1) 3693 betaveros";
81+
let expected_entry = {
82+
let username = "betaveros".to_owned();
83+
LeaderboardEntry {
84+
position: 1,
85+
points: 3693,
86+
username,
87+
}
88+
};
89+
let result_entry = LeaderboardEntry::try_from(entry);
90+
match result_entry {
91+
Ok(result) => assert_eq!(expected_entry, result),
92+
Err(e) => panic!("error parsing the entry: {}", e.description()),
93+
}
94+
}
95+
96+
#[test]
97+
fn try_from_for_leaderboard_entry_anonymous_user() {
98+
let entry = "3) 3042 (anonymous user #1510407)";
99+
let expected_entry = {
100+
let username = "(anonymous user #1510407)".to_owned();
101+
LeaderboardEntry {
102+
position: 3,
103+
points: 3042,
104+
username,
105+
}
106+
};
107+
let result_entry = LeaderboardEntry::try_from(entry);
108+
match result_entry {
109+
Ok(result) => assert_eq!(expected_entry, result),
110+
Err(e) => panic!("error parsing the entry: {}", e.description()),
111+
}
112+
}
113+
114+
#[test]
115+
fn try_from_string_vec_for_leaderboard() {
116+
let entries: Vec<String> = vec!["1) 3693 betaveros", "2) 14 me"]
117+
.iter()
118+
.map(|&x| x.to_owned())
119+
.collect();
120+
let expected_leaderboard = Leaderboard {
121+
entries: vec![
122+
LeaderboardEntry {
123+
position: 1,
124+
points: 3693,
125+
username: "betaveros".to_owned(),
126+
},
127+
LeaderboardEntry {
128+
position: 2,
129+
points: 14,
130+
username: "me".to_owned(),
131+
},
132+
],
133+
};
134+
match Leaderboard::try_from(entries) {
135+
Ok(result) => assert_eq!(expected_leaderboard, result),
136+
Err(e) => panic!("Test case failed {}", e.description()),
137+
}
138+
}
139+
}

src/domain/ports.rs

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
mod aoc_client;
2+
mod get_leaderboard;
3+
mod input_cache;
4+
5+
pub use aoc_client::AocClient;
6+
pub use get_leaderboard::GetLeaderboard;
7+
pub use input_cache::InputCache;

src/domain/ports/aoc_client.rs

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use crate::domain::errors::*;
2+
use crate::domain::Description;
3+
use crate::domain::{Submission, SubmissionResult};
4+
use crate::infrastructure::aoc_api::aoc_client_impl::InputResponse;
5+
6+
pub trait AocClient {
7+
fn submit_answer(&self, submission: Submission) -> Result<SubmissionResult>;
8+
fn get_description<Desc>(&self, year: &u16, day: &u8) -> Result<Desc>
9+
where
10+
Desc: Description + TryFrom<reqwest::blocking::Response>;
11+
fn get_input(&self, year: &u16, day: &u8) -> InputResponse;
12+
}

src/domain/ports/get_leaderboard.rs

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
use crate::domain::errors::*;
2+
use crate::domain::Leaderboard;
3+
4+
pub trait GetLeaderboard {
5+
fn get_leaderboard(&self, year: u16) -> Result<Leaderboard>;
6+
}

src/domain/ports/input_cache.rs

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use crate::domain::errors::*;
2+
3+
pub trait InputCache {
4+
fn save(input: &str, year: u16, day: u8) -> Result<()>;
5+
fn load(year: u16, day: u8) -> Result<String>;
6+
fn clear() -> Result<()>;
7+
}

src/driver.rs

+23-10
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ use std::collections::HashMap;
33
use chrono::TimeZone;
44
use error_chain::bail;
55

6-
use crate::aoc_api::{AocApi, ResponseStatus};
6+
use crate::domain::ports::{AocClient, GetLeaderboard, InputCache};
77
use crate::domain::{errors::*, DurationString, Submission, SubmissionStatus};
8-
use crate::infrastructure::{CliDisplay, Configuration};
9-
use crate::input_cache::InputCache;
8+
use crate::infrastructure::aoc_api::aoc_client_impl::ResponseStatus;
9+
use crate::infrastructure::aoc_api::AocApi;
10+
use crate::infrastructure::{CliDisplay, FileInputCache};
11+
use crate::infrastructure::{Configuration, HttpDescription};
1012
use crate::submission_history::SubmissionHistory;
1113

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

34-
match InputCache::load(year, day) {
36+
match FileInputCache::load(year, day) {
3537
Ok(input) => return Ok(input),
3638
Err(e) => match e {
3739
Error(ErrorKind::NoCacheFound(message), _) => {
@@ -46,10 +48,11 @@ impl Driver {
4648
},
4749
};
4850

49-
let aoc_api = AocApi::new(&self.configuration);
51+
let http_client = AocApi::prepare_http_client(&self.configuration);
52+
let aoc_api = AocApi::new(http_client, self.configuration.clone());
5053
let input = aoc_api.get_input(&year, &day);
5154
if input.status == ResponseStatus::Ok {
52-
if InputCache::cache(&input.body, year, day).is_err() {
55+
if FileInputCache::save(&input.body, year, day).is_err() {
5356
eprintln!("Failed saving the input to the cache");
5457
}
5558
} else {
@@ -65,7 +68,8 @@ impl Driver {
6568
part: crate::domain::RiddlePart,
6669
answer: String,
6770
) -> Result<()> {
68-
let aoc_api = AocApi::new(&self.configuration);
71+
let http_client = AocApi::prepare_http_client(&self.configuration);
72+
let aoc_api = AocApi::new(http_client, self.configuration.clone());
6973

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

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

135139
/// Returns the description of the riddles
136140
pub fn get_description(&self, year: u16, day: u8) -> Result<String> {
137-
let aoc_api = AocApi::new(&self.configuration);
141+
let http_client = AocApi::prepare_http_client(&self.configuration);
142+
let aoc_api = AocApi::new(http_client, self.configuration.clone());
138143
Ok(aoc_api
139-
.get_description(&year, &day)?
144+
.get_description::<HttpDescription>(&year, &day)?
140145
.cli_fmt(&self.configuration))
141146
}
142147

@@ -180,6 +185,14 @@ impl Driver {
180185
}
181186
Ok(directories)
182187
}
188+
189+
pub fn get_leaderboard(&self, year: u16) -> Result<String> {
190+
let http_client = AocApi::prepare_http_client(&self.configuration);
191+
let aoc_client = AocApi::new(http_client, self.configuration.clone());
192+
let leaderboard = aoc_client.get_leaderboard(year)?;
193+
194+
Ok(leaderboard.cli_fmt(&self.configuration))
195+
}
183196
}
184197

185198
#[cfg(test)]

src/infrastructure.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
pub mod aoc_api;
12
mod cli_display;
23
mod configuration;
34
mod http_description;
5+
mod input_cache;
46

57
pub use crate::infrastructure::cli_display::CliDisplay;
68
pub use crate::infrastructure::configuration::Configuration;
79
pub use crate::infrastructure::http_description::HttpDescription;
10+
pub use crate::infrastructure::input_cache::FileInputCache;

src/infrastructure/aoc_api.rs

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use crate::Configuration;
2+
3+
const AOC_URL: &str = "https://adventofcode.com";
4+
5+
#[derive(Debug)]
6+
pub struct AocApi {
7+
http_client: reqwest::blocking::Client,
8+
configuration: Configuration,
9+
}
10+
11+
mod aoc_api_impl;
12+
pub mod aoc_client_impl;
13+
pub mod get_leaderboard_impl;

0 commit comments

Comments
 (0)