From 03deb5d77673df48268eb37d867cd8092702b81e Mon Sep 17 00:00:00 2001 From: Connor Slade Date: Sun, 26 Nov 2023 18:52:57 -0500 Subject: [PATCH] Scaffold: Custom formatter and start on init --- scaffold/src/args.rs | 50 ++++++-- scaffold/src/commands/init.rs | 50 +++++++- scaffold/src/commands/timer.rs | 12 +- scaffold/src/commands/token.rs | 20 ++-- scaffold/src/formatter.rs | 210 +++++++++++++++++++++++++++++++++ scaffold/src/main.rs | 11 +- scaffold/src/session.rs | 15 ++- scaffold/template.txt | 6 +- 8 files changed, 335 insertions(+), 39 deletions(-) create mode 100644 scaffold/src/formatter.rs diff --git a/scaffold/src/args.rs b/scaffold/src/args.rs index e083804..f4f924f 100644 --- a/scaffold/src/args.rs +++ b/scaffold/src/args.rs @@ -16,15 +16,35 @@ pub struct Args { pub subcommand: SubCommand, } +impl Args { + pub fn as_token_args(&self) -> &TokenArgs { + match &self.subcommand { + SubCommand::Token(args) => args, + _ => panic!("Expected token subcommand"), + } + } + + pub fn as_timer_args(&self) -> &TimerArgs { + match &self.subcommand { + SubCommand::Timer(args) => args, + _ => panic!("Expected timer subcommand"), + } + } + + pub fn as_init_args(&self) -> &InitArgs { + match &self.subcommand { + SubCommand::Init(args) => args, + _ => panic!("Expected init subcommand"), + } + } +} + #[derive(Parser, Debug)] pub enum SubCommand { /// Verify that the session token provided is still valid Verify, /// Update the token stored in `AOC_TOKEN`. - Token { - /// The session token you grabbed from the website. - token: String, - }, + Token(TokenArgs), /// Waits for the next midnight (EST) from December first to the twenty-fifth then returns. /// Chaining this command with another command, like init, will ensure that the input is fetched as soon as it is available. Timer(TimerArgs), @@ -33,6 +53,12 @@ pub enum SubCommand { Init(InitArgs), } +#[derive(Parser, Debug)] +pub struct TokenArgs { + /// The session token you grabbed from the website. + pub token: String, +} + #[derive(Parser, Debug)] pub struct TimerArgs { /// Time in seconds to offset the timer by. @@ -50,16 +76,16 @@ pub struct TimerArgs { #[derive(Parser, Debug)] pub struct InitArgs { /// A formatter that will be used to get the path for the input file. - #[arg(short, long, default_value = "{year}/{day:pad}.txt")] - input_location: String, + #[arg(short, long, default_value = "{{year}}/{{day:pad(2)}}.txt")] + pub input_location: String, /// A formatter that will be used to get the path for the solution file. - #[arg(short, long, default_value = "aoc_{year}/src/day_{day:pad}.rs")] + #[arg(short, long, default_value = "aoc_{{year}}/src/day_{{day:pad(2)}}.rs")] solution_location: String, /// Location formatter of the file importing each solution module. - #[arg(long, default_value = "aoc_{year}/src/lib.rs")] + #[arg(long, default_value = "aoc_{{year}}/src/lib.rs")] module_location: String, /// A formatter for a new line that will be added to the module file before the marker. - #[arg(long, default_values_t = ["mod day_{day:pad};".to_owned(), "&day_{day:pad}::Day{day:pad},".to_owned()])] + #[arg(long, default_values_t = ["mod day_{{day:pad(2)}};".to_owned(), "&day_{{day:pad(2)}}::Day{{day:pad(2)}},".to_owned()])] module_templates: Vec, /// A marker is a string that will be found in the module file and is used to determine where to insert the new line. /// If not provided, the default markers will be used. @@ -72,11 +98,11 @@ pub struct InitArgs { /// Don't create a solution file. /// Useful if you want to use this command with a different language or organization. #[arg(short, long)] - no_scaffold: bool, + pub no_scaffold: bool, /// The day to fetch the input for. - day: u8, + pub day: u8, /// The year to fetch the input for. #[arg(default_value_t = current_year())] - year: u16, + pub year: u16, } diff --git a/scaffold/src/commands/init.rs b/scaffold/src/commands/init.rs index 9c60d9f..a555cc6 100644 --- a/scaffold/src/commands/init.rs +++ b/scaffold/src/commands/init.rs @@ -1,7 +1,51 @@ +use std::{ + fs::{self, File}, + io::Write, + path::Path, +}; + use anyhow::Result; +use url::Url; + +use crate::{ + args::{Args, InitArgs}, + formatter::Formatter, + misc::current_year, + session::{Authenticated, Session}, +}; + +pub fn init(session: &Session, cmd: &InitArgs, args: &Args) -> Result<()> { + write_input(session, cmd, args)?; + Ok(()) +} + +fn write_input(session: &Session, cmd: &InitArgs, args: &Args) -> Result<()> { + let file_location = Formatter::new(&cmd.input_location)? + .format::<&[_]>(&[("year", cmd.year), ("day", cmd.day as u16)])?; + + let path = Path::new(&file_location); + if let Some(parent) = path.parent() { + if !parent.exists() { + fs::create_dir_all(parent)?; + } + } + + let mut file = File::create(path)?; + let input = fetch_input(session, &args.address, cmd.day, Some(cmd.year))?; + file.write_all(input.as_bytes())?; + println!("[*] Wrote input to {file_location}"); + Ok(()) +} + +fn fetch_input(session: &Session, base: &Url, day: u8, year: Option) -> Result { + let year = year.unwrap_or_else(current_year); + println!("[*] Fetching input for {day}/{year}"); -use crate::session::Session; + let url = base.join(&format!("{year}/day/{day}/input"))?; + let body = ureq::get(url.as_str()) + .authenticated(session) + .call()? + .into_string()?; -pub fn init(session: &Session, day: u8, year: Option) -> Result<()> { - todo!() + Ok(body) } diff --git a/scaffold/src/commands/timer.rs b/scaffold/src/commands/timer.rs index fcb927d..87a12d8 100644 --- a/scaffold/src/commands/timer.rs +++ b/scaffold/src/commands/timer.rs @@ -12,24 +12,24 @@ use crate::args::TimerArgs; /// The timezone of the Advent of Code release. const AOC_TIMEZONE: u32 = 5; -pub fn timer(timer: TimerArgs) -> Result<()> { +pub fn timer(cmd: &TimerArgs) -> Result<()> { let mut stop_time = next_release()?; - if let Some(offset) = timer.offset { + if let Some(offset) = cmd.offset { stop_time = stop_time + chrono::Duration::seconds(offset as i64); } if Utc::now() >= stop_time { println!("[*] The next puzzle has already been released."); - if timer.offset.is_some() { + if cmd.offset.is_some() { println!("[*] Note: This may be because of the offset you set"); } return Ok(()); } - if timer.quiet { + if cmd.quiet { println!("[*] Waiting..."); } else { println!("[*] The next puzzle will be released in:"); @@ -41,14 +41,14 @@ pub fn timer(timer: TimerArgs) -> Result<()> { break; } - if !timer.quiet { + if !cmd.quiet { let time_left = (stop_time - now).to_std()?; let time_left = Duration::new(time_left.as_secs(), 0); print!("\r\x1b[0K[*] {}", humantime::format_duration(time_left)); io::stdout().flush()?; } - thread::sleep(Duration::from_secs_f32(timer.frequency)); + thread::sleep(Duration::from_secs_f32(cmd.frequency)); } Ok(()) diff --git a/scaffold/src/commands/token.rs b/scaffold/src/commands/token.rs index edf39c8..fb0c951 100644 --- a/scaffold/src/commands/token.rs +++ b/scaffold/src/commands/token.rs @@ -1,16 +1,20 @@ use anyhow::Result; -use url::Url; -use crate::{commands::verify::verify_inner, session::Session, TOKEN_VAR}; +use crate::{ + args::{Args, TokenArgs}, + commands::verify::verify_inner, + session::Session, + TOKEN_VAR, +}; -pub fn token(session: &Option, token: String, url: &Url) -> Result<()> { - if token.len() != 128 { - anyhow::bail!("Invalid token length of {}, should be 128", token.len()); +pub fn token(session: &Option, cmd: &TokenArgs, args: &Args) -> Result<()> { + if cmd.token.len() != 128 { + anyhow::bail!("Invalid token length of {}, should be 128", cmd.token.len()); } println!("[*] Validating session token..."); - let new_session = Session::new(token.clone()); - verify_inner(&new_session, url)?; + let new_session = Session::new(&cmd.token); + verify_inner(&new_session, &args.address)?; println!("[*] Session token is valid."); if session.is_some() && session.as_ref().unwrap().is_from_env() { @@ -19,6 +23,6 @@ pub fn token(session: &Option, token: String, url: &Url) -> Result<()> println!("[*] Setting session token"); } - globalenv::set_var(TOKEN_VAR, &token)?; + globalenv::set_var(TOKEN_VAR, &cmd.token)?; Ok(()) } diff --git a/scaffold/src/formatter.rs b/scaffold/src/formatter.rs new file mode 100644 index 0000000..d489050 --- /dev/null +++ b/scaffold/src/formatter.rs @@ -0,0 +1,210 @@ +use std::{borrow::Cow, fmt::Display}; + +use anyhow::{bail, Context, Result}; + +use self::tokenize::Tokenizer; + +#[derive(Debug)] +pub struct Formatter { + components: Box<[Component]>, +} + +#[derive(Debug)] +enum Component { + Literal(String), + Format { + name: String, + processors: Box<[Processor]>, + }, +} + +#[derive(Debug)] +enum Processor { + Pad { width: usize }, + Uppercase, +} + +pub trait Arguments { + fn get(&self, name: &str) -> Option>; +} + +impl Formatter { + pub fn new(format: &str) -> Result { + let components = Tokenizer::new(format).tokenize()?.into(); + Ok(Self { components }) + } + + pub fn format(&self, args: T) -> Result { + let mut output = String::new(); + for component in self.components.iter() { + match component { + Component::Literal(literal) => output.push_str(literal), + Component::Format { name, processors } => { + let value = args + .get(name) + .with_context(|| format!("No argument named {name} found"))?; + + let mut value = value.to_string(); + for processor in processors.iter() { + value = processor.process(&value)?; + } + + output.push_str(&value); + } + } + } + + Ok(output) + } +} + +impl Processor { + fn parse(name: &str, args: &str) -> Result { + Ok(match name.to_lowercase().as_str() { + "pad" => { + let width = args.parse().unwrap(); + Self::Pad { width } + } + "uppercase" => Self::Uppercase, + _ => bail!("Unknown processor: {}", name), + }) + } + + fn process(&self, input: &str) -> Result { + Ok(match self { + Self::Pad { width } => { + let mut output = input.to_string(); + while output.len() < *width { + output.insert(0, '0'); + } + output + } + Self::Uppercase => input.to_uppercase(), + }) + } +} + +impl Arguments for &[(&str, T)] { + fn get(&self, name: &str) -> Option> { + self.iter() + .find(|(key, _)| key == &name) + .map(|(_, value)| Cow::Owned(value.to_string())) + } +} + +mod tokenize { + use anyhow::{bail, Result}; + + use super::{Component, Processor}; + + pub struct Tokenizer { + input: Box<[char]>, + index: usize, + output: Vec, + } + + impl Tokenizer { + pub fn new(input: &str) -> Self { + Self { + input: input.chars().collect(), + index: 0, + output: Vec::new(), + } + } + + pub fn tokenize(mut self) -> Result> { + while self.index < self.input.len() { + let c = self.input[self.index]; + match c { + // TODO: This can go out of bounds + '{' if self.input[self.index + 1] == '{' => self.tokenize_format()?, + _ => self.tokenize_literal(), + } + } + + Ok(self.output) + } + + fn tokenize_literal(&mut self) { + let mut literal = String::new(); + let mut past_start = false; + while self.index < self.input.len() { + let c = self.input[self.index]; + match c { + '{' if past_start => break, + _ => { + literal.push(c); + past_start = true; + } + } + self.index += 1; + } + + self.output.push(Component::Literal(literal)); + } + + fn tokenize_format(&mut self) -> Result<()> { + self.index += 2; + let mut name = String::new(); + let mut processors = Vec::new(); + while self.index < self.input.len() { + let c = self.input[self.index]; + match c { + '}' => { + self.index += 1; + break; + } + ':' => { + processors.push(self.parse_processor()?); + continue; + } + _ => name.push(c), + } + self.index += 1; + } + + self.index += 1; + self.output.push(Component::Format { + name, + processors: processors.into(), + }); + Ok(()) + } + + fn parse_processor(&mut self) -> Result { + self.index += 1; + let mut name = String::new(); + while self.index < self.input.len() { + let c = self.input[self.index]; + match c { + '}' => { + break; + } + ':' => { + break; + } + _ => name.push(c), + } + self.index += 1; + } + + let (name, args) = name.split_once('(').unwrap_or((&name, "")); + let args = &args[..args.len().saturating_sub(1)]; + Processor::parse(name.to_lowercase().as_str(), args) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + let formatter = Formatter::new("Hello, {{title:pad(2)}}.{{name}}!").unwrap(); + let output = formatter + .format::<&[(&str, &str)]>(&[("title", "Mr"), ("name", "John")]) + .unwrap(); + assert_eq!(output, "Hello, Mr.John!"); + } +} diff --git a/scaffold/src/main.rs b/scaffold/src/main.rs index 343dfac..fd04d5c 100644 --- a/scaffold/src/main.rs +++ b/scaffold/src/main.rs @@ -8,6 +8,7 @@ mod args; #[macro_use] mod misc; mod commands; +mod formatter; mod session; const TOKEN_VAR: &str = "AOC_TOKEN"; @@ -15,16 +16,16 @@ const TOKEN_VAR: &str = "AOC_TOKEN"; fn main() -> Result<()> { let args = Args::parse(); - let session = match args.token { + let session = match &args.token { Some(token) => Ok(Session::new(token)), None => Session::from_env(), }; - match args.subcommand { + match &args.subcommand { SubCommand::Verify => commands::verify::verify(&session?, &args.address)?, - SubCommand::Token { token } => commands::token::token(&session.ok(), token, &args.address)?, - SubCommand::Timer(args) => commands::timer::timer(args)?, - _ => todo!(), + SubCommand::Token(e) => commands::token::token(&session.ok(), e, &args)?, + SubCommand::Timer(e) => commands::timer::timer(e)?, + SubCommand::Init(e) => commands::init::init(&session?, e, &args)?, } Ok(()) diff --git a/scaffold/src/session.rs b/scaffold/src/session.rs index 7c7464f..3042d4d 100644 --- a/scaffold/src/session.rs +++ b/scaffold/src/session.rs @@ -2,6 +2,7 @@ use std::env; use anyhow::{Context, Result}; use scraper::Html; +use ureq::Request; use url::Url; use crate::TOKEN_VAR; @@ -12,9 +13,9 @@ pub struct Session { } impl Session { - pub fn new(token: String) -> Self { + pub fn new(token: &str) -> Self { Self { - token, + token: token.to_owned(), from_env: false, } } @@ -60,3 +61,13 @@ impl Session { pub struct SessionVerification { pub name: String, } + +pub trait Authenticated { + fn authenticated(self, session: &Session) -> Request; +} + +impl Authenticated for Request { + fn authenticated(self, session: &Session) -> Request { + self.set("Cookie", &format!("session={}", session.token)) + } +} diff --git a/scaffold/template.txt b/scaffold/template.txt index d3e5760..a33f92b 100644 --- a/scaffold/template.txt +++ b/scaffold/template.txt @@ -1,10 +1,10 @@ use common::{Answer, Solution}; -pub struct Day{day:pad}; +pub struct Day{{day:pad}}; -impl Solution for Day{day:pad} { +impl Solution for Day{{day:pad}} { fn name(&self) -> &'static str { - "{problem_name}" + "{{problem_name}}" } fn part_a(&self, input: &str) -> Answer {