Skip to content

Commit 7659e86

Browse files
authored
feat(config): add local problem config with auto-detection (#25)
* feat(config): add local problem config with auto-detection Signed-off-by: dfayd <78728332+dfayd0@users.noreply.github.com> * fix(config): apply PR review suggestions for error handling Signed-off-by: dfayd <78728332+dfayd0@users.noreply.github.com> * refactor(config): use io::Error::other for cleaner error creation Signed-off-by: dfayd <78728332+dfayd0@users.noreply.github.com> --------- Signed-off-by: dfayd <78728332+dfayd0@users.noreply.github.com>
1 parent 564233a commit 7659e86

9 files changed

Lines changed: 489 additions & 17 deletions

File tree

.githooks/pre-push

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,23 @@
22

33
echo "Running pre-push checks..."
44

5+
# Check code formatting
6+
echo "Checking code formatting..."
7+
if ! cargo +nightly fmt --check; then
8+
echo "❌ Code formatting check failed. Please run 'cargo +nightly fmt' and commit the changes."
9+
exit 1
10+
fi
11+
12+
# Lint code
13+
echo "Running clippy..."
14+
if ! cargo clippy --all-features -- -D warnings; then
15+
echo "❌ Clippy linting failed. Please fix the warnings and commit the changes."
16+
exit 1
17+
fi
18+
519
# Run tests
620
echo "Running cargo test..."
7-
if ! cargo test; then
21+
if ! cargo test -- --test-threads=1; then
822
echo "❌ Tests failed. Push aborted."
923
exit 1
1024
fi

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ The configuration file is located at:
6262
```sh
6363
~/.config/leetcode-cli/config.toml
6464
```
65+
6566
and should look like this:
6667

6768
```toml
@@ -84,6 +85,8 @@ Login to LeetCode and obtain the csrftoken from the cookie value.
8485

8586
## Usage
8687

88+
### Global Commands
89+
8790
For more details on available commands, run:
8891

8992
```sh
@@ -96,6 +99,44 @@ For a specific command, run:
9699
leetcode_cli <command> --help
97100
```
98101

102+
### Local Configuration
103+
104+
When you start a problem using `leetcode_cli start --id <problem_id>`, the tool automatically creates a `.leetcode-cli` file in the problem directory. This file contains:
105+
106+
```toml
107+
problem_id = 42
108+
problem_name = "trapping_rain_water"
109+
language = "Rust"
110+
```
111+
112+
### Working with Problems
113+
114+
Once you're in a problem directory (one that contains a `.leetcode-cli` file), you can run commands without specifying the problem ID:
115+
116+
```sh
117+
# Start a problem (creates the local config)
118+
leetcode_cli start --id 42 --lang rust
119+
120+
# Navigate to the problem directory
121+
cd ~/leetcode/42_trapping_rain_water
122+
123+
# Test your solution (automatically detects problem ID and main file)
124+
leetcode_cli test
125+
126+
# Submit your solution (automatically detects problem ID and main file)
127+
leetcode_cli submit
128+
129+
# You can still override the defaults if needed
130+
leetcode_cli test --id 42 --file src/custom_solution.rs
131+
```
132+
133+
#### Supported Commands
134+
135+
- `info --id <id>`: Get problem information (ID required)
136+
- `start --id <id> [--lang <language>]`: Start working on a problem (creates local config)
137+
- `test [--id <id>] [--file <path>]`: Test your solution (uses local config if available)
138+
- `submit [--id <id>] [--file <path>]`: Submit your solution (uses local config if available)
139+
99140
## Contributing
100141

101142
Contributions are welcome! Feel free to open issues or submit pull requests to improve the tool.

src/cli.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ pub enum Commands {
2424
},
2525
Test {
2626
#[arg(short = 'i', long)]
27-
id: u32,
27+
id: Option<u32>,
2828
#[arg(short = 'p', long = "file")]
29-
path_to_file: String,
29+
path_to_file: Option<String>,
3030
},
3131
Submit {
3232
#[arg(short = 'i', long)]
33-
id: u32,
33+
id: Option<u32>,
3434

3535
#[arg(short = 'p', long = "file")]
36-
path_to_file: String,
36+
path_to_file: Option<String>,
3737
},
3838
}

src/leetcode_api_runner.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use nanohtml2text::html2text;
1616

1717
use crate::{
1818
config::RuntimeConfigSetup,
19+
local_config::LocalConfig,
1920
readme_parser::LeetcodeReadmeParser,
2021
test_generator::TestGenerator,
2122
utils::*,
@@ -99,6 +100,11 @@ impl LeetcodeApiRunner {
99100
write_readme(&pb_dir, id, &pb_name, &md_desc)?;
100101
write_to_file(&src_dir, &get_file_name(&lang), &file_content)?;
101102

103+
// Create local config file
104+
let local_config =
105+
LocalConfig::new(id, pb_name.clone(), language_to_string(&lang));
106+
local_config.write_to_dir(&pb_dir)?;
107+
102108
let success_message = format!(
103109
"{}: {} created at \n{}\nin {}.",
104110
id,
@@ -154,15 +160,14 @@ impl LeetcodeApiRunner {
154160

155161
pub async fn test_response(
156162
&self, id: u32, path_to_file: &String,
157-
) -> io::Result<()> {
163+
) -> io::Result<String> {
158164
let problem_info = self.api.set_problem_by_id(id).await?;
159165
let file_content = std::fs::read_to_string(path_to_file)
160166
.expect("Unable to read the file");
161167
let language = get_language_from_extension(path_to_file);
162168

163169
let test_res = problem_info.send_test(language, &file_content).await?;
164-
println!("Test response for problem {id}: {test_res:?}");
165-
Ok(())
170+
Ok(format!("Test response for problem {id}: \n{test_res:#?}"))
166171
}
167172

168173
pub async fn submit_response(

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod cli;
22
pub mod code_signature;
33
pub mod config;
44
pub mod leetcode_api_runner;
5+
pub mod local_config;
56
pub mod readme_parser;
67
pub mod test_generator;
78
pub mod utils;
@@ -12,3 +13,4 @@ pub use cli::{
1213
};
1314
pub use config::RuntimeConfigSetup;
1415
pub use leetcode_api_runner::LeetcodeApiRunner;
16+
pub use local_config::LocalConfig;

src/local_config.rs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
use std::{
2+
fs,
3+
io,
4+
path::Path,
5+
};
6+
7+
use serde::{
8+
Deserialize,
9+
Serialize,
10+
};
11+
12+
#[derive(Deserialize, Serialize, Debug, Clone)]
13+
pub struct LocalConfig {
14+
pub problem_id: u32,
15+
pub problem_name: String,
16+
pub language: String,
17+
}
18+
19+
impl LocalConfig {
20+
pub fn new(
21+
problem_id: u32, problem_name: String, language: String,
22+
) -> Self {
23+
Self { problem_id, problem_name, language }
24+
}
25+
26+
/// Find and read local config from current directory or parent directories
27+
pub fn find_and_read() -> io::Result<Option<Self>> {
28+
let mut current_dir = std::env::current_dir()?;
29+
30+
loop {
31+
let config_path = current_dir.join(".leetcode-cli");
32+
if config_path.exists() {
33+
let content = fs::read_to_string(&config_path)?;
34+
let config: LocalConfig =
35+
toml::from_str(&content).map_err(|e| {
36+
io::Error::new(io::ErrorKind::InvalidData, e)
37+
})?;
38+
return Ok(Some(config));
39+
}
40+
41+
if !current_dir.pop() {
42+
break;
43+
}
44+
}
45+
46+
Ok(None)
47+
}
48+
49+
/// Write local config to specified directory
50+
pub fn write_to_dir(&self, dir: &Path) -> io::Result<()> {
51+
let config_path = dir.join(".leetcode-cli");
52+
let content = toml::to_string(self)
53+
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
54+
fs::write(config_path, content)
55+
}
56+
57+
/// Read local config from specified file path
58+
pub fn read_from_path(path: &Path) -> io::Result<Self> {
59+
let content = fs::read_to_string(path)?;
60+
toml::from_str(&content)
61+
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
62+
}
63+
64+
/// Get the main source file name based on language
65+
pub fn get_main_file(&self) -> String {
66+
match self.language.to_lowercase().as_str() {
67+
"rust" => "main.rs".to_string(),
68+
"python" | "python3" => "main.py".to_string(),
69+
"javascript" => "main.js".to_string(),
70+
"typescript" => "main.ts".to_string(),
71+
"go" => "main.go".to_string(),
72+
"java" => "Main.java".to_string(),
73+
"c++" => "main.cpp".to_string(),
74+
"c" => "main.c".to_string(),
75+
_ => "main.txt".to_string(),
76+
}
77+
}
78+
79+
/// Resolve problem ID and file path from CLI args or local config
80+
pub fn resolve_problem_params(
81+
id: Option<u32>, path_to_file: Option<String>,
82+
) -> io::Result<(u32, String)> {
83+
match (id, &path_to_file) {
84+
(Some(id), Some(path)) => Ok((id, path.clone())),
85+
_ => {
86+
// Try to find local config
87+
match Self::find_and_read()? {
88+
Some(config) => {
89+
let problem_id = id.unwrap_or(config.problem_id);
90+
let file_path = path_to_file.unwrap_or_else(|| {
91+
format!("src/{}", config.get_main_file())
92+
});
93+
Ok((problem_id, file_path))
94+
},
95+
None => {
96+
if id.is_none() {
97+
return Err(io::Error::new(
98+
io::ErrorKind::NotFound,
99+
"No problem ID provided and no .leetcode-cli \
100+
config found. Either provide --id or run \
101+
from a problem directory",
102+
));
103+
}
104+
if path_to_file.is_none() {
105+
return Err(io::Error::new(
106+
io::ErrorKind::NotFound,
107+
"No file path provided",
108+
));
109+
}
110+
// If we get here, both id and path_to_file must be Some
111+
match (id, path_to_file) {
112+
(Some(id), Some(path)) => Ok((id, path)),
113+
_ => Err(io::Error::other(
114+
"Unexpected error: id or path_to_file missing \
115+
after checks",
116+
)),
117+
}
118+
},
119+
}
120+
},
121+
}
122+
}
123+
}
124+
125+
#[cfg(test)]
126+
mod tests {
127+
use tempfile::TempDir;
128+
129+
use super::*;
130+
131+
#[test]
132+
fn test_local_config_creation() {
133+
let config =
134+
LocalConfig::new(1, "two_sum".to_string(), "Rust".to_string());
135+
136+
assert_eq!(config.problem_id, 1);
137+
assert_eq!(config.problem_name, "two_sum");
138+
assert_eq!(config.language, "Rust");
139+
}
140+
141+
#[test]
142+
fn test_write_and_read_config() {
143+
let temp_dir = TempDir::new().unwrap();
144+
let config =
145+
LocalConfig::new(1, "two_sum".to_string(), "Rust".to_string());
146+
147+
config.write_to_dir(temp_dir.path()).unwrap();
148+
149+
let config_path = temp_dir.path().join(".leetcode-cli");
150+
assert!(config_path.exists());
151+
152+
let read_config = LocalConfig::read_from_path(&config_path).unwrap();
153+
assert_eq!(read_config.problem_id, 1);
154+
assert_eq!(read_config.problem_name, "two_sum");
155+
assert_eq!(read_config.language, "Rust");
156+
}
157+
158+
#[test]
159+
fn test_get_main_file() {
160+
let config =
161+
LocalConfig::new(1, "two_sum".to_string(), "Rust".to_string());
162+
assert_eq!(config.get_main_file(), "main.rs");
163+
164+
let config =
165+
LocalConfig::new(1, "two_sum".to_string(), "Python".to_string());
166+
assert_eq!(config.get_main_file(), "main.py");
167+
}
168+
}

src/main.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use leetcode_cli::{
99
Cli,
1010
Commands,
1111
LeetcodeApiRunner,
12+
LocalConfig,
1213
RuntimeConfigSetup,
1314
};
1415

@@ -57,30 +58,41 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5758
let start_problem = api_runner.start_problem(*id, lang).await;
5859
stop_and_clear_spinner(spin);
5960
match start_problem {
60-
Ok((success_message, _, warning)) => {
61+
Ok((success_message, pb_dir, warning)) => {
6162
if let Some(warning) = warning {
6263
eprintln!("{warning}");
6364
}
6465
println!("{success_message}");
6566
println!("\nHappy coding :)");
67+
println!(
68+
"\n(ps: to use local config feature, you should \ncd \
69+
{}\n;)",
70+
pb_dir.display()
71+
);
6672
},
6773
Err(e) => eprintln!("Error starting problem: {e}"),
6874
}
6975
},
7076
Commands::Test { id, path_to_file } => {
77+
let (problem_id, file_path) =
78+
LocalConfig::resolve_problem_params(*id, path_to_file.clone())?;
79+
7180
let spin = spin_the_spinner("Running tests...");
7281
let test_result =
73-
api_runner.test_response(*id, &path_to_file.clone()).await;
82+
api_runner.test_response(problem_id, &file_path).await;
7483
stop_and_clear_spinner(spin);
7584
match test_result {
76-
Ok(_) => println!("Test result"),
77-
Err(e) => eprintln!("Error running tests: {e}"),
85+
Ok(message) => println!("{message}"),
86+
Err(e) => eprintln!("Error running tests:\n{e}"),
7887
}
7988
},
8089
Commands::Submit { id, path_to_file } => {
90+
let (problem_id, file_path) =
91+
LocalConfig::resolve_problem_params(*id, path_to_file.clone())?;
92+
8193
let spin = spin_the_spinner("Submitting solution...");
8294
let submit_result =
83-
api_runner.submit_response(*id, &path_to_file.clone()).await;
95+
api_runner.submit_response(problem_id, &file_path).await;
8496
stop_and_clear_spinner(spin);
8597
match submit_result {
8698
Ok(_) => println!("Submit result"),

0 commit comments

Comments
 (0)