Skip to content

Commit 9b9d96e

Browse files
authored
Merge pull request #13 from T3pp31/codex/add-conflicts_with-to-cipherargs
CLI: `--text`/`--file` の同時指定を clap 競合で弾くようにし入力取得を簡素化
2 parents 45ea271 + 7070d04 commit 9b9d96e

File tree

2 files changed

+76
-51
lines changed

2 files changed

+76
-51
lines changed

src/cli.rs

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ pub struct Cli {
2222
#[derive(Args)]
2323
pub struct CipherArgs {
2424
/// Text to process
25-
#[arg(short, long)]
25+
#[arg(short, long, conflicts_with = "file")]
2626
pub text: Option<String>,
2727

2828
/// Input file path
29-
#[arg(short = 'f', long)]
29+
#[arg(short = 'f', long, conflicts_with = "text")]
3030
pub file: Option<String>,
3131

3232
/// Shift value (any integer; safe mode: -25 to 25, default: 3)
@@ -57,11 +57,11 @@ pub enum Commands {
5757
/// Show all possible decryptions (brute force)
5858
BruteForce {
5959
/// Text to decrypt
60-
#[arg(short, long)]
60+
#[arg(short, long, conflicts_with = "file")]
6161
text: Option<String>,
6262

6363
/// Input file path
64-
#[arg(short = 'f', long)]
64+
#[arg(short = 'f', long, conflicts_with = "text")]
6565
file: Option<String>,
6666
},
6767
}
@@ -136,52 +136,47 @@ pub fn run_cli() -> Result<(), Box<dyn std::error::Error>> {
136136
/// # Errors
137137
///
138138
/// Returns an error if:
139-
/// - Both text and file are provided simultaneously
140139
/// - File reading fails
141140
/// - Stdin reading fails
142141
fn get_input_text(
143142
text: Option<String>,
144143
file: Option<String>,
145144
) -> Result<String, Box<dyn std::error::Error>> {
146-
match (text, file) {
147-
(Some(t), None) => {
148-
if t.len() > MAX_INPUT_SIZE {
149-
return Err(format!(
150-
"Input text exceeds maximum size of {} bytes",
151-
MAX_INPUT_SIZE
152-
)
153-
.into());
154-
}
155-
Ok(t)
145+
if let Some(t) = text {
146+
if t.len() > MAX_INPUT_SIZE {
147+
return Err(format!(
148+
"Input text exceeds maximum size of {} bytes",
149+
MAX_INPUT_SIZE
150+
)
151+
.into());
156152
}
157-
(None, Some(f)) => {
158-
let metadata =
159-
fs::metadata(&f).map_err(|e| format!("Failed to read file '{}': {}", f, e))?;
160-
if metadata.len() > MAX_INPUT_SIZE as u64 {
161-
return Err(format!(
162-
"Input file '{}' exceeds maximum size of {} bytes",
163-
f, MAX_INPUT_SIZE
164-
)
165-
.into());
166-
}
167-
fs::read_to_string(&f).map_err(|e| format!("Failed to read file '{}': {}", f, e).into())
168-
}
169-
(Some(_), Some(_)) => Err("Cannot specify both text and file".into()),
170-
(None, None) => {
171-
print!("Enter text: ");
172-
io::stdout().flush()?;
173-
let mut input = String::new();
174-
io::stdin().read_line(&mut input)?;
175-
if input.len() > MAX_INPUT_SIZE {
176-
return Err(format!(
177-
"Input text exceeds maximum size of {} bytes",
178-
MAX_INPUT_SIZE
179-
)
180-
.into());
181-
}
182-
Ok(trim_trailing_newline(&input).to_string())
153+
return Ok(t);
154+
}
155+
156+
if let Some(f) = file {
157+
let metadata = fs::metadata(&f).map_err(|e| format!("Failed to read file '{}': {}", f, e))?;
158+
if metadata.len() > MAX_INPUT_SIZE as u64 {
159+
return Err(format!(
160+
"Input file '{}' exceeds maximum size of {} bytes",
161+
f, MAX_INPUT_SIZE
162+
)
163+
.into());
183164
}
165+
return fs::read_to_string(&f).map_err(|e| format!("Failed to read file '{}': {}", f, e).into());
166+
}
167+
168+
print!("Enter text: ");
169+
io::stdout().flush()?;
170+
let mut input = String::new();
171+
io::stdin().read_line(&mut input)?;
172+
if input.len() > MAX_INPUT_SIZE {
173+
return Err(format!(
174+
"Input text exceeds maximum size of {} bytes",
175+
MAX_INPUT_SIZE
176+
)
177+
.into());
184178
}
179+
Ok(input.trim().to_string())
185180
}
186181

187182
fn trim_trailing_newline(input: &str) -> &str {
@@ -391,13 +386,10 @@ mod tests {
391386
}
392387

393388
#[test]
394-
fn test_get_input_text_both_provided() {
389+
fn test_get_input_text_prefers_text_when_both_provided() {
395390
let result = get_input_text(Some("Hello".to_string()), Some("file.txt".to_string()));
396-
assert!(result.is_err());
397-
assert!(result
398-
.unwrap_err()
399-
.to_string()
400-
.contains("Cannot specify both text and file"));
391+
assert!(result.is_ok());
392+
assert_eq!(result.unwrap(), "Hello");
401393
}
402394

403395
#[test]

tests/cli_output_tests.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//! | テスト観点 | 分類 | 期待値 |
88
//! |----------------------------------|----------|---------------------------------------|
99
//! | 存在しないファイル読込 | 異常系 | エラーメッセージにファイルパス含む |
10-
//! | text+file同時指定 | 異常系 | "Cannot specify both" エラー |
10+
//! | text+file同時指定 | 異常系 | clap が即エラーを返す |
1111
//! | テキスト引数のみ指定 | 正常系 | テキストがそのまま返る |
1212
//! | ファイルから正常読込 | 正常系 | ファイル内容が返る |
1313
//! | 空ファイル読込 | 境界値 | 空文字列返却 |
@@ -180,10 +180,43 @@ fn test_cli_both_text_and_file_error() {
180180

181181
let stderr = String::from_utf8_lossy(&output.stderr);
182182

183-
// Then: Error message mentions the conflict
183+
// Then: clap validates conflict before application logic
184+
assert!(!output.status.success(), "Command should fail on clap validation");
184185
assert!(
185-
stderr.contains("Cannot specify both"),
186-
"Error should mention 'Cannot specify both', got: {}",
186+
stderr.contains("cannot be used with") || stderr.contains("conflicts with"),
187+
"Error should mention clap conflict, got: {}",
188+
stderr
189+
);
190+
}
191+
192+
#[test]
193+
fn test_cli_brute_force_both_text_and_file_error() {
194+
// Given: Both text and file arguments provided for brute-force
195+
let mut temp_file = NamedTempFile::new().unwrap();
196+
write!(temp_file, "content").unwrap();
197+
let file_path = temp_file.path().to_string_lossy().to_string();
198+
199+
// When: CLI receives both arguments
200+
let output = std::process::Command::new("cargo")
201+
.args([
202+
"run",
203+
"--",
204+
"brute-force",
205+
"--text",
206+
"Khoor",
207+
"--file",
208+
&file_path,
209+
])
210+
.output()
211+
.expect("Failed to execute CLI");
212+
213+
let stderr = String::from_utf8_lossy(&output.stderr);
214+
215+
// Then: clap validates conflict before application logic
216+
assert!(!output.status.success(), "Command should fail on clap validation");
217+
assert!(
218+
stderr.contains("cannot be used with") || stderr.contains("conflicts with"),
219+
"Error should mention clap conflict, got: {}",
187220
stderr
188221
);
189222
}

0 commit comments

Comments
 (0)