feat: download puzzle descriptions (#21)

* add support for downloading puzzle descriptions in `cargo download`
* add `cargo read` command to read puzzles in terminal
* extract `aoc_cli` module
* use `aoc-cli`'s new overwrite option to eliminate temp files
This commit is contained in:
Felix Spöttel 2022-12-06 18:22:40 +01:00 committed by GitHub
parent 7ef01ab32f
commit 4d10812fd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 193 additions and 78 deletions

View file

@ -1,6 +1,7 @@
[alias] [alias]
scaffold = "run --bin scaffold -- " scaffold = "run --bin scaffold --quiet --release -- "
download = "run --bin download -- " download = "run --bin download --quiet --release -- "
read = "run --bin read --quiet --release -- "
solve = "run --bin" solve = "run --bin"
all = "run" all = "run"

View file

@ -50,7 +50,7 @@ Every [solution](https://github.com/fspoettel/advent-of-code-rust/blob/main/src/
When editing a solution, `rust-analyzer` will display buttons for running / debugging unit tests above the unit test blocks. When editing a solution, `rust-analyzer` will display buttons for running / debugging unit tests above the unit test blocks.
### Download input for a day ### Download input & description for a day
> **Note** > **Note**
> This command requires [installing the aoc-cli crate](#download-puzzle-inputs-via-aoc-cli). > This command requires [installing the aoc-cli crate](#download-puzzle-inputs-via-aoc-cli).
@ -60,18 +60,20 @@ When editing a solution, `rust-analyzer` will display buttons for running / debu
cargo download <day> cargo download <day>
# output: # output:
# Downloading input with aoc-cli... # Loaded session cookie from "/Users/<snip>/.adventofcode.session".
# Loaded session cookie from "/home/felix/.adventofcode.session". # Fetching puzzle for day 1, 2022...
# Downloading input for day 1, 2021... # Saving puzzle description to "src/puzzles/01.md"...
# Saving puzzle input to "/tmp/tmp.MBdcAdL9Iw/input"... # Downloading input for day 1, 2022...
# Saving puzzle input to "src/inputs/01.txt"...
# Done! # Done!
# --- # ---
# 🎄 Successfully wrote input to "src/inputs/01.txt"! # 🎄 Successfully wrote input to "src/inputs/01.txt".
# 🎄 Successfully wrote puzzle to "src/puzzles/01.md".
``` ```
To download inputs for previous years, append the `--year/-y` flag. _(example: `cargo download 1 --year 2020`)_ To download inputs for previous years, append the `--year/-y` flag. _(example: `cargo download 1 --year 2020`)_
Puzzle inputs are not checked into git. [Reasoning](https://old.reddit.com/r/adventofcode/comments/k99rod/sharing_input_data_were_we_requested_not_to/gf2ukkf/?context=3). Puzzle descriptions are stored in `src/puzzles` as markdown files. Puzzle inputs are not checked into git. [Reasoning](https://old.reddit.com/r/adventofcode/comments/k99rod/sharing_input_data_were_we_requested_not_to/gf2ukkf/?context=3).
### Run solutions for a day ### Run solutions for a day
@ -139,11 +141,28 @@ cargo fmt
cargo clippy cargo clippy
``` ```
### Read puzzle description in terminal
> **Note**
> This command requires [installing the aoc-cli crate](#download-puzzle-inputs-via-aoc-cli).
```sh
# example: `cargo read 1`
cargo read <day>
# output:
# Loaded session cookie from "/Users/<snip>/.adventofcode.session".
# Fetching puzzle for day 1, 2022...
# ...the input...
```
To read inputs for previous years, append the `--year/-y` flag. _(example: `cargo read 1 --year 2020`)_
## Optional template features ## Optional template features
### Download puzzle inputs via aoc-cli ### Download puzzle inputs via aoc-cli
1. Install [`aoc-cli`](https://github.com/scarvalhojr/aoc-cli/) via cargo: `cargo install aoc-cli --version 0.5.0`. 1. Install [`aoc-cli`](https://github.com/scarvalhojr/aoc-cli/) via cargo: `cargo install aoc-cli --version 0.7.0`
2. Create an `.adventofcode.session` file in your home directory and paste your session cookie[^1] into it. To get this, press F12 anywhere on the Advent of Code website to open your browser developer tools. Look in your Cookies under the Application or Storage tab, and copy out the `session` cookie value. 2. Create an `.adventofcode.session` file in your home directory and paste your session cookie[^1] into it. To get this, press F12 anywhere on the Advent of Code website to open your browser developer tools. Look in your Cookies under the Application or Storage tab, and copy out the `session` cookie value.
Once installed, you can use the [download command](#download-input-for-a-day). Once installed, you can use the [download command](#download-input-for-a-day).

View file

@ -2,14 +2,12 @@
* This file contains template code. * This file contains template code.
* There is no need to edit this file unless you want to change template functionality. * There is no need to edit this file unless you want to change template functionality.
*/ */
use std::io::Write; use advent_of_code::aoc_cli;
use std::path::PathBuf; use std::process;
use std::{env::temp_dir, io, process::Command};
use std::{fs, process};
struct Args { struct Args {
day: u8, day: u8,
year: Option<i16>, year: Option<u16>,
} }
fn parse_args() -> Result<Args, pico_args::Error> { fn parse_args() -> Result<Args, pico_args::Error> {
@ -20,86 +18,29 @@ fn parse_args() -> Result<Args, pico_args::Error> {
}) })
} }
fn remove_file(path: &PathBuf) {
#[allow(unused_must_use)]
{
fs::remove_file(path);
}
}
fn exit_with_status(status: i32, path: &PathBuf) -> ! {
remove_file(path);
process::exit(status);
}
fn main() { fn main() {
// acquire a temp file path to write aoc-cli output to.
// aoc-cli expects this file not to be present - delete just in case.
let mut tmp_file_path = temp_dir();
tmp_file_path.push("aoc_input_tmp");
remove_file(&tmp_file_path);
let args = match parse_args() { let args = match parse_args() {
Ok(args) => args, Ok(args) => args,
Err(e) => { Err(e) => {
eprintln!("Failed to process arguments: {}", e); eprintln!("Failed to process arguments: {}", e);
exit_with_status(1, &tmp_file_path); process::exit(1);
} }
}; };
let day_padded = format!("{:02}", args.day); if aoc_cli::check().is_err() {
let input_path = format!("src/inputs/{}.txt", day_padded);
// check if aoc binary exists and is callable.
if Command::new("aoc").arg("-V").output().is_err() {
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it."); eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
exit_with_status(1, &tmp_file_path); process::exit(1);
} }
let mut cmd_args = vec![]; match aoc_cli::download(args.day, args.year) {
if let Some(year) = args.year {
cmd_args.push("--year".into());
cmd_args.push(year.to_string());
}
cmd_args.append(&mut vec![
"--input-file".into(),
tmp_file_path.to_string_lossy().to_string(),
"--day".into(),
args.day.to_string(),
"download".into(),
]);
println!("Downloading input with >aoc {}", cmd_args.join(" "));
match Command::new("aoc").args(cmd_args).output() {
Ok(cmd_output) => { Ok(cmd_output) => {
io::stdout()
.write_all(&cmd_output.stdout)
.expect("could not write cmd stdout to pipe.");
io::stderr()
.write_all(&cmd_output.stderr)
.expect("could not write cmd stderr to pipe.");
if !cmd_output.status.success() { if !cmd_output.status.success() {
exit_with_status(1, &tmp_file_path); process::exit(1);
} }
} }
Err(e) => { Err(e) => {
eprintln!("failed to spawn aoc-cli: {}", e); eprintln!("failed to spawn aoc-cli: {}", e);
exit_with_status(1, &tmp_file_path); process::exit(1);
}
}
match fs::copy(&tmp_file_path, &input_path) {
Ok(_) => {
println!("---");
println!("🎄 Successfully wrote input to \"{}\".", &input_path);
exit_with_status(0, &tmp_file_path);
}
Err(e) => {
eprintln!("could not copy downloaded input to input file: {}", e);
exit_with_status(1, &tmp_file_path);
} }
} }
} }

46
src/bin/read.rs Normal file
View file

@ -0,0 +1,46 @@
/*
* This file contains template code.
* There is no need to edit this file unless you want to change template functionality.
*/
use advent_of_code::aoc_cli;
use std::process;
struct Args {
day: u8,
year: Option<u16>,
}
fn parse_args() -> Result<Args, pico_args::Error> {
let mut args = pico_args::Arguments::from_env();
Ok(Args {
day: args.free_from_str()?,
year: args.opt_value_from_str(["-y", "--year"])?,
})
}
fn main() {
let args = match parse_args() {
Ok(args) => args,
Err(e) => {
eprintln!("Failed to process arguments: {}", e);
process::exit(1);
}
};
if aoc_cli::check().is_err() {
eprintln!("command \"aoc\" not found or not callable. Try running \"cargo install aoc-cli\" to install it.");
process::exit(1);
}
match aoc_cli::read(args.day, args.year) {
Ok(cmd_output) => {
if !cmd_output.status.success() {
process::exit(1);
}
}
Err(e) => {
eprintln!("failed to spawn aoc-cli: {}", e);
process::exit(1);
}
}
}

View file

@ -123,3 +123,111 @@ mod tests {
); );
} }
} }
pub mod aoc_cli {
use std::{
fmt::Display,
fs::create_dir_all,
process::{Command, Output, Stdio},
};
pub enum AocCliError {
CommandNotFound,
CommandNotCallable,
BadExitStatus(Output),
IoError,
}
impl Display for AocCliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AocCliError::CommandNotFound => write!(f, "aoc-cli is not present in environment."),
AocCliError::CommandNotCallable => write!(f, "aoc-cli could not be called."),
AocCliError::BadExitStatus(_) => write!(f, "aoc-cli exited with a non-zero status."),
AocCliError::IoError => write!(f, "could not write output files to file system."),
}
}
}
pub fn check() -> Result<(), AocCliError> {
Command::new("aoc")
.arg("-V")
.output()
.map_err(|_| AocCliError::CommandNotFound)?;
Ok(())
}
pub fn read(day: u8, year: Option<u16>) -> Result<Output, AocCliError> {
// TODO: output local puzzle if present.
let args = build_args("read", &[], day, year);
call_aoc_cli(&args)
}
pub fn download(day: u8, year: Option<u16>) -> Result<Output, AocCliError> {
let input_path = get_input_path(day);
let puzzle_path = get_puzzle_path(day);
create_dir_all("src/puzzles").map_err(|_| AocCliError::IoError)?;
let args = build_args(
"download",
&[
"--overwrite".into(),
"--input-file".into(),
input_path.to_string(),
"--puzzle-file".into(),
puzzle_path.to_string(),
],
day,
year,
);
let output = call_aoc_cli(&args)?;
if output.status.success() {
println!("---");
println!("🎄 Successfully wrote input to \"{}\".", &input_path);
println!("🎄 Successfully wrote puzzle to \"{}\".", &puzzle_path);
Ok(output)
} else {
Err(AocCliError::BadExitStatus(output))
}
}
fn get_input_path(day: u8) -> String {
let day_padded = format!("{:02}", day);
format!("src/inputs/{}.txt", day_padded)
}
fn get_puzzle_path(day: u8) -> String {
let day_padded = format!("{:02}", day);
format!("src/puzzles/{}.md", day_padded)
}
fn build_args(command: &str, args: &[String], day: u8, year: Option<u16>) -> Vec<String> {
let mut cmd_args = args.to_vec();
if let Some(year) = year {
cmd_args.push("--year".into());
cmd_args.push(year.to_string());
}
cmd_args.append(&mut vec!["--day".into(), day.to_string(), command.into()]);
cmd_args
}
fn call_aoc_cli(args: &[String]) -> Result<Output, AocCliError> {
if cfg!(debug_assertions) {
println!("Calling >aoc with: {}", args.join(" "));
}
Command::new("aoc")
.args(args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|_| AocCliError::CommandNotCallable)
}
}