diff --git a/.githooks/pre-push b/.githooks/pre-push index cad7d4e..14d95d7 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -2,9 +2,23 @@ echo "Running pre-push checks..." +# Check code formatting +echo "Checking code formatting..." +if ! cargo +nightly fmt --check; then + echo "❌ Code formatting check failed. Please run 'cargo +nightly fmt' and commit the changes." + exit 1 +fi + +# Lint code +echo "Running clippy..." +if ! cargo clippy --all-features -- -D warnings; then + echo "❌ Clippy linting failed. Please fix the warnings and commit the changes." + exit 1 +fi + # Run tests echo "Running cargo test..." -if ! cargo test; then +if ! cargo test -- --test-threads=1; then echo "❌ Tests failed. Push aborted." exit 1 fi diff --git a/README.md b/README.md index bf43339..5c51c5d 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ The configuration file is located at: ```sh ~/.config/leetcode-cli/config.toml ``` + and should look like this: ```toml @@ -84,6 +85,8 @@ Login to LeetCode and obtain the csrftoken from the cookie value. ## Usage +### Global Commands + For more details on available commands, run: ```sh @@ -96,6 +99,44 @@ For a specific command, run: leetcode_cli --help ``` +### Local Configuration + +When you start a problem using `leetcode_cli start --id `, the tool automatically creates a `.leetcode-cli` file in the problem directory. This file contains: + +```toml +problem_id = 42 +problem_name = "trapping_rain_water" +language = "Rust" +``` + +### Working with Problems + +Once you're in a problem directory (one that contains a `.leetcode-cli` file), you can run commands without specifying the problem ID: + +```sh +# Start a problem (creates the local config) +leetcode_cli start --id 42 --lang rust + +# Navigate to the problem directory +cd ~/leetcode/42_trapping_rain_water + +# Test your solution (automatically detects problem ID and main file) +leetcode_cli test + +# Submit your solution (automatically detects problem ID and main file) +leetcode_cli submit + +# You can still override the defaults if needed +leetcode_cli test --id 42 --file src/custom_solution.rs +``` + +#### Supported Commands + +- `info --id `: Get problem information (ID required) +- `start --id [--lang ]`: Start working on a problem (creates local config) +- `test [--id ] [--file ]`: Test your solution (uses local config if available) +- `submit [--id ] [--file ]`: Submit your solution (uses local config if available) + ## Contributing Contributions are welcome! Feel free to open issues or submit pull requests to improve the tool. diff --git a/src/cli.rs b/src/cli.rs index 0210fd6..da0b77a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -24,15 +24,15 @@ pub enum Commands { }, Test { #[arg(short = 'i', long)] - id: u32, + id: Option, #[arg(short = 'p', long = "file")] - path_to_file: String, + path_to_file: Option, }, Submit { #[arg(short = 'i', long)] - id: u32, + id: Option, #[arg(short = 'p', long = "file")] - path_to_file: String, + path_to_file: Option, }, } diff --git a/src/leetcode_api_runner.rs b/src/leetcode_api_runner.rs index e6a5534..4df33f1 100644 --- a/src/leetcode_api_runner.rs +++ b/src/leetcode_api_runner.rs @@ -16,6 +16,7 @@ use nanohtml2text::html2text; use crate::{ config::RuntimeConfigSetup, + local_config::LocalConfig, readme_parser::LeetcodeReadmeParser, test_generator::TestGenerator, utils::*, @@ -99,6 +100,11 @@ impl LeetcodeApiRunner { write_readme(&pb_dir, id, &pb_name, &md_desc)?; write_to_file(&src_dir, &get_file_name(&lang), &file_content)?; + // Create local config file + let local_config = + LocalConfig::new(id, pb_name.clone(), language_to_string(&lang)); + local_config.write_to_dir(&pb_dir)?; + let success_message = format!( "{}: {} created at \n{}\nin {}.", id, @@ -154,15 +160,14 @@ impl LeetcodeApiRunner { pub async fn test_response( &self, id: u32, path_to_file: &String, - ) -> io::Result<()> { + ) -> io::Result { let problem_info = self.api.set_problem_by_id(id).await?; let file_content = std::fs::read_to_string(path_to_file) .expect("Unable to read the file"); let language = get_language_from_extension(path_to_file); let test_res = problem_info.send_test(language, &file_content).await?; - println!("Test response for problem {id}: {test_res:?}"); - Ok(()) + Ok(format!("Test response for problem {id}: \n{test_res:#?}")) } pub async fn submit_response( diff --git a/src/lib.rs b/src/lib.rs index d930f59..df130d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod cli; pub mod code_signature; pub mod config; pub mod leetcode_api_runner; +pub mod local_config; pub mod readme_parser; pub mod test_generator; pub mod utils; @@ -12,3 +13,4 @@ pub use cli::{ }; pub use config::RuntimeConfigSetup; pub use leetcode_api_runner::LeetcodeApiRunner; +pub use local_config::LocalConfig; diff --git a/src/local_config.rs b/src/local_config.rs new file mode 100644 index 0000000..e570e3b --- /dev/null +++ b/src/local_config.rs @@ -0,0 +1,168 @@ +use std::{ + fs, + io, + path::Path, +}; + +use serde::{ + Deserialize, + Serialize, +}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct LocalConfig { + pub problem_id: u32, + pub problem_name: String, + pub language: String, +} + +impl LocalConfig { + pub fn new( + problem_id: u32, problem_name: String, language: String, + ) -> Self { + Self { problem_id, problem_name, language } + } + + /// Find and read local config from current directory or parent directories + pub fn find_and_read() -> io::Result> { + let mut current_dir = std::env::current_dir()?; + + loop { + let config_path = current_dir.join(".leetcode-cli"); + if config_path.exists() { + let content = fs::read_to_string(&config_path)?; + let config: LocalConfig = + toml::from_str(&content).map_err(|e| { + io::Error::new(io::ErrorKind::InvalidData, e) + })?; + return Ok(Some(config)); + } + + if !current_dir.pop() { + break; + } + } + + Ok(None) + } + + /// Write local config to specified directory + pub fn write_to_dir(&self, dir: &Path) -> io::Result<()> { + let config_path = dir.join(".leetcode-cli"); + let content = toml::to_string(self) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + fs::write(config_path, content) + } + + /// Read local config from specified file path + pub fn read_from_path(path: &Path) -> io::Result { + let content = fs::read_to_string(path)?; + toml::from_str(&content) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) + } + + /// Get the main source file name based on language + pub fn get_main_file(&self) -> String { + match self.language.to_lowercase().as_str() { + "rust" => "main.rs".to_string(), + "python" | "python3" => "main.py".to_string(), + "javascript" => "main.js".to_string(), + "typescript" => "main.ts".to_string(), + "go" => "main.go".to_string(), + "java" => "Main.java".to_string(), + "c++" => "main.cpp".to_string(), + "c" => "main.c".to_string(), + _ => "main.txt".to_string(), + } + } + + /// Resolve problem ID and file path from CLI args or local config + pub fn resolve_problem_params( + id: Option, path_to_file: Option, + ) -> io::Result<(u32, String)> { + match (id, &path_to_file) { + (Some(id), Some(path)) => Ok((id, path.clone())), + _ => { + // Try to find local config + match Self::find_and_read()? { + Some(config) => { + let problem_id = id.unwrap_or(config.problem_id); + let file_path = path_to_file.unwrap_or_else(|| { + format!("src/{}", config.get_main_file()) + }); + Ok((problem_id, file_path)) + }, + None => { + if id.is_none() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "No problem ID provided and no .leetcode-cli \ + config found. Either provide --id or run \ + from a problem directory", + )); + } + if path_to_file.is_none() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "No file path provided", + )); + } + // If we get here, both id and path_to_file must be Some + match (id, path_to_file) { + (Some(id), Some(path)) => Ok((id, path)), + _ => Err(io::Error::other( + "Unexpected error: id or path_to_file missing \ + after checks", + )), + } + }, + } + }, + } + } +} + +#[cfg(test)] +mod tests { + use tempfile::TempDir; + + use super::*; + + #[test] + fn test_local_config_creation() { + let config = + LocalConfig::new(1, "two_sum".to_string(), "Rust".to_string()); + + assert_eq!(config.problem_id, 1); + assert_eq!(config.problem_name, "two_sum"); + assert_eq!(config.language, "Rust"); + } + + #[test] + fn test_write_and_read_config() { + let temp_dir = TempDir::new().unwrap(); + let config = + LocalConfig::new(1, "two_sum".to_string(), "Rust".to_string()); + + config.write_to_dir(temp_dir.path()).unwrap(); + + let config_path = temp_dir.path().join(".leetcode-cli"); + assert!(config_path.exists()); + + let read_config = LocalConfig::read_from_path(&config_path).unwrap(); + assert_eq!(read_config.problem_id, 1); + assert_eq!(read_config.problem_name, "two_sum"); + assert_eq!(read_config.language, "Rust"); + } + + #[test] + fn test_get_main_file() { + let config = + LocalConfig::new(1, "two_sum".to_string(), "Rust".to_string()); + assert_eq!(config.get_main_file(), "main.rs"); + + let config = + LocalConfig::new(1, "two_sum".to_string(), "Python".to_string()); + assert_eq!(config.get_main_file(), "main.py"); + } +} diff --git a/src/main.rs b/src/main.rs index ff4f6a3..2aa38c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use leetcode_cli::{ Cli, Commands, LeetcodeApiRunner, + LocalConfig, RuntimeConfigSetup, }; @@ -57,30 +58,41 @@ async fn main() -> Result<(), Box> { let start_problem = api_runner.start_problem(*id, lang).await; stop_and_clear_spinner(spin); match start_problem { - Ok((success_message, _, warning)) => { + Ok((success_message, pb_dir, warning)) => { if let Some(warning) = warning { eprintln!("{warning}"); } println!("{success_message}"); println!("\nHappy coding :)"); + println!( + "\n(ps: to use local config feature, you should \ncd \ + {}\n;)", + pb_dir.display() + ); }, Err(e) => eprintln!("Error starting problem: {e}"), } }, Commands::Test { id, path_to_file } => { + let (problem_id, file_path) = + LocalConfig::resolve_problem_params(*id, path_to_file.clone())?; + let spin = spin_the_spinner("Running tests..."); let test_result = - api_runner.test_response(*id, &path_to_file.clone()).await; + api_runner.test_response(problem_id, &file_path).await; stop_and_clear_spinner(spin); match test_result { - Ok(_) => println!("Test result"), - Err(e) => eprintln!("Error running tests: {e}"), + Ok(message) => println!("{message}"), + Err(e) => eprintln!("Error running tests:\n{e}"), } }, Commands::Submit { id, path_to_file } => { + let (problem_id, file_path) = + LocalConfig::resolve_problem_params(*id, path_to_file.clone())?; + let spin = spin_the_spinner("Submitting solution..."); let submit_result = - api_runner.submit_response(*id, &path_to_file.clone()).await; + api_runner.submit_response(problem_id, &file_path).await; stop_and_clear_spinner(spin); match submit_result { Ok(_) => println!("Submit result"), diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index 730b05a..9b551ab 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -36,8 +36,22 @@ fn test_cli_test_command() { match cli.command { Commands::Test { id, path_to_file } => { - assert_eq!(id, 1); - assert_eq!(path_to_file, "main.rs"); + assert_eq!(id, Some(1)); + assert_eq!(path_to_file, Some("main.rs".to_string())); + }, + _ => panic!("Expected Test command"), + } +} + +#[test] +fn test_cli_test_command_no_args() { + let args = vec!["leetcode_cli", "test"]; + let cli = Cli::try_parse_from(args).unwrap(); + + match cli.command { + Commands::Test { id, path_to_file } => { + assert_eq!(id, None); + assert_eq!(path_to_file, None); }, _ => panic!("Expected Test command"), } @@ -51,8 +65,22 @@ fn test_cli_submit_command() { match cli.command { Commands::Submit { id, path_to_file } => { - assert_eq!(id, 1); - assert_eq!(path_to_file, "solution.py"); + assert_eq!(id, Some(1)); + assert_eq!(path_to_file, Some("solution.py".to_string())); + }, + _ => panic!("Expected Submit command"), + } +} + +#[test] +fn test_cli_submit_command_no_args() { + let args = vec!["leetcode_cli", "submit"]; + let cli = Cli::try_parse_from(args).unwrap(); + + match cli.command { + Commands::Submit { id, path_to_file } => { + assert_eq!(id, None); + assert_eq!(path_to_file, None); }, _ => panic!("Expected Submit command"), } diff --git a/tests/local_config_tests.rs b/tests/local_config_tests.rs new file mode 100644 index 0000000..dbccff6 --- /dev/null +++ b/tests/local_config_tests.rs @@ -0,0 +1,202 @@ +use leetcode_cli::local_config::LocalConfig; +use tempfile::TempDir; + +#[test] +fn test_local_config_creation() { + let config = LocalConfig::new( + 42, + "valid_parentheses".to_string(), + "Rust".to_string(), + ); + + assert_eq!(config.problem_id, 42); + assert_eq!(config.problem_name, "valid_parentheses"); + assert_eq!(config.language, "Rust"); +} + +#[test] +fn test_write_and_read_config() { + let temp_dir = TempDir::new().unwrap(); + let config = + LocalConfig::new(1, "two_sum".to_string(), "Python".to_string()); + + // Write config + config.write_to_dir(temp_dir.path()).unwrap(); + + // Verify file exists + let config_path = temp_dir.path().join(".leetcode-cli"); + assert!(config_path.exists()); + + // Read config back + let read_config = LocalConfig::read_from_path(&config_path).unwrap(); + assert_eq!(read_config.problem_id, 1); + assert_eq!(read_config.problem_name, "two_sum"); + assert_eq!(read_config.language, "Python"); +} + +#[test] +fn test_get_main_file_rust() { + let config = LocalConfig::new(1, "two_sum".to_string(), "Rust".to_string()); + assert_eq!(config.get_main_file(), "main.rs"); +} + +#[test] +fn test_get_main_file_python() { + let config = + LocalConfig::new(1, "two_sum".to_string(), "Python".to_string()); + assert_eq!(config.get_main_file(), "main.py"); +} + +#[test] +fn test_get_main_file_javascript() { + let config = + LocalConfig::new(1, "two_sum".to_string(), "JavaScript".to_string()); + assert_eq!(config.get_main_file(), "main.js"); +} + +#[test] +fn test_find_config_not_found() { + // Change to temp dir where no config exists + let temp_dir = TempDir::new().unwrap(); + let original_dir = std::env::current_dir().unwrap(); + + std::env::set_current_dir(temp_dir.path()).unwrap(); + + let result = LocalConfig::find_and_read().unwrap(); + assert!(result.is_none()); + + // Restore original directory + std::env::set_current_dir(original_dir).unwrap(); +} + +#[test] +fn test_find_config_in_current_dir() { + let temp_dir = TempDir::new().unwrap(); + let original_dir = std::env::current_dir().unwrap(); + + // Create config in temp dir + let config = + LocalConfig::new(123, "test_problem".to_string(), "Go".to_string()); + config.write_to_dir(temp_dir.path()).unwrap(); + + // Change to temp dir + std::env::set_current_dir(temp_dir.path()).unwrap(); + + // Find config + let found_config = LocalConfig::find_and_read().unwrap().unwrap(); + assert_eq!(found_config.problem_id, 123); + assert_eq!(found_config.problem_name, "test_problem"); + assert_eq!(found_config.language, "Go"); + + // Restore original directory + std::env::set_current_dir(original_dir).unwrap(); +} + +#[test] +fn test_find_config_in_parent_dir() { + let temp_dir = TempDir::new().unwrap(); + let original_dir = std::env::current_dir().unwrap(); + + // Create config in temp dir + let config = LocalConfig::new( + 456, + "parent_test".to_string(), + "TypeScript".to_string(), + ); + config.write_to_dir(temp_dir.path()).unwrap(); + + // Create subdirectory + let sub_dir = temp_dir.path().join("subdir"); + std::fs::create_dir(&sub_dir).unwrap(); + + // Change to subdirectory + std::env::set_current_dir(&sub_dir).unwrap(); + + // Find config (should find it in parent) + let found_config = LocalConfig::find_and_read().unwrap().unwrap(); + assert_eq!(found_config.problem_id, 456); + assert_eq!(found_config.problem_name, "parent_test"); + assert_eq!(found_config.language, "TypeScript"); + + // Restore original directory + std::env::set_current_dir(original_dir).unwrap(); +} + +#[test] +fn test_get_main_file_case_insensitive() { + // Test uppercase + let config = LocalConfig::new(1, "test".to_string(), "RUST".to_string()); + assert_eq!(config.get_main_file(), "main.rs"); + + // Test mixed case + let config = LocalConfig::new(1, "test".to_string(), "PyThOn".to_string()); + assert_eq!(config.get_main_file(), "main.py"); + + // Test lowercase + let config = + LocalConfig::new(1, "test".to_string(), "typescript".to_string()); + assert_eq!(config.get_main_file(), "main.ts"); + + // Test C++ case insensitive (special case with symbols) + let config = LocalConfig::new(1, "test".to_string(), "C++".to_string()); + assert_eq!(config.get_main_file(), "main.cpp"); +} + +#[test] +fn test_resolve_problem_params_with_both_args() { + let result = LocalConfig::resolve_problem_params( + Some(42), + Some("custom.rs".to_string()), + ); + assert!(result.is_ok()); + let (id, path) = result.unwrap(); + assert_eq!(id, 42); + assert_eq!(path, "custom.rs"); +} + +#[test] +fn test_resolve_problem_params_no_args_no_config() { + let temp_dir = TempDir::new().unwrap(); + let original_dir = std::env::current_dir().unwrap(); + + // Change to temp dir where no config exists + std::env::set_current_dir(temp_dir.path()).unwrap(); + + let result = LocalConfig::resolve_problem_params(None, None); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("No problem ID provided")); + + // Restore original directory + std::env::set_current_dir(original_dir).unwrap(); +} + +#[test] +fn test_resolve_problem_params_with_config() { + let temp_dir = TempDir::new().unwrap(); + let original_dir = std::env::current_dir().unwrap(); + + // Create config in temp dir with Rust language + let config = + LocalConfig::new(123, "test_problem".to_string(), "Rust".to_string()); + config.write_to_dir(temp_dir.path()).unwrap(); + + // Change to temp dir + std::env::set_current_dir(temp_dir.path()).unwrap(); + + // Test resolution without args (should use config) + let result = LocalConfig::resolve_problem_params(None, None); + assert!(result.is_ok()); + let (id, path) = result.unwrap(); + assert_eq!(id, 123); + assert_eq!(path, "src/main.rs"); + + // Test resolution with partial args (should mix CLI and config) + let result = LocalConfig::resolve_problem_params(Some(456), None); + assert!(result.is_ok()); + let (id, path) = result.unwrap(); + assert_eq!(id, 456); // From CLI + assert_eq!(path, "src/main.rs"); // From config + + // Restore original directory + std::env::set_current_dir(original_dir).unwrap(); +}