From 3c7c5c65b2d581dd986e7207eef62e5fec397b69 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 2 Jul 2022 22:23:56 +0800 Subject: [PATCH 01/11] fix: clippy warning for rust 1.61 stable --- src/cache/mod.rs | 97 ++++++++++++++++++++++++--------------------- src/cache/models.rs | 27 ++++++++----- src/cfg.rs | 9 +++-- src/cmds/test.rs | 11 +++-- tests/de.rs | 1 - 5 files changed, 79 insertions(+), 66 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 5e1309f..a3cc7fa 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -9,10 +9,10 @@ use self::sql::*; use crate::{cfg, err::Error, plugins::LeetCode}; use colored::Colorize; use diesel::prelude::*; +use reqwest::Response; use serde::de::DeserializeOwned; use serde_json::Value; use std::collections::HashMap; -use reqwest::Response; /// sqlite connection pub fn conn(p: String) -> SqliteConnection { @@ -60,32 +60,27 @@ impl Cache { Ok(()) } - async fn get_user_info(&self) -> Result<(String,bool), Error> { - let user = parser::user( - self.clone().0 - .get_user_info().await? - .json().await? - ); + async fn get_user_info(&self) -> Result<(String, bool), Error> { + let user = parser::user(self.clone().0.get_user_info().await?.json().await?); match user { None => Err(Error::NoneError), Some(None) => Err(Error::CookieError), - Some(Some((s,b))) => Ok((s,b)) + Some(Some((s, b))) => Ok((s, b)), } } async fn is_session_bad(&self) -> bool { // i.e. self.get_user_info().contains_err(Error::CookieError) - match self.get_user_info().await { - Err(Error::CookieError) => true, - _ => false - } + matches!(self.get_user_info().await, Err(Error::CookieError)) } async fn resp_to_json(&self, resp: Response) -> Result { - let maybe_json: Result = resp.json().await; + let maybe_json: Result = resp.json().await; if maybe_json.is_err() && self.is_session_bad().await { Err(Error::CookieError) - } else { Ok(maybe_json?) } + } else { + Ok(maybe_json?) + } } /// Download leetcode problems to db @@ -123,7 +118,7 @@ impl Cache { Ok(p) } - /// Get daily problem + /// Get daily problem pub async fn get_daily_problem_id(&self) -> Result { parser::daily( self.clone() @@ -131,8 +126,9 @@ impl Cache { .get_question_daily() .await? .json() // does not require LEETCODE_SESSION - .await? - ).ok_or(Error::NoneError) + .await?, + ) + .ok_or(Error::NoneError) } /// Get problems from cache @@ -179,13 +175,14 @@ impl Cache { debug!("{:#?}", &json); match parser::desc(&mut rdesc, json) { None => return Err(Error::NoneError), - Some(false) => return - if self.is_session_bad().await { + Some(false) => { + return if self.is_session_bad().await { Err(Error::CookieError) } else { Err(Error::PremiumError) - }, - Some(true) => () + } + } + Some(true) => (), } // update the question @@ -215,7 +212,8 @@ impl Cache { .await? .json() .await?, - ).ok_or(Error::NoneError)?; + ) + .ok_or(Error::NoneError)?; let t = Tag { r#tag: rslug.to_string(), r#refs: serde_json::to_string(&ids)?, @@ -258,22 +256,20 @@ impl Cache { let mut code: String = "".to_string(); let maybe_file_testcases: Option = test_cases_path(&p) - .map(|filename| { - let mut tests = "".to_string(); - File::open(filename) - .and_then(|mut file_descriptor| file_descriptor.read_to_string(&mut tests)) - .map(|_| Some(tests)) - .unwrap_or(None) - }) - .unwrap_or(None); + .map(|filename| { + let mut tests = "".to_string(); + File::open(filename) + .and_then(|mut file_descriptor| file_descriptor.read_to_string(&mut tests)) + .map(|_| Some(tests)) + .unwrap_or(None) + }) + .unwrap_or(None); // Takes test cases using following priority // 1. cli parameter // 2. test cases from the file // 3. sample test case from the task - let testcase = testcase - .or(maybe_file_testcases) - .unwrap_or(d.case); + let testcase = testcase.or(maybe_file_testcases).unwrap_or(d.case); File::open(code_path(&p, None)?)?.read_to_string(&mut code)?; @@ -286,10 +282,19 @@ impl Cache { json.insert("data_input", testcase); let url = match run { - Run::Test => conf.sys.urls.get("test").ok_or(Error::NoneError)?.replace("$slug", &p.slug), + Run::Test => conf + .sys + .urls + .get("test") + .ok_or(Error::NoneError)? + .replace("$slug", &p.slug), Run::Submit => { json.insert("judge_type", "large".to_string()); - conf.sys.urls.get("submit").ok_or(Error::NoneError)?.replace("$slug", &p.slug) + conf.sys + .urls + .get("submit") + .ok_or(Error::NoneError)? + .replace("$slug", &p.slug) } }; @@ -297,7 +302,11 @@ impl Cache { json, [ url, - conf.sys.urls.get("problems").ok_or(Error::NoneError)?.replace("$slug", &p.slug), + conf.sys + .urls + .get("problems") + .ok_or(Error::NoneError)? + .replace("$slug", &p.slug), ], )) } @@ -309,13 +318,9 @@ impl Cache { trace!("Run veriy recursion..."); std::thread::sleep(Duration::from_micros(3000)); - let json: VerifyResult = self.resp_to_json( - self - .clone() - .0 - .verify_result(rid.clone()) - .await? - ).await?; + let json: VerifyResult = self + .resp_to_json(self.clone().0.verify_result(rid.clone()).await?) + .await?; Ok(json) } @@ -343,8 +348,10 @@ impl Cache { // Check if leetcode accepted the Run request if match run { Run::Test => run_res.interpret_id.is_empty(), - Run::Submit => run_res.submission_id == 0 - } { return Err(Error::CookieError) } + Run::Submit => run_res.submission_id == 0, + } { + return Err(Error::CookieError); + } let mut res: VerifyResult = VerifyResult::default(); while res.state != "SUCCESS" { diff --git a/src/cache/models.rs b/src/cache/models.rs index 2624233..bedc089 100644 --- a/src/cache/models.rs +++ b/src/cache/models.rs @@ -57,15 +57,15 @@ impl std::fmt::Display for Problem { 1 => { id.push_str(&SPACE.repeat(2)); id.push_str(&self.fid.to_string()); - id.push_str(&SPACE.to_string()); + id.push_str(SPACE); } 2 => { - id.push_str(&SPACE.to_string()); + id.push_str(SPACE); id.push_str(&self.fid.to_string()); - id.push_str(&SPACE.to_string()); + id.push_str(SPACE); } 3 => { - id.push_str(&SPACE.to_string()); + id.push_str(SPACE); id.push_str(&self.fid.to_string()); } 4 => { @@ -268,7 +268,7 @@ impl std::fmt::Display for VerifyResult { &"Runtime: ".before_spaces(7).dimmed(), &self.status.status_runtime.dimmed(), &"\nYour input:".after_spaces(4), - &self.data_input.replace("\n", "↩ "), + &self.data_input.replace('\n', "↩ "), &"\nOutput:".after_spaces(8), ca, &"\nExpected:".after_spaces(6), @@ -342,7 +342,7 @@ impl std::fmt::Display for VerifyResult { " Runtime: ".dimmed(), &self.status.status_runtime.dimmed(), &"\nYour input:".after_spaces(4), - &self.data_input.replace("\n", "↩ "), + &self.data_input.replace('\n', "↩ "), &"\nOutput:".after_spaces(8), ca, &"\nExpected:".after_spaces(6), @@ -373,7 +373,7 @@ impl std::fmt::Display for VerifyResult { .bold() .yellow(), &"Last case:".after_spaces(5).dimmed(), - &self.submit.last_testcase.replace("\n", "↩ ").dimmed(), + &self.submit.last_testcase.replace('\n', "↩ ").dimmed(), &"\nOutput:".after_spaces(8), self.code_output[0], &"\nExpected:".after_spaces(6), @@ -385,7 +385,7 @@ impl std::fmt::Display for VerifyResult { "\n{}\n\n{}{}\n", &self.status.status_msg.yellow().bold(), &"Last case:".after_spaces(5).dimmed(), - &self.data_input.replace("\n", "↩ "), + &self.data_input.replace('\n', "↩ "), )?, // Output Timeout Exceeded // @@ -393,7 +393,12 @@ impl std::fmt::Display for VerifyResult { // if anybody reach this, welcome to fix this! 13 | 14 => write!(f, "\n{}\n", &self.status.status_msg.yellow().bold(),)?, // Runtime error - 15 => write!(f, "\n{}\n{}\n'", &self.status.status_msg.red().bold(), &self.status.runtime_error)?, + 15 => write!( + f, + "\n{}\n{}\n'", + &self.status.status_msg.red().bold(), + &self.status.runtime_error + )?, // Compile Error 20 => write!( f, @@ -436,7 +441,7 @@ impl std::fmt::Display for VerifyResult { f, "{}{}", &"Stdout:".after_spaces(8).purple(), - &self.std_output.replace("\n", &"\n".after_spaces(15)) + &self.std_output.replace('\n', &"\n".after_spaces(15)) ) } else { write!(f, "") @@ -495,7 +500,7 @@ mod verify { #[serde(default)] pub status_runtime: String, #[serde(default)] - pub runtime_error: String + pub runtime_error: String, } #[derive(Debug, Default, Deserialize)] diff --git a/src/cfg.rs b/src/cfg.rs index ba57d59..410088d 100644 --- a/src/cfg.rs +++ b/src/cfg.rs @@ -5,9 +5,9 @@ //! //! + Edit leetcode.toml at `~/.leetcode/leetcode.toml` directly //! + Use `leetcode config` to update it +use crate::Error; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs, path::PathBuf}; -use crate::Error; const DEFAULT_CONFIG: &str = r#" # usually you don't wanna change those @@ -150,8 +150,11 @@ pub struct Storage { impl Storage { /// convert root path pub fn root(&self) -> Result { - let home = dirs::home_dir().ok_or(Error::NoneError)?.to_string_lossy().to_string(); - let path = self.root.replace("~", &home); + let home = dirs::home_dir() + .ok_or(Error::NoneError)? + .to_string_lossy() + .to_string(); + let path = self.root.replace('~', &home); Ok(path) } diff --git a/src/cmds/test.rs b/src/cmds/test.rs index 37c0e7f..bf88011 100644 --- a/src/cmds/test.rs +++ b/src/cmds/test.rs @@ -1,6 +1,6 @@ //! Test command -use crate::Error; use super::Command; +use crate::Error; use async_trait::async_trait; use clap::{App, ArgMatches}; @@ -49,11 +49,10 @@ impl Command for TestCommand { use crate::cache::{Cache, Run}; let id: i32 = m.value_of("id").ok_or(Error::NoneError)?.parse()?; let testcase = m.value_of("testcase"); - let case_str: Option; - match testcase { - Some(case) => case_str = Option::from(case.replace("\\n", "\n")), - _ => case_str = None, - } + let case_str: Option = match testcase { + Some(case) => Option::from(case.replace("\\n", "\n")), + _ => None, + }; let cache = Cache::new()?; let res = cache.exec_problem(id, Run::Test, case_str).await?; diff --git a/tests/de.rs b/tests/de.rs index 06d30df..2a90009 100644 --- a/tests/de.rs +++ b/tests/de.rs @@ -1,5 +1,4 @@ use leetcode_cli::cache::models::VerifyResult; -use serde_json; #[test] fn de_from_test_success() { From 925e0fa47e188e83b520e13c4a99680e05574107 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 3 Jul 2022 10:02:57 +0800 Subject: [PATCH 02/11] feat: upgrade dep version, and use Rust 2021 Edition --- Cargo.toml | 42 +++++++++++++++++++++--------------------- src/bin/lc.rs | 3 +-- src/cli.rs | 16 ++++++++-------- src/cmds/data.rs | 12 ++++++------ src/cmds/edit.rs | 11 +++++------ src/cmds/exec.rs | 9 ++++----- src/cmds/list.rs | 20 ++++++++++---------- src/cmds/mod.rs | 8 ++++---- src/cmds/pick.rs | 22 ++++++++++++---------- src/cmds/stat.rs | 8 ++++---- src/cmds/test.rs | 9 ++++----- src/flag.rs | 8 ++++---- src/plugins/chrome.rs | 9 ++++++--- 13 files changed, 89 insertions(+), 88 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 85699e8..1bce70f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ path = "src/bin/lc.rs" name = "leetcode-cli" version = "0.3.11" authors = ["clearloop "] -edition = "2018" +edition = "2021" description = "Leet your code in command-line." repository = "https://github.com/clearloop/leetcode-cli" license = "MIT" @@ -16,33 +16,33 @@ keywords = ["cli", "games", "leetcode"] readme = './README.md' [dependencies] -async-trait = "0.1.41" -tokio = "0.2.22" -clap = "2.33.0" -colored = "1.9.1" -dirs = "2.0.2" -env_logger = "0.7.1" -escaper = "0.1.0" -keyring = "0.8.0" -log = "0.4" -openssl = "0.10.26" -pyo3 = { version = "0.8.5", optional = true } -rand = "0.7.2" -serde = { version = "1.0.104", features = ["derive"] } -serde_json = "1.0.44" -toml = "0.5.5" -regex = "1" +async-trait = "0.1.56" +tokio = { version = "1", features = ["full"] } +clap = { version = "3.2.8", features = ["cargo"] } +colored = "2.0.0" +dirs = "4.0.0" +env_logger = "0.9.0" +escaper = "0.1.1" +keyring = "1.1.2" +log = "0.4.17" +openssl = "0.10.40" +pyo3 = { version = "0.16.5", optional = true } +rand = "0.8.5" +serde = { version = "1.0.138", features = ["derive"] } +serde_json = "1.0.82" +toml = "0.5.9" +regex = "1.5.6" [dependencies.diesel] -version = "1.4.3" +version = "1.4.8" features = ["sqlite"] [dependencies.reqwest] -version = "0.10.3" +version = "0.11.11" features = ["gzip", "json"] [dev-dependencies.cargo-husky] -version = "1" +version = "1.5.0" default-features = false features = ["precommit-hook", "user-hooks"] @@ -50,4 +50,4 @@ features = ["precommit-hook", "user-hooks"] pym = ["pyo3"] [target.'cfg(target_family = "unix")'.dependencies] -nix = "0.17.0" +nix = "0.24.1" diff --git a/src/bin/lc.rs b/src/bin/lc.rs index f353003..4364cb7 100644 --- a/src/bin/lc.rs +++ b/src/bin/lc.rs @@ -2,8 +2,7 @@ use leetcode_cli::cli; use tokio::runtime::Builder; fn main() { - if let Err(err) = Builder::new() - .basic_scheduler() + if let Err(err) = Builder::new_multi_thread() .enable_all() .build() .expect("Build tokio runtime failed") diff --git a/src/cli.rs b/src/cli.rs index df86b7b..f232a2d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -44,19 +44,19 @@ pub async fn main() -> Result<(), Error> { if m.is_present("debug") { Debug::handler()?; } else { - env_logger::from_env(env_logger::Env::new().default_filter_or("info")) + env_logger::Builder::from_env(env_logger::Env::new().default_filter_or("info")) .format_timestamp(None) .init(); } match m.subcommand() { - ("data", Some(sub_m)) => Ok(DataCommand::handler(sub_m).await?), - ("edit", Some(sub_m)) => Ok(EditCommand::handler(sub_m).await?), - ("exec", Some(sub_m)) => Ok(ExecCommand::handler(sub_m).await?), - ("list", Some(sub_m)) => Ok(ListCommand::handler(sub_m).await?), - ("pick", Some(sub_m)) => Ok(PickCommand::handler(sub_m).await?), - ("stat", Some(sub_m)) => Ok(StatCommand::handler(sub_m).await?), - ("test", Some(sub_m)) => Ok(TestCommand::handler(sub_m).await?), + Some(("data", sub_m)) => Ok(DataCommand::handler(sub_m).await?), + Some(("edit", sub_m)) => Ok(EditCommand::handler(sub_m).await?), + Some(("exec", sub_m)) => Ok(ExecCommand::handler(sub_m).await?), + Some(("list", sub_m)) => Ok(ListCommand::handler(sub_m).await?), + Some(("pick", sub_m)) => Ok(PickCommand::handler(sub_m).await?), + Some(("stat", sub_m)) => Ok(StatCommand::handler(sub_m).await?), + Some(("test", sub_m)) => Ok(TestCommand::handler(sub_m).await?), _ => Err(Error::MatchError), } } diff --git a/src/cmds/data.rs b/src/cmds/data.rs index 0ec57bf..2e837c9 100644 --- a/src/cmds/data.rs +++ b/src/cmds/data.rs @@ -2,7 +2,7 @@ use super::Command; use crate::{cache::Cache, helper::Digit, Error}; use async_trait::async_trait; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{Command as ClapCommand, Arg, ArgMatches}; use colored::Colorize; /// Abstract `data` command @@ -25,28 +25,28 @@ pub struct DataCommand; #[async_trait] impl Command for DataCommand { /// `data` command usage - fn usage<'a, 'cache>() -> App<'a, 'cache> { - SubCommand::with_name("data") + fn usage<'a>() -> ClapCommand<'a> { + ClapCommand::new("data") .about("Manage Cache") .visible_alias("d") .arg( Arg::with_name("delete") .display_order(1) - .short("d") + .short('d') .long("delete") .help("Delete cache"), ) .arg( Arg::with_name("update") .display_order(2) - .short("u") + .short('u') .long("update") .help("Update cache"), ) } /// `data` handler - async fn handler(m: &ArgMatches<'_>) -> Result<(), Error> { + async fn handler(m: &ArgMatches) -> Result<(), Error> { use std::fs::File; use std::path::Path; diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index 21285fb..3252052 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -2,7 +2,7 @@ use crate::Error; use super::Command; use async_trait::async_trait; -use clap::{App, ArgMatches}; +use clap::{Command as ClapCommand, ArgMatches, Arg}; /// Abstract `edit` command /// @@ -25,14 +25,13 @@ pub struct EditCommand; #[async_trait] impl Command for EditCommand { /// `edit` usage - fn usage<'a, 'edit>() -> App<'a, 'edit> { - use clap::{Arg, SubCommand}; - SubCommand::with_name("edit") + fn usage<'a>() -> ClapCommand<'a> { + ClapCommand::new("edit") .about("Edit question by id") .visible_alias("e") .arg( Arg::with_name("lang") - .short("l") + .short('l') .long("lang") .takes_value(true) .help("Edit with specific language"), @@ -46,7 +45,7 @@ impl Command for EditCommand { } /// `edit` handler - async fn handler(m: &ArgMatches<'_>) -> Result<(), crate::Error> { + async fn handler(m: &ArgMatches) -> Result<(), crate::Error> { use crate::{cache::models::Question, Cache}; use std::fs::File; use std::io::Write; diff --git a/src/cmds/exec.rs b/src/cmds/exec.rs index 755fd37..e660f58 100644 --- a/src/cmds/exec.rs +++ b/src/cmds/exec.rs @@ -2,7 +2,7 @@ use crate::Error; use super::Command; use async_trait::async_trait; -use clap::{App, ArgMatches}; +use clap::{Command as ClapCommand, ArgMatches, Arg}; /// Abstract Exec Command /// @@ -25,9 +25,8 @@ pub struct ExecCommand; #[async_trait] impl Command for ExecCommand { /// `exec` usage - fn usage<'a, 'edit>() -> App<'a, 'edit> { - use clap::{Arg, SubCommand}; - SubCommand::with_name("exec") + fn usage<'a>() -> ClapCommand<'a> { + ClapCommand::new("exec") .about("Submit solution") .visible_alias("x") .arg( @@ -39,7 +38,7 @@ impl Command for ExecCommand { } /// `exec` handler - async fn handler(m: &ArgMatches<'_>) -> Result<(), crate::Error> { + async fn handler(m: &ArgMatches) -> Result<(), crate::Error> { use crate::cache::{Cache, Run}; let id: i32 = m.value_of("id").ok_or(Error::NoneError)?.parse()?; diff --git a/src/cmds/list.rs b/src/cmds/list.rs index b7a9b1e..1adc6c7 100644 --- a/src/cmds/list.rs +++ b/src/cmds/list.rs @@ -36,7 +36,7 @@ use super::Command; use crate::{cache::Cache, err::Error, helper::Digit}; use async_trait::async_trait; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{Command as ClapCommand, Arg, ArgMatches}; /// Abstract `list` command /// /// ## handler @@ -72,34 +72,34 @@ static LIST_AFTER_HELP: &str = r#"EXAMPLES: #[async_trait] impl Command for ListCommand { /// `list` command usage - fn usage<'a, 'list>() -> App<'a, 'list> { - SubCommand::with_name("list") + fn usage<'a>() -> ClapCommand<'a> { + ClapCommand::new("list") .about("List problems") .visible_alias("l") .arg( Arg::with_name("category") - .short("c") + .short('c') .long("category") .takes_value(true) .help(CATEGORY_HELP), ) .arg( Arg::with_name("plan") - .short("p") + .short('p') .long("plan") .takes_value(true) .help("Invoking python scripts to filter questions"), ) .arg( Arg::with_name("query") - .short("q") + .short('q') .long("query") .takes_value(true) .help(QUERY_HELP), ) .arg( Arg::with_name("range") - .short("r") + .short('r') .long("range") .takes_value(true) .min_values(2) @@ -108,13 +108,13 @@ impl Command for ListCommand { .after_help(LIST_AFTER_HELP) .arg( Arg::with_name("stat") - .short("s") + .short('s') .long("stat") .help("Show statistics of listed problems"), ) .arg( Arg::with_name("tag") - .short("t") + .short('t') .long("tag") .takes_value(true) .help("Filter questions by tag"), @@ -131,7 +131,7 @@ impl Command for ListCommand { /// List commands contains "-c", "-q", "-s" flags. /// + matches with `-c` will override the default keyword. /// + `-qs` - async fn handler(m: &ArgMatches<'_>) -> Result<(), Error> { + async fn handler(m: &ArgMatches) -> Result<(), Error> { trace!("Input list command..."); let cache = Cache::new()?; diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index 224d3e0..cc00a0f 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -12,16 +12,16 @@ //! ``` use crate::err::Error; use async_trait::async_trait; -use clap::{App, ArgMatches}; +use clap::{Command as ClapCommand, ArgMatches}; /// Abstract commands' trait. #[async_trait] pub trait Command { - /// Usage of the spefic command - fn usage<'a, 'c>() -> App<'a, 'c>; + /// Usage of the specific command + fn usage<'a>() -> ClapCommand<'a>; /// The handler will deal [args, options,...] from the command-line - async fn handler(m: &ArgMatches<'_>) -> Result<(), Error>; + async fn handler(m: &ArgMatches) -> Result<(), Error>; } mod data; diff --git a/src/cmds/pick.rs b/src/cmds/pick.rs index 79d2751..1308aa7 100644 --- a/src/cmds/pick.rs +++ b/src/cmds/pick.rs @@ -2,7 +2,7 @@ use super::Command; use crate::err::Error; use async_trait::async_trait; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{Arg, ArgMatches, Command as ClapCommand}; /// Abstract pick command /// /// ```sh @@ -43,35 +43,35 @@ s = starred S = not starred"#; #[async_trait] impl Command for PickCommand { /// `pick` usage - fn usage<'a, 'pick>() -> App<'a, 'pick> { - SubCommand::with_name("pick") + fn usage<'a>() -> ClapCommand<'a> { + ClapCommand::new("pick") .about("Pick a problem") .visible_alias("p") .arg(Arg::with_name("id").help("Problem id").takes_value(true)) .arg( Arg::with_name("plan") - .short("p") + .short('p') .long("plan") .takes_value(true) .help("Invoking python scripts to filter questions"), ) .arg( Arg::with_name("query") - .short("q") + .short('q') .long("query") .takes_value(true) .help(QUERY_HELP), ) .arg( Arg::with_name("tag") - .short("t") + .short('t') .long("tag") .takes_value(true) .help("Filter questions by tag"), ) .arg( Arg::with_name("daily") - .short("d") + .short('d') .long("daily") .takes_value(false) .help("Pick today's daily challenge"), @@ -79,7 +79,7 @@ impl Command for PickCommand { } /// `pick` handler - async fn handler(m: &ArgMatches<'_>) -> Result<(), Error> { + async fn handler(m: &ArgMatches) -> Result<(), Error> { use crate::cache::Cache; use rand::Rng; @@ -118,7 +118,9 @@ impl Command for PickCommand { let daily_id = if m.is_present("daily") { Some(cache.get_daily_problem_id().await?) - } else { None }; + } else { + None + }; let fid = m .value_of("id") @@ -126,7 +128,7 @@ impl Command for PickCommand { .or(daily_id) .unwrap_or_else(|| { // Pick random without specify id - let problem = &problems[rand::thread_rng().gen_range(0, problems.len())]; + let problem = &problems[rand::thread_rng().gen_range(0..problems.len())]; problem.fid }); diff --git a/src/cmds/stat.rs b/src/cmds/stat.rs index b8fea24..661ca98 100644 --- a/src/cmds/stat.rs +++ b/src/cmds/stat.rs @@ -1,7 +1,7 @@ //! status command use super::Command; use async_trait::async_trait; -use clap::{App, ArgMatches, SubCommand}; +use clap::{Command as ClapCommand, ArgMatches}; use colored::Colorize; /// Abstract statues command @@ -22,14 +22,14 @@ pub struct StatCommand; #[async_trait] impl Command for StatCommand { /// `stat` usage - fn usage<'a, 'stat>() -> App<'a, 'stat> { - SubCommand::with_name("stat") + fn usage<'a>() -> ClapCommand<'a> { + ClapCommand::new("stat") .about("Show simple chart about submissions") .visible_alias("s") } /// `stat` handler - async fn handler(_m: &ArgMatches<'_>) -> Result<(), crate::err::Error> { + async fn handler(_m: &ArgMatches) -> Result<(), crate::err::Error> { use crate::{helper::Digit, Cache}; let cache = Cache::new()?; diff --git a/src/cmds/test.rs b/src/cmds/test.rs index bf88011..62b4fc4 100644 --- a/src/cmds/test.rs +++ b/src/cmds/test.rs @@ -2,7 +2,7 @@ use super::Command; use crate::Error; use async_trait::async_trait; -use clap::{App, ArgMatches}; +use clap::{Arg, ArgMatches, Command as ClapCommand}; /// Abstract Test Command /// @@ -25,9 +25,8 @@ pub struct TestCommand; #[async_trait] impl Command for TestCommand { /// `test` usage - fn usage<'a, 'edit>() -> App<'a, 'edit> { - use clap::{Arg, SubCommand}; - SubCommand::with_name("test") + fn usage<'a>() -> ClapCommand<'a> { + ClapCommand::new("test") .about("Test question by id") .visible_alias("t") .arg( @@ -45,7 +44,7 @@ impl Command for TestCommand { } /// `test` handler - async fn handler(m: &ArgMatches<'_>) -> Result<(), Error> { + async fn handler(m: &ArgMatches) -> Result<(), Error> { use crate::cache::{Cache, Run}; let id: i32 = m.value_of("id").ok_or(Error::NoneError)?.parse()?; let testcase = m.value_of("testcase"); diff --git a/src/flag.rs b/src/flag.rs index 0264bb2..6706a06 100644 --- a/src/flag.rs +++ b/src/flag.rs @@ -12,7 +12,7 @@ use env_logger::Env; /// Abstract flag trait pub trait Flag { - fn usage<'a, 'f>() -> Arg<'a, 'f>; + fn usage<'a>() -> Arg<'a>; fn handler() -> Result<(), Error>; } @@ -20,15 +20,15 @@ pub trait Flag { pub struct Debug; impl Flag for Debug { - fn usage<'a, 'f>() -> Arg<'a, 'f> { + fn usage<'a>() -> Arg<'a> { Arg::with_name("debug") - .short("d") + .short('d') .long("debug") .help("debug mode") } fn handler() -> Result<(), Error> { - env_logger::from_env(Env::default().default_filter_or("leetcode")).init(); + env_logger::Builder::from_env(Env::default().default_filter_or("leetcode")).init(); Ok(()) } diff --git a/src/plugins/chrome.rs b/src/plugins/chrome.rs index 8922b7a..20ba976 100644 --- a/src/plugins/chrome.rs +++ b/src/plugins/chrome.rs @@ -1,6 +1,6 @@ use crate::{cache, Error}; use diesel::prelude::*; -use keyring::Keyring; +use keyring::Entry; use openssl::{hash, pkcs5, symm}; use std::collections::HashMap; @@ -73,7 +73,7 @@ pub fn cookies() -> Result { } // Get system password - let ring = Keyring::new("Chrome Safe Storage", "Chrome"); + let ring = Entry::new("Chrome Safe Storage", "Chrome"); let pass = ring.get_password().expect("Get Password failed"); // Decode cookies @@ -86,7 +86,10 @@ pub fn cookies() -> Result { Ok(Ident { csrf: m.get("csrftoken").ok_or(Error::NoneError)?.to_string(), - session: m.get("LEETCODE_SESSION").ok_or(Error::NoneError)?.to_string(), + session: m + .get("LEETCODE_SESSION") + .ok_or(Error::NoneError)? + .to_string(), }) } From 38b823c41cd706a6a006207b16e3e4cb0f5b0e57 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 3 Jul 2022 10:47:24 +0800 Subject: [PATCH 03/11] feat: remove test file --- src/cache/mod.rs | 28 ++++++++++++---------------- src/cmds/edit.rs | 9 ++------- src/helper.rs | 21 +++------------------ 3 files changed, 17 insertions(+), 41 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index a3cc7fa..2b57542 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -236,10 +236,10 @@ impl Cache { &self, run: Run, rfid: i32, - testcase: Option, + test_case: Option, ) -> Result<(HashMap<&'static str, String>, [String; 2]), Error> { trace!("pre run code..."); - use crate::helper::{code_path, test_cases_path}; + use crate::helper::code_path; use std::fs::File; use std::io::Read; @@ -255,21 +255,17 @@ impl Cache { let mut json: HashMap<&'static str, String> = HashMap::new(); let mut code: String = "".to_string(); - let maybe_file_testcases: Option = test_cases_path(&p) - .map(|filename| { - let mut tests = "".to_string(); - File::open(filename) - .and_then(|mut file_descriptor| file_descriptor.read_to_string(&mut tests)) - .map(|_| Some(tests)) - .unwrap_or(None) - }) - .unwrap_or(None); + let maybe_all_testcases: Option = if d.all_cases.is_empty() { + None + } else { + Some(d.all_cases.to_string()) + }; // Takes test cases using following priority // 1. cli parameter - // 2. test cases from the file + // 2. test cases from problem desc all test cases // 3. sample test case from the task - let testcase = testcase.or(maybe_file_testcases).unwrap_or(d.case); + let test_case = test_case.or(maybe_all_testcases).unwrap_or(d.case); File::open(code_path(&p, None)?)?.read_to_string(&mut code)?; @@ -279,7 +275,7 @@ impl Cache { // pass manually data json.insert("name", p.name.to_string()); - json.insert("data_input", testcase); + json.insert("data_input", test_case); let url = match run { Run::Test => conf @@ -330,10 +326,10 @@ impl Cache { &self, rfid: i32, run: Run, - testcase: Option, + test_case: Option, ) -> Result { trace!("Exec problem filter —— Test or Submit"); - let (json, [url, refer]) = self.pre_run_code(run.clone(), rfid, testcase).await?; + let (json, [url, refer]) = self.pre_run_code(run.clone(), rfid, test_case).await?; trace!("Pre run code result {:?}, {:?}, {:?}", json, url, refer); let run_res: RunCode = self diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index 3252052..65d7b07 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -1,8 +1,8 @@ //! Edit command -use crate::Error; use super::Command; +use crate::Error; use async_trait::async_trait; -use clap::{Command as ClapCommand, ArgMatches, Arg}; +use clap::{Arg, ArgMatches, Command as ClapCommand}; /// Abstract `edit` command /// @@ -64,7 +64,6 @@ impl Command for EditCommand { let lang = conf.code.lang; let path = crate::helper::code_path(&target, Some(lang.to_owned()))?; - let tests_path = crate::helper::test_cases_path(&target)?; if !Path::new(&path).exists() { let mut qr = serde_json::from_str(&target.desc); @@ -75,22 +74,18 @@ impl Command for EditCommand { let question: Question = qr?; let mut file_code = File::create(&path)?; - let mut file_tests = File::create(&tests_path)?; let mut flag = false; for d in question.defs.0 { if d.value == lang { flag = true; file_code.write_all(d.code.to_string().as_bytes())?; - file_tests.write_all(question.all_cases.as_bytes())?; } } // if language is not found in the list of supported languges clean up files if !flag { std::fs::remove_file(&path)?; - std::fs::remove_file(&tests_path)?; - return Err(crate::Error::FeatureError(format!( "This question doesn't support {}, please try another", &lang diff --git a/src/helper.rs b/src/helper.rs index b68fe68..08aaea8 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,6 +1,6 @@ //! A set of helper traits pub use self::digit::Digit; -pub use self::file::{code_path, load_script, test_cases_path}; +pub use self::file::{code_path, load_script}; pub use self::filter::{filter, squash}; pub use self::html::HTML; @@ -151,7 +151,8 @@ mod html { let mut s = self.clone(); // some problems (e.g. 1653) have ZWSPs. s.retain(|x| x != '\u{200B}'); - s }; + s + }; let res: Vec; // styled { @@ -283,22 +284,6 @@ mod file { use crate::{cache::models::Problem, Error}; - /// Generate test casese path by fid - pub fn test_cases_path(target: &Problem) -> Result { - let conf = crate::cfg::locate()?; - - let mut path = format!( - "{}/{}.tests.dat", - conf.storage.code()?, - conf.code.pick, - ); - - path = path.replace("${fid}", &target.fid.to_string()); - path = path.replace("${slug}", &target.slug.to_string()); - - Ok(path) - } - /// Generate code path by fid pub fn code_path(target: &Problem, l: Option) -> Result { let conf = crate::cfg::locate()?; From 82cbf31ebf7dfa0e31b0d7a887e8bf4e906c66ee Mon Sep 17 00:00:00 2001 From: David Date: Sun, 3 Jul 2022 10:52:39 +0800 Subject: [PATCH 04/11] fix: replace deprecated is_present --- src/cli.rs | 2 +- src/cmds/data.rs | 9 +++++---- src/cmds/edit.rs | 2 +- src/cmds/list.rs | 18 ++++++++++-------- src/cmds/pick.rs | 8 ++++---- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index f232a2d..357adad 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -41,7 +41,7 @@ pub async fn main() -> Result<(), Error> { .setting(AppSettings::ArgRequiredElseHelp) .get_matches(); - if m.is_present("debug") { + if m.contains_id("debug") { Debug::handler()?; } else { env_logger::Builder::from_env(env_logger::Env::new().default_filter_or("info")) diff --git a/src/cmds/data.rs b/src/cmds/data.rs index 2e837c9..c590458 100644 --- a/src/cmds/data.rs +++ b/src/cmds/data.rs @@ -2,7 +2,7 @@ use super::Command; use crate::{cache::Cache, helper::Digit, Error}; use async_trait::async_trait; -use clap::{Command as ClapCommand, Arg, ArgMatches}; +use clap::{Arg, ArgMatches, Command as ClapCommand}; use colored::Colorize; /// Abstract `data` command @@ -58,7 +58,8 @@ impl Command for DataCommand { let out = format!( " {}{}", Path::new(&path) - .file_name().ok_or(Error::NoneError)? + .file_name() + .ok_or(Error::NoneError)? .to_string_lossy() .to_string() .digit(65 - (len.len() as i32)) @@ -72,13 +73,13 @@ impl Command for DataCommand { title.push_str(&"-".repeat(65)); let mut flags = 0; - if m.is_present("delete") { + if m.contains_id("delete") { flags += 1; cache.clean()?; println!("{}", "ok!".bright_green()); } - if m.is_present("update") { + if m.contains_id("update") { flags += 1; cache.update().await?; println!("{}", "ok!".bright_green()); diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index 65d7b07..fd70b35 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -57,7 +57,7 @@ impl Command for EditCommand { let mut conf = cache.to_owned().0.conf; // condition language - if m.is_present("lang") { + if m.contains_id("lang") { conf.code.lang = m.value_of("lang").ok_or(Error::NoneError)?.to_string(); conf.sync()?; } diff --git a/src/cmds/list.rs b/src/cmds/list.rs index 1adc6c7..0f39329 100644 --- a/src/cmds/list.rs +++ b/src/cmds/list.rs @@ -36,7 +36,7 @@ use super::Command; use crate::{cache::Cache, err::Error, helper::Digit}; use async_trait::async_trait; -use clap::{Command as ClapCommand, Arg, ArgMatches}; +use clap::{Arg, ArgMatches, Command as ClapCommand}; /// Abstract `list` command /// /// ## handler @@ -147,14 +147,14 @@ impl Command for ListCommand { // pym scripts #[cfg(feature = "pym")] { - if m.is_present("plan") { + if m.contains_id("plan") { let ids = crate::pym::exec(m.value_of("plan").unwrap_or(""))?; crate::helper::squash(&mut ps, ids)?; } } // filter tag - if m.is_present("tag") { + if m.contains_id("tag") { let ids = cache .get_tagged_questions(m.value_of("tag").unwrap_or("")) .await?; @@ -162,19 +162,21 @@ impl Command for ListCommand { } // filter category - if m.is_present("category") { + if m.contains_id("category") { ps.retain(|x| x.category == m.value_of("category").unwrap_or("algorithms")); } // filter query - if m.is_present("query") { + if m.contains_id("query") { let query = m.value_of("query").ok_or(Error::NoneError)?; crate::helper::filter(&mut ps, query.to_string()); } // filter range - if m.is_present("range") { - let num_range: Vec = m.values_of("range").ok_or(Error::NoneError)? + if m.contains_id("range") { + let num_range: Vec = m + .values_of("range") + .ok_or(Error::NoneError)? .into_iter() .map(|x| x.parse::().unwrap_or(0)) .collect(); @@ -191,7 +193,7 @@ impl Command for ListCommand { println!("{}", out.join("\n")); // one more thing, filter stat - if m.is_present("stat") { + if m.contains_id("stat") { let mut listed = 0; let mut locked = 0; let mut starred = 0; diff --git a/src/cmds/pick.rs b/src/cmds/pick.rs index 1308aa7..98c9e4b 100644 --- a/src/cmds/pick.rs +++ b/src/cmds/pick.rs @@ -95,14 +95,14 @@ impl Command for PickCommand { // pym scripts #[cfg(feature = "pym")] { - if m.is_present("plan") { + if m.contains_id("plan") { let ids = crate::pym::exec(m.value_of("plan").unwrap_or(""))?; crate::helper::squash(&mut problems, ids)?; } } // tag filter - if m.is_present("tag") { + if m.contains_id("tag") { let ids = cache .clone() .get_tagged_questions(m.value_of("tag").unwrap_or("")) @@ -111,12 +111,12 @@ impl Command for PickCommand { } // query filter - if m.is_present("query") { + if m.contains_id("query") { let query = m.value_of("query").ok_or(Error::NoneError)?; crate::helper::filter(&mut problems, query.to_string()); } - let daily_id = if m.is_present("daily") { + let daily_id = if m.contains_id("daily") { Some(cache.get_daily_problem_id().await?) } else { None From 210d772ed779c69095c15a588f73df41c405b041 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 3 Jul 2022 12:48:32 +0800 Subject: [PATCH 05/11] feat: use scraper to parser html fragment --- Cargo.toml | 2 +- src/cmds/pick.rs | 15 +++-- src/helper.rs | 167 ++++------------------------------------------- 3 files changed, 22 insertions(+), 162 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1bce70f..a73df5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,6 @@ clap = { version = "3.2.8", features = ["cargo"] } colored = "2.0.0" dirs = "4.0.0" env_logger = "0.9.0" -escaper = "0.1.1" keyring = "1.1.2" log = "0.4.17" openssl = "0.10.40" @@ -32,6 +31,7 @@ serde = { version = "1.0.138", features = ["derive"] } serde_json = "1.0.82" toml = "0.5.9" regex = "1.5.6" +scraper = "0.13.0" [dependencies.diesel] version = "1.4.8" diff --git a/src/cmds/pick.rs b/src/cmds/pick.rs index 98c9e4b..03cabc6 100644 --- a/src/cmds/pick.rs +++ b/src/cmds/pick.rs @@ -133,14 +133,15 @@ impl Command for PickCommand { }); let r = cache.get_question(fid).await; - if r.is_err() { - let e = r.err().ok_or(Error::NoneError)?; - eprintln!("{:?}", &e); - if let Error::FeatureError(_) | Error::NetworkError(_) = e { - Self::handler(m).await?; + + match r { + Ok(r) => println!("{}", r), + Err(e) => { + eprintln!("{:?}", e); + if let Error::FeatureError(_) | Error::NetworkError(_) = e { + Self::handler(m).await?; + } } - } else { - println!("{}", r?); } Ok(()) diff --git a/src/helper.rs b/src/helper.rs index 08aaea8..bd8a4de 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -92,168 +92,27 @@ mod filter { /// Render html to command-line mod html { - // use crate::Error; - use colored::{Color, Colorize}; - use escaper::decode_html; - use regex::Regex; - pub enum Token { - Plain(String), - Bold(String), - Sup(String), - Sub(String), - Font((String, Color)), - Eof(String), - } - + use scraper::Html; /// Html render plugin pub trait HTML { - fn ser(&self) -> Vec; fn render(&self) -> String; } - pub fn superscript(n: u8) -> String { - match n { - x if x >= 10 => format!("{}{}", superscript(n / 10), superscript(n % 10)), - 0 => "⁰".to_string(), - 1 => "¹".to_string(), - 2 => "²".to_string(), - 3 => "³".to_string(), - 4 => "⁴".to_string(), - 5 => "⁵".to_string(), - 6 => "⁶".to_string(), - 7 => "⁷".to_string(), - 8 => "⁸".to_string(), - 9 => "⁹".to_string(), - _ => n.to_string(), - } - } - - pub fn subscript(n: u8) -> String { - match n { - x if x >= 10 => format!("{}{}", subscript(n / 10), subscript(n % 10)), - 0 => "₀".to_string(), - 1 => "₁".to_string(), - 2 => "₂".to_string(), - 3 => "₃".to_string(), - 4 => "₄".to_string(), - 5 => "₅".to_string(), - 6 => "₆".to_string(), - 7 => "₇".to_string(), - 8 => "₈".to_string(), - 9 => "₉".to_string(), - _ => n.to_string(), - } - } impl HTML for String { - fn ser(&self) -> Vec { - // empty tags - let tks = { - let mut s = self.clone(); - // some problems (e.g. 1653) have ZWSPs. - s.retain(|x| x != '\u{200B}'); - s - }; - let res: Vec; - // styled - { - let mut ptr = 0; - let mut output = vec![]; - let mut bold = false; - let mut sup = false; - let mut sub = false; - let mut color: Option = None; - - // TODO: check how to make this `unwrap` more flexible.. - // - // or looks better. - // - // or do some handwrite matching. - let re_color = Regex::new(r#"color=['"]([^'"]+)"#).unwrap(); - for (i, e) in tks.chars().enumerate() { - match e { - '<' => { - if bold { - output.push(Token::Bold(tks[ptr..i].to_string())); - bold = false; - } else if sup { - output.push(Token::Sup(tks[ptr..i].to_string())); - sup = false; - } else if sub { - output.push(Token::Sub(tks[ptr..i].to_string())); - sub = false; - } else if color.is_some() { - output.push(Token::Font((tks[ptr..i].to_string(), color.unwrap()))); - color = None; - } else { - output.push(Token::Plain(tks[ptr..i].to_string())); - } - ptr = i; - } - '>' => { - match &tks[i - 1..i] { - "-" => continue, - _ => match &tks[(ptr + 1)..i] { - "b" | "strong" => bold = true, - "sup" => sup = true, - "sub" => sub = true, - s if s.starts_with("font") => { - color = re_color - .captures(s) - .and_then(|caps| caps.get(1)) - .and_then(|cap| cap.as_str().parse().ok()); - } - _ => {} - }, - } - ptr = i + 1; - } - _ => {} - } - } - output.push(Token::Eof(tks[ptr..tks.len()].to_string())); - res = output; - } - - res - } - fn render(&self) -> String { - let ts = self.ser(); - let mut tks: Vec = vec![]; - - for i in ts { - match i { - Token::Plain(s) => tks.push(s.normal().to_string()), - Token::Bold(s) => { - if s.contains("Example") { - let mut br = "-".repeat(50).dimmed().to_string(); - br.push_str("\n\n"); - tks.push(br); - } else if s.contains("Note") { - let mut br = "* ".repeat(25).dimmed().to_string(); - br.push_str("\n\n"); - tks.push(br); - } + let rep = self + .replace(r#""#, "") + .replace(r#""#, "^") + .replace(r#""#, "") + .replace(r#""#, "_"); + let frag = Html::parse_fragment(rep.as_str()); + + let res = frag + .root_element() + .text() + .fold(String::new(), |acc, e| acc + e); - tks.push(s.bold().to_string()); - } - Token::Sup(s) => tks.push(match s.parse::() { - Ok(n) => superscript(n), - _ => s, - }), - Token::Sub(s) => tks.push(match s.parse::() { - Ok(n) => subscript(n), - _ => s, - }), - Token::Font((s, color)) => tks.push(s.color(color).to_string()), - Token::Eof(s) => tks.push(s.normal().to_string()), - } - } - - // post replace - let tks = tks.join(""); - - decode_html(&tks).unwrap_or(tks) + res } } } From 0558081e605c11bfcb7f38c9eee9ca57cb251e1e Mon Sep 17 00:00:00 2001 From: David Date: Sun, 3 Jul 2022 13:43:46 +0800 Subject: [PATCH 06/11] feat: like vsc-leetcode-cli edit file has extra problem desc 1. edit add file include extra information 2. remove test file, and use memory all_cases 3. use scraper crate parser the desc html fragment --- src/cache/mod.rs | 15 ++++++++++++--- src/cache/models.rs | 36 +++++++++++++++++++++++++++++++++--- src/cmds/edit.rs | 17 +++++++++++++---- src/cmds/mod.rs | 4 +++- src/cmds/pick.rs | 2 +- src/helper.rs | 6 +++--- 6 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 2b57542..4e233e7 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -6,6 +6,7 @@ mod sql; use self::models::*; use self::schemas::{problems::dsl::*, tags::dsl::*}; use self::sql::*; +use crate::cmds::{CODE_END, CODE_START}; use crate::{cfg, err::Error, plugins::LeetCode}; use colored::Colorize; use diesel::prelude::*; @@ -26,7 +27,7 @@ pub enum Run { Submit, } -impl std::default::Default for Run { +impl Default for Run { fn default() -> Self { Run::Submit } @@ -37,7 +38,7 @@ impl std::default::Default for Run { pub struct Cache(pub LeetCode); impl Cache { - /// Ref to sqliteconnection + /// Ref to sqlite connection fn conn(&self) -> Result { Ok(conn(self.0.conf.storage.cache()?)) } @@ -269,6 +270,14 @@ impl Cache { File::open(code_path(&p, None)?)?.read_to_string(&mut code)?; + let begin = code.find(CODE_START).unwrap_or(0); + let end = code.find(CODE_END).unwrap_or(code.len()); + let code = if let Some(solution) = code.get(begin..end) { + solution.to_string() + } else { + code + }; + json.insert("lang", conf.code.lang.to_string()); json.insert("question_id", p.id.to_string()); json.insert("typed_code", code); @@ -311,7 +320,7 @@ impl Cache { async fn recur_verify(&self, rid: String) -> Result { use std::time::Duration; - trace!("Run veriy recursion..."); + trace!("Run verify recursion..."); std::thread::sleep(Duration::from_micros(3000)); let json: VerifyResult = self diff --git a/src/cache/models.rs b/src/cache/models.rs index bedc089..b338adf 100644 --- a/src/cache/models.rs +++ b/src/cache/models.rs @@ -30,6 +30,25 @@ pub struct Problem { pub desc: String, } +impl Problem { + fn display_level(&self) -> &str { + match self.level { + 1 => "Easy", + 2 => "Medium", + 3 => "Hard", + _ => "Unknown", + } + } + pub fn desc_comment(&self) -> String { + let mut res = String::new(); + res += format!("// Category: {}\n", self.category).as_str(); + res += format!("// Level: {}\n", self.display_level(),).as_str(); + res += format!("// Percent: {}%\n\n", self.percent).as_str(); + + res + "\n" + } +} + static DONE: &str = " ✔"; static ETC: &str = "..."; static LOCK: &str = "🔒"; @@ -124,9 +143,20 @@ pub struct Question { pub t_content: String, } -impl std::fmt::Display for Question { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self.content.render()) +impl Question { + pub fn desc(&self) -> String { + self.content.render() + } + + pub fn desc_comment(&self) -> String { + let desc = self.content.render(); + + let mut res = desc + .lines() + .fold("/*\n".to_string(), |acc, e| acc + " * " + e + "\n"); + res += " */\n"; + + res } } diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index fd70b35..1259411 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -22,6 +22,9 @@ use clap::{Arg, ArgMatches, Command as ClapCommand}; /// ``` pub struct EditCommand; +pub const CODE_START: &str = r#"// @lc code=start"#; +pub const CODE_END: &str = r#"// @lc code=end"#; + #[async_trait] impl Command for EditCommand { /// `edit` usage @@ -53,9 +56,10 @@ impl Command for EditCommand { let id: i32 = m.value_of("id").ok_or(Error::NoneError)?.parse()?; let cache = Cache::new()?; - let target = cache.get_problem(id)?; + let problem = cache.get_problem(id)?; let mut conf = cache.to_owned().0.conf; + let p_desc_comment = problem.desc_comment(); // condition language if m.contains_id("lang") { conf.code.lang = m.value_of("lang").ok_or(Error::NoneError)?.to_string(); @@ -63,10 +67,10 @@ impl Command for EditCommand { } let lang = conf.code.lang; - let path = crate::helper::code_path(&target, Some(lang.to_owned()))?; + let path = crate::helper::code_path(&problem, Some(lang.to_owned()))?; if !Path::new(&path).exists() { - let mut qr = serde_json::from_str(&target.desc); + let mut qr = serde_json::from_str(&problem.desc); if qr.is_err() { qr = Ok(cache.get_question(id).await?); } @@ -74,12 +78,17 @@ impl Command for EditCommand { let question: Question = qr?; let mut file_code = File::create(&path)?; + let question_desc = question.desc_comment() + "\n"; let mut flag = false; for d in question.defs.0 { if d.value == lang { flag = true; - file_code.write_all(d.code.to_string().as_bytes())?; + file_code.write_all(p_desc_comment.as_bytes())?; + file_code.write_all(question_desc.as_bytes())?; + file_code.write_all((CODE_START.to_string() + "\n").as_bytes())?; + file_code.write_all((d.code.to_string() + "\n").as_bytes())?; + file_code.write_all((CODE_END.to_string() + "\n").as_bytes())?; } } diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index cc00a0f..30e5841 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -12,7 +12,7 @@ //! ``` use crate::err::Error; use async_trait::async_trait; -use clap::{Command as ClapCommand, ArgMatches}; +use clap::{ArgMatches, Command as ClapCommand}; /// Abstract commands' trait. #[async_trait] @@ -38,3 +38,5 @@ pub use list::ListCommand; pub use pick::PickCommand; pub use stat::StatCommand; pub use test::TestCommand; + +pub use edit::{CODE_END, CODE_START}; diff --git a/src/cmds/pick.rs b/src/cmds/pick.rs index 03cabc6..d744f30 100644 --- a/src/cmds/pick.rs +++ b/src/cmds/pick.rs @@ -135,7 +135,7 @@ impl Command for PickCommand { let r = cache.get_question(fid).await; match r { - Ok(r) => println!("{}", r), + Ok(q) => println!("{}", q.desc()), Err(e) => { eprintln!("{:?}", e); if let Error::FeatureError(_) | Error::NetworkError(_) = e { diff --git a/src/helper.rs b/src/helper.rs index bd8a4de..eb2c73f 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -144,7 +144,7 @@ mod file { use crate::{cache::models::Problem, Error}; /// Generate code path by fid - pub fn code_path(target: &Problem, l: Option) -> Result { + pub fn code_path(problem: &Problem, l: Option) -> Result { let conf = crate::cfg::locate()?; let mut lang = conf.code.lang; if l.is_some() { @@ -158,8 +158,8 @@ mod file { suffix(&lang)?, ); - path = path.replace("${fid}", &target.fid.to_string()); - path = path.replace("${slug}", &target.slug.to_string()); + path = path.replace("${fid}", &problem.fid.to_string()); + path = path.replace("${slug}", &problem.slug.to_string()); Ok(path) } From 6a23c1852c5913858f83c06cd2055afb8188308e Mon Sep 17 00:00:00 2001 From: David Date: Sun, 3 Jul 2022 16:03:12 +0800 Subject: [PATCH 07/11] fix: solve issue 72 --- src/cmds/pick.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmds/pick.rs b/src/cmds/pick.rs index d744f30..cba7e5d 100644 --- a/src/cmds/pick.rs +++ b/src/cmds/pick.rs @@ -138,7 +138,7 @@ impl Command for PickCommand { Ok(q) => println!("{}", q.desc()), Err(e) => { eprintln!("{:?}", e); - if let Error::FeatureError(_) | Error::NetworkError(_) = e { + if let Error::NetworkError(_) = e { Self::handler(m).await?; } } From f47b7c8688a6637d67b7ee388442ba55341d58ce Mon Sep 17 00:00:00 2001 From: David Date: Mon, 4 Jul 2022 22:04:10 +0800 Subject: [PATCH 08/11] not use deprecated code --- src/cli.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 357adad..76f17a7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,7 +7,7 @@ use crate::{ err::Error, flag::{Debug, Flag}, }; -use clap::{crate_name, crate_version, App, AppSettings}; +use clap::{crate_name, crate_version}; /// This should be called before calling any cli method or printing any output. pub fn reset_signal_pipe_handler() { @@ -25,7 +25,7 @@ pub fn reset_signal_pipe_handler() { /// Get maches pub async fn main() -> Result<(), Error> { let _ = reset_signal_pipe_handler(); - let m = App::new(crate_name!()) + let m = clap::Command::new(crate_name!()) .version(crate_version!()) .about("May the Code be with You 👻") .subcommands(vec![ @@ -38,7 +38,7 @@ pub async fn main() -> Result<(), Error> { TestCommand::usage().display_order(7), ]) .arg(Debug::usage()) - .setting(AppSettings::ArgRequiredElseHelp) + .arg_required_else_help(true) .get_matches(); if m.contains_id("debug") { From 5d7a82e4ca7bfcb5769cad6b54e178c101e408e1 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 8 Jul 2022 20:06:53 +0800 Subject: [PATCH 09/11] revert: use generate the test file --- src/cache/mod.rs | 21 ++++++++++++++++++--- src/cli.rs | 2 +- src/cmds/edit.rs | 19 +++++++++++++++++++ src/helper.rs | 14 ++++++++++++-- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 4e233e7..2eec037 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -7,6 +7,7 @@ use self::models::*; use self::schemas::{problems::dsl::*, tags::dsl::*}; use self::sql::*; use crate::cmds::{CODE_END, CODE_START}; +use crate::helper::test_cases_path; use crate::{cfg, err::Error, plugins::LeetCode}; use colored::Colorize; use diesel::prelude::*; @@ -256,6 +257,16 @@ impl Cache { let mut json: HashMap<&'static str, String> = HashMap::new(); let mut code: String = "".to_string(); + let maybe_file_testcases: Option = test_cases_path(&p) + .map(|file_name| { + let mut tests = "".to_string(); + File::open(file_name) + .and_then(|mut file_descriptor| file_descriptor.read_to_string(&mut tests)) + .map(|_| Some(tests)) + .unwrap_or(None) + }) + .unwrap_or(None); + let maybe_all_testcases: Option = if d.all_cases.is_empty() { None } else { @@ -264,9 +275,13 @@ impl Cache { // Takes test cases using following priority // 1. cli parameter - // 2. test cases from problem desc all test cases - // 3. sample test case from the task - let test_case = test_case.or(maybe_all_testcases).unwrap_or(d.case); + // 2. if test cases file exist, use the file test cases(user can edit it) + // 3. test cases from problem desc all test cases + // 4. sample test case from the task + let test_case = test_case + .or(maybe_file_testcases) + .or(maybe_all_testcases) + .unwrap_or(d.case); File::open(code_path(&p, None)?)?.read_to_string(&mut code)?; diff --git a/src/cli.rs b/src/cli.rs index 76f17a7..3ec98bf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -24,7 +24,7 @@ pub fn reset_signal_pipe_handler() { /// Get maches pub async fn main() -> Result<(), Error> { - let _ = reset_signal_pipe_handler(); + reset_signal_pipe_handler(); let m = clap::Command::new(crate_name!()) .version(crate_version!()) .about("May the Code be with You 👻") diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index 1259411..1acff43 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -45,6 +45,13 @@ impl Command for EditCommand { .required(true) .help("question id"), ) + .arg( + Arg::with_name("test") + .long("test") + .short('t') + .required(false) + .help("write test file"), + ) } /// `edit` handler @@ -59,6 +66,8 @@ impl Command for EditCommand { let problem = cache.get_problem(id)?; let mut conf = cache.to_owned().0.conf; + let test_flag = m.contains_id("test"); + let p_desc_comment = problem.desc_comment(); // condition language if m.contains_id("lang") { @@ -80,6 +89,9 @@ impl Command for EditCommand { let mut file_code = File::create(&path)?; let question_desc = question.desc_comment() + "\n"; + let test_path = crate::helper::test_cases_path(&problem)?; + let mut file_tests = File::create(&test_path)?; + let mut flag = false; for d in question.defs.0 { if d.value == lang { @@ -89,12 +101,19 @@ impl Command for EditCommand { file_code.write_all((CODE_START.to_string() + "\n").as_bytes())?; file_code.write_all((d.code.to_string() + "\n").as_bytes())?; file_code.write_all((CODE_END.to_string() + "\n").as_bytes())?; + + if test_flag { + file_tests.write_all(question.all_cases.as_bytes())?; + } } } // if language is not found in the list of supported languges clean up files if !flag { std::fs::remove_file(&path)?; + if test_flag { + std::fs::remove_file(&test_path)?; + } return Err(crate::Error::FeatureError(format!( "This question doesn't support {}, please try another", &lang diff --git a/src/helper.rs b/src/helper.rs index eb2c73f..c4a70a3 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,6 +1,6 @@ //! A set of helper traits pub use self::digit::Digit; -pub use self::file::{code_path, load_script}; +pub use self::file::{code_path, load_script, test_cases_path}; pub use self::filter::{filter, squash}; pub use self::html::HTML; @@ -143,8 +143,18 @@ mod file { use crate::{cache::models::Problem, Error}; + /// Generate test cases path by fid + pub fn test_cases_path(problem: &Problem) -> Result { + let conf = crate::cfg::locate()?; + let mut path = format!("{}/{}.tests.dat", conf.storage.code()?, conf.code.pick); + + path = path.replace("${fid}", &problem.fid.to_string()); + path = path.replace("${slug}", &problem.slug.to_string()); + Ok(path) + } + /// Generate code path by fid - pub fn code_path(problem: &Problem, l: Option) -> Result { + pub fn code_path(problem: &Problem, l: Option) -> Result { let conf = crate::cfg::locate()?; let mut lang = conf.code.lang; if l.is_some() { From 371548c2cec94f8de34aea1fdba230612f239f01 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 8 Jul 2022 20:07:20 +0800 Subject: [PATCH 10/11] feat: storage config path use the absolutely dir, not in the root config --- src/cfg.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cfg.rs b/src/cfg.rs index 410088d..ff91601 100644 --- a/src/cfg.rs +++ b/src/cfg.rs @@ -9,7 +9,7 @@ use crate::Error; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs, path::PathBuf}; -const DEFAULT_CONFIG: &str = r#" +const DEFAULT_CONFIG: &str = r##" # usually you don't wanna change those [sys] categories = [ @@ -65,11 +65,12 @@ csrf = "" session = "" [storage] -cache = "Problems" -code = "code" root = "~/.leetcode" +cache = "Problems" scripts = "scripts" -"#; +# absolutely path for the code, other use root as parent dir +code = "code" +"##; /// Sync with `~/.leetcode/config.toml` #[derive(Clone, Debug, Deserialize, Serialize)] @@ -169,10 +170,9 @@ impl Storage { /// get code path pub fn code(&self) -> Result { - let root = &self.root()?; - let p = PathBuf::from(root).join(&self.code); + let p = PathBuf::from(&self.code); if !PathBuf::from(&p).exists() { - std::fs::create_dir(&p)? + fs::create_dir(&p)? } Ok(p.to_string_lossy().to_string()) From 8fed4a6fb971b0fa788f5ec4ca91d9b8b5c430cf Mon Sep 17 00:00:00 2001 From: David Date: Fri, 8 Jul 2022 21:16:56 +0800 Subject: [PATCH 11/11] fix: fmt fix --- src/cache/parser.rs | 37 ++++++++++++++++++++++--------------- src/cmds/exec.rs | 4 ++-- src/cmds/stat.rs | 2 +- src/plugins/leetcode.rs | 26 +++++++++++++++++++++----- 4 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/cache/parser.rs b/src/cache/parser.rs index fb9ad2b..dbbc87e 100644 --- a/src/cache/parser.rs +++ b/src/cache/parser.rs @@ -50,10 +50,11 @@ pub fn desc(q: &mut Question, v: Value) -> Option { stats: serde_json::from_str(o.get("stats")?.as_str()?).ok()?, defs: serde_json::from_str(o.get("codeDefinition")?.as_str()?).ok()?, case: o.get("sampleTestCase")?.as_str()?.to_string(), - all_cases: o.get("exampleTestcases") - .unwrap_or(o.get("sampleTestCase")?) // soft fail to the sampleTestCase - .as_str()? - .to_string(), + all_cases: o + .get("exampleTestcases") + .unwrap_or(o.get("sampleTestCase")?) // soft fail to the sampleTestCase + .as_str()? + .to_string(), metadata: serde_json::from_str(o.get("metaData")?.as_str()?).ok()?, test: o.get("enableRunCode")?.as_bool()?, t_content: o @@ -85,29 +86,35 @@ pub fn tags(v: Value) -> Option> { Some(res) } -/// daily parser +/// daily parser pub fn daily(v: Value) -> Option { trace!("Parse daily..."); v.as_object()? - .get("data")?.as_object()? - .get("activeDailyCodingChallengeQuestion")?.as_object()? - .get("question")?.as_object()? - .get("questionFrontendId")?.as_str()? - .parse().ok() + .get("data")? + .as_object()? + .get("activeDailyCodingChallengeQuestion")? + .as_object()? + .get("question")? + .as_object()? + .get("questionFrontendId")? + .as_str()? + .parse() + .ok() } /// user parser -pub fn user(v: Value) -> Option> { +pub fn user(v: Value) -> Option> { // None => error while parsing // Some(None) => User not found // Some("...") => username - let user = v.as_object()?.get("data")? - .as_object()?.get("user")?; - if *user == Value::Null { return Some(None) } + let user = v.as_object()?.get("data")?.as_object()?.get("user")?; + if *user == Value::Null { + return Some(None); + } let user = user.as_object()?; Some(Some(( user.get("username")?.as_str()?.to_owned(), - user.get("isCurrentUserPremium")?.as_bool()? + user.get("isCurrentUserPremium")?.as_bool()?, ))) } diff --git a/src/cmds/exec.rs b/src/cmds/exec.rs index e660f58..85b6d1a 100644 --- a/src/cmds/exec.rs +++ b/src/cmds/exec.rs @@ -1,8 +1,8 @@ //! Exec command -use crate::Error; use super::Command; +use crate::Error; use async_trait::async_trait; -use clap::{Command as ClapCommand, ArgMatches, Arg}; +use clap::{Arg, ArgMatches, Command as ClapCommand}; /// Abstract Exec Command /// diff --git a/src/cmds/stat.rs b/src/cmds/stat.rs index 661ca98..147392b 100644 --- a/src/cmds/stat.rs +++ b/src/cmds/stat.rs @@ -1,7 +1,7 @@ //! status command use super::Command; use async_trait::async_trait; -use clap::{Command as ClapCommand, ArgMatches}; +use clap::{ArgMatches, Command as ClapCommand}; use colored::Colorize; /// Abstract statues command diff --git a/src/plugins/leetcode.rs b/src/plugins/leetcode.rs index 4394532..e06627f 100644 --- a/src/plugins/leetcode.rs +++ b/src/plugins/leetcode.rs @@ -72,7 +72,8 @@ impl LeetCode { .conf .sys .urls - .get("problems").ok_or(Error::NoneError)? + .get("problems") + .ok_or(Error::NoneError)? .replace("$category", category); Req { @@ -110,7 +111,9 @@ impl LeetCode { Req { default_headers: self.default_headers, - refer: Some((self.conf.sys.urls.get("tag").ok_or(Error::NoneError)?).replace("$slug", slug)), + refer: Some( + (self.conf.sys.urls.get("tag").ok_or(Error::NoneError)?).replace("$slug", slug), + ), info: false, json: Some(json), mode: Mode::Post, @@ -133,7 +136,8 @@ impl LeetCode { username isCurrentUserPremium } - }".to_owned() + }" + .to_owned(), ); Req { @@ -185,7 +189,13 @@ impl LeetCode { /// Get specific problem detail pub async fn get_question_detail(self, slug: &str) -> Result { trace!("Requesting {} detail...", &slug); - let refer = self.conf.sys.urls.get("problems").ok_or(Error::NoneError)?.replace("$slug", slug); + let refer = self + .conf + .sys + .urls + .get("problems") + .ok_or(Error::NoneError)? + .replace("$slug", slug); let mut json: Json = HashMap::new(); json.insert( "query", @@ -245,7 +255,13 @@ impl LeetCode { /// Get the result of submission / testing pub async fn verify_result(self, id: String) -> Result { trace!("Verifying result..."); - let url = self.conf.sys.urls.get("verify").ok_or(Error::NoneError)?.replace("$id", &id); + let url = self + .conf + .sys + .urls + .get("verify") + .ok_or(Error::NoneError)? + .replace("$id", &id); Req { default_headers: self.default_headers, refer: None,