From f073493f31af832b679e6ec8b249bb94e611babc Mon Sep 17 00:00:00 2001 From: Lucas Henry Date: Thu, 5 Mar 2026 14:17:14 +0100 Subject: [PATCH 1/2] feat: grep command now exits with code 2 when no matches are found This aligns the grep command behavior with the standard Unix grep utility: - Exit code 0: matches found - Exit code 1: error occurred - Exit code 2: no matches found The changes also properly handle multiple file and directory searches by aggregating results across all files and only returning 'no matches' if none of the searched files produced any matches. --- src/main.rs | 65 ++++++++++++++++++++++++++++++++++++++++--------- src/yaml_ops.rs | 28 +++++++++------------ 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/src/main.rs b/src/main.rs index 226e304..5505583 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,14 @@ fn get_terminal_width() -> usize { 80 } +fn no_matches_error() -> AppError { + AppError::message("No matches found") +} + +fn is_no_matches_error(error: &AppError) -> bool { + matches!(error, AppError::Message(message) if message == "No matches found") +} + fn main() { let command = match parse_cli() { Ok(command) => command, @@ -35,9 +43,15 @@ fn main() { } }; - if let Err(error) = execute_command(command) { - eprintln!("Error: {error}"); - process::exit(1); + match execute_command(command) { + Ok(()) => {} + Err(error) if is_no_matches_error(&error) => { + process::exit(2); + } + Err(error) => { + eprintln!("Error: {error}"); + process::exit(1); + } } } @@ -93,10 +107,21 @@ fn run_grep(pattern: &str, recursive: bool, files: &[String]) -> AppResult<()> { } let show_filename = should_show_filename(files); + let mut found_any = false; + for file in files { - grep_path(Path::new(file), pattern, recursive, show_filename)?; + match grep_path(Path::new(file), pattern, recursive, show_filename) { + Ok(()) => found_any = true, + Err(error) if is_no_matches_error(&error) => {} + Err(error) => return Err(error), + } + } + + if found_any { + Ok(()) + } else { + Err(no_matches_error()) } - Ok(()) } fn should_show_filename(files: &[String]) -> bool { @@ -149,7 +174,7 @@ fn grep_file(path: &Path, pattern: &str, show_filename: bool) -> AppResult<()> { let contents = fs::read_to_string(path).map_err(|error| AppError::read_file(display.as_ref(), error))?; let value = serde_yaml::from_str(&contents) - .map_err(|error| AppError::parse_yaml(format!("in '{}'", display), error))?; + .map_err(|error| AppError::parse_yaml(format!("in '{display}'"), error))?; print_grep_results(show_filename.then_some(display.as_ref()), pattern, &value) } @@ -160,14 +185,18 @@ fn print_grep_results( value: &serde_yaml::Value, ) -> AppResult<()> { let results = yaml_ops::grep(value, pattern)?; + if results.is_empty() { + return Err(no_matches_error()); + } + let width = get_terminal_width(); for (key, value) in results { let formatted = yaml_ops::format_result(&key, &value, width); if let Some(filename) = filename { - println!("{}:{}", filename, formatted); + println!("{filename}:{formatted}"); } else { - println!("{}", formatted); + println!("{formatted}"); } } @@ -178,20 +207,32 @@ fn search_dir(dir: &Path, pattern: &str, show_filename: bool) -> AppResult<()> { let entries = fs::read_dir(dir).map_err(|error| AppError::read_dir(dir.display().to_string(), error))?; + let mut found_any = false; + for entry in entries { let entry = entry.map_err(AppError::ReadDirEntry)?; let path = entry.path(); if path.is_dir() { - search_dir(&path, pattern, show_filename)?; + match search_dir(&path, pattern, show_filename) { + Ok(()) => found_any = true, + Err(error) if is_no_matches_error(&error) => {} + Err(error) => return Err(error), + } } else if path.is_file() && should_process_file(&path) { - if let Err(error) = grep_file(&path, pattern, show_filename) { - eprintln!("Warning: {error}"); + match grep_file(&path, pattern, show_filename) { + Ok(()) => found_any = true, + Err(error) if is_no_matches_error(&error) => {} + Err(error) => return Err(error), } } } - Ok(()) + if found_any { + Ok(()) + } else { + Err(no_matches_error()) + } } fn should_process_file(path: &Path) -> bool { diff --git a/src/yaml_ops.rs b/src/yaml_ops.rs index 35ad7ba..215f190 100644 --- a/src/yaml_ops.rs +++ b/src/yaml_ops.rs @@ -390,7 +390,7 @@ fn get_value_at_path(value: &Value, path: &YamlPath) -> AppResult> pub fn copy_in_document(yaml_content: &str, source_key: &str, dest_key: &str) -> AppResult { let source_yaml = parse_yaml_document(yaml_content, "from source document")?; let value = get_value(&source_yaml, source_key)?.ok_or_else(|| { - AppError::message(format!("Key '{}' not found in source document", source_key)) + AppError::message(format!("Key '{source_key}' not found in source document")) })?; yaml_set(yaml_content, dest_key, value) @@ -424,10 +424,7 @@ pub fn copy_value( let source_yaml = parse_yaml_document(&source_contents, &format!("from '{source_file}'"))?; let value = get_value(&source_yaml, source_key)?.ok_or_else(|| { - AppError::message(format!( - "Key '{}' not found in '{}'", - source_key, source_file - )) + AppError::message(format!("Key '{source_key}' not found in '{source_file}'")) })?; let updated = yaml_set(&dest_contents, dest_key, value)?; @@ -453,10 +450,7 @@ pub fn move_value( let source_yaml = parse_yaml_document(&source_contents, &format!("from '{source_file}'"))?; let value = get_value(&source_yaml, source_key)?.ok_or_else(|| { - AppError::message(format!( - "Key '{}' not found in '{}'", - source_key, source_file - )) + AppError::message(format!("Key '{source_key}' not found in '{source_file}'")) })?; let dest_contents = if Path::new(dest_file).exists() { @@ -477,16 +471,16 @@ pub fn move_value( pub fn format_result(key: &str, value: &Value, terminal_width: usize) -> String { match value { Value::Mapping(_) => format_mapping_result(key, value), - Value::String(s) => truncate_if_needed(&format!("{}: {}", key, s), terminal_width), - Value::Number(n) => truncate_if_needed(&format!("{}: {}", key, n), terminal_width), - Value::Bool(b) => truncate_if_needed(&format!("{}: {}", key, b), terminal_width), - Value::Null => format!("{}: null", key), + Value::String(s) => truncate_if_needed(&format!("{key}: {s}"), terminal_width), + Value::Number(n) => truncate_if_needed(&format!("{key}: {n}"), terminal_width), + Value::Bool(b) => truncate_if_needed(&format!("{key}: {b}"), terminal_width), + Value::Null => format!("{key}: null"), _ => { let val_str = serde_yaml::to_string(value) .unwrap_or_else(|_| "".to_string()) .trim() .to_string(); - truncate_if_needed(&format!("{}: {}", key, val_str), terminal_width) + truncate_if_needed(&format!("{key}: {val_str}"), terminal_width) } } } @@ -502,7 +496,7 @@ fn truncate_if_needed(text: &str, terminal_width: usize) -> String { fn format_mapping_result(key: &str, value: &Value) -> String { let yaml_str = match serde_yaml::to_string(value) { Ok(result) => result, - Err(_) => return format!("{}: ", key), + Err(_) => return format!("{key}: "), }; let indented = yaml_str @@ -511,13 +505,13 @@ fn format_mapping_result(key: &str, value: &Value) -> String { if line.is_empty() { line.to_string() } else { - format!(" {}", line) + format!(" {line}") } }) .collect::>() .join("\n"); - format!("{}:\n{}", key, indented) + format!("{key}:\n{indented}") } #[cfg(test)] From f352e24ecfcb9d22810d9648a1e86a823d2beee4 Mon Sep 17 00:00:00 2001 From: Lucas Henry Date: Wed, 11 Mar 2026 16:32:05 +0100 Subject: [PATCH 2/2] feat: set grep output to single line mode --- src/cli.rs | 154 ++++++++++++++++++++++++--- src/main.rs | 95 +++++++++++++---- src/yaml_ops.rs | 269 +++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 441 insertions(+), 77 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 7059c66..b368755 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,6 +7,7 @@ pub enum Command { Grep { pattern: String, recursive: bool, + full: bool, files: Vec, }, Set { @@ -43,13 +44,8 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Commands { Grep { - pattern: String, - - #[arg(short = 'R')] - recursive: bool, - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - files: Vec, + args: Vec, }, Set { file: String, @@ -87,15 +83,15 @@ fn command_from_cli(cli: Cli) -> AppResult { fn command_from_parsed(command: Commands) -> AppResult { match command { - Commands::Grep { - pattern, - recursive, - files, - } => Ok(Command::Grep { - pattern, - recursive, - files, - }), + Commands::Grep { args } => { + let (pattern, recursive, full, files) = parse_grep_args(args)?; + Ok(Command::Grep { + pattern, + recursive, + full, + files, + }) + } Commands::Set { file, updates } => Ok(Command::Set { file, updates: parse_updates(updates)?, @@ -134,6 +130,30 @@ fn command_from_parsed(command: Commands) -> AppResult { } } +fn parse_grep_args(args: Vec) -> AppResult<(String, bool, bool, Vec)> { + if args.is_empty() { + return Err(AppError::cli("grep requires at least a pattern")); + } + + let mut pattern = None; + let mut recursive = false; + let mut full = false; + let mut files = Vec::new(); + + for arg in args { + match arg.as_str() { + "-R" => recursive = true, + "--full" => full = true, + _ if pattern.is_none() => pattern = Some(arg), + _ => files.push(arg), + } + } + + pattern + .map(|pattern| (pattern, recursive, full, files)) + .ok_or_else(|| AppError::cli("grep requires a pattern")) +} + fn parse_updates(updates: Vec) -> AppResult> { if updates.is_empty() { return Err(AppError::cli("set requires at least one key=value pair")); @@ -251,6 +271,7 @@ mod tests { Command::Grep { pattern: "pattern".to_string(), recursive: false, + full: false, files: vec!["file.yaml".to_string()], } ); @@ -265,6 +286,7 @@ mod tests { Command::Grep { pattern: "pattern".to_string(), recursive: true, + full: false, files: vec!["dir".to_string()], } ); @@ -287,6 +309,7 @@ mod tests { Command::Grep { pattern: "pattern".to_string(), recursive: false, + full: false, files: vec![ "file1.yaml".to_string(), "file2.yaml".to_string(), @@ -315,6 +338,107 @@ mod tests { Command::Grep { pattern: "pattern".to_string(), recursive: false, + full: false, + files: Vec::new(), + } + ); + } + + #[test] + fn test_parse_grep_with_full_flag() { + let cmd = test_with_args(vec!["ym", "grep", "--full", "pattern", "file.yaml"]).unwrap(); + + assert_eq!( + cmd, + Command::Grep { + pattern: "pattern".to_string(), + recursive: false, + full: true, + files: vec!["file.yaml".to_string()], + } + ); + } + + #[test] + fn test_parse_grep_with_flags_after_pattern() { + let cmd = test_with_args(vec!["ym", "grep", "pattern", ".", "-R"]).unwrap(); + + assert_eq!( + cmd, + Command::Grep { + pattern: "pattern".to_string(), + recursive: true, + full: false, + files: vec![".".to_string()], + } + ); + } + + #[test] + fn test_parse_grep_with_flags_between_args() { + let cmd = test_with_args(vec!["ym", "grep", "pattern", "-R", "file.yaml"]).unwrap(); + + assert_eq!( + cmd, + Command::Grep { + pattern: "pattern".to_string(), + recursive: true, + full: false, + files: vec!["file.yaml".to_string()], + } + ); + } + + #[test] + fn test_parse_grep_with_multiple_flags_after_args() { + let cmd = test_with_args(vec![ + "ym", + "grep", + "pattern", + "file1.yaml", + "file2.yaml", + "-R", + "--full", + ]) + .unwrap(); + + assert_eq!( + cmd, + Command::Grep { + pattern: "pattern".to_string(), + recursive: true, + full: true, + files: vec!["file1.yaml".to_string(), "file2.yaml".to_string()], + } + ); + } + + #[test] + fn test_parse_grep_with_flags_mixed_positions() { + let cmd = + test_with_args(vec!["ym", "grep", "-R", "pattern", "--full", "file.yaml"]).unwrap(); + + assert_eq!( + cmd, + Command::Grep { + pattern: "pattern".to_string(), + recursive: true, + full: true, + files: vec!["file.yaml".to_string()], + } + ); + } + + #[test] + fn test_parse_grep_only_pattern_with_flags() { + let cmd = test_with_args(vec!["ym", "grep", "pattern", "-R", "--full"]).unwrap(); + + assert_eq!( + cmd, + Command::Grep { + pattern: "pattern".to_string(), + recursive: true, + full: true, files: Vec::new(), } ); diff --git a/src/main.rs b/src/main.rs index 5505583..9f283b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ mod yaml_ops; use cli::{parse_cli, Command}; use error::{AppError, AppResult}; +use yaml_ops::GrepOutputMode; fn get_terminal_width() -> usize { if let Some(size) = termsize::get() { @@ -60,8 +61,9 @@ fn execute_command(command: Command) -> AppResult<()> { Command::Grep { pattern, recursive, + full, files, - } => run_grep(&pattern, recursive, &files), + } => run_grep(&pattern, recursive, full, &files), Command::Set { file, updates } => { apply_file_update(&file, |contents| yaml_ops::set_values(contents, &updates)) } @@ -101,16 +103,28 @@ where Ok(()) } -fn run_grep(pattern: &str, recursive: bool, files: &[String]) -> AppResult<()> { +fn run_grep(pattern: &str, recursive: bool, full: bool, files: &[String]) -> AppResult<()> { + let output_mode = if full { + GrepOutputMode::Full + } else { + GrepOutputMode::Inline + }; + if files.is_empty() { - return grep_stdin(pattern); + return grep_stdin(pattern, output_mode); } - let show_filename = should_show_filename(files); + let show_filename = should_show_filename(files, output_mode); let mut found_any = false; for file in files { - match grep_path(Path::new(file), pattern, recursive, show_filename) { + match grep_path( + Path::new(file), + pattern, + recursive, + show_filename, + output_mode, + ) { Ok(()) => found_any = true, Err(error) if is_no_matches_error(&error) => {} Err(error) => return Err(error), @@ -124,7 +138,11 @@ fn run_grep(pattern: &str, recursive: bool, files: &[String]) -> AppResult<()> { } } -fn should_show_filename(files: &[String]) -> bool { +fn should_show_filename(files: &[String], output_mode: GrepOutputMode) -> bool { + if matches!(output_mode, GrepOutputMode::Full) { + return true; + } + if files.len() != 1 { return true; } @@ -132,7 +150,7 @@ fn should_show_filename(files: &[String]) -> bool { Path::new(&files[0]).is_dir() } -fn grep_stdin(pattern: &str) -> AppResult<()> { +fn grep_stdin(pattern: &str, output_mode: GrepOutputMode) -> AppResult<()> { let mut buffer = String::new(); io::stdin() .read_to_string(&mut buffer) @@ -144,17 +162,23 @@ fn grep_stdin(pattern: &str) -> AppResult<()> { let value = serde_yaml::from_str(&buffer).map_err(|error| AppError::parse_yaml("from stdin", error))?; - print_grep_results(None, pattern, &value) + print_grep_results(None, pattern, &value, output_mode) } -fn grep_path(path: &Path, pattern: &str, recursive: bool, show_filename: bool) -> AppResult<()> { +fn grep_path( + path: &Path, + pattern: &str, + recursive: bool, + show_filename: bool, + output_mode: GrepOutputMode, +) -> AppResult<()> { if path.is_file() { - return grep_file(path, pattern, show_filename); + return grep_file(path, pattern, show_filename, output_mode); } if path.is_dir() { return if recursive { - search_dir(path, pattern, show_filename) + search_dir(path, pattern, show_filename, output_mode) } else { Err(AppError::message(format!( "'{}' is a directory (use -R to search recursively)", @@ -169,20 +193,31 @@ fn grep_path(path: &Path, pattern: &str, recursive: bool, show_filename: bool) - ))) } -fn grep_file(path: &Path, pattern: &str, show_filename: bool) -> AppResult<()> { +fn grep_file( + path: &Path, + pattern: &str, + show_filename: bool, + output_mode: GrepOutputMode, +) -> AppResult<()> { let display = path.to_string_lossy(); let contents = fs::read_to_string(path).map_err(|error| AppError::read_file(display.as_ref(), error))?; let value = serde_yaml::from_str(&contents) .map_err(|error| AppError::parse_yaml(format!("in '{display}'"), error))?; - print_grep_results(show_filename.then_some(display.as_ref()), pattern, &value) + print_grep_results( + show_filename.then_some(display.as_ref()), + pattern, + &value, + output_mode, + ) } fn print_grep_results( filename: Option<&str>, pattern: &str, value: &serde_yaml::Value, + output_mode: GrepOutputMode, ) -> AppResult<()> { let results = yaml_ops::grep(value, pattern)?; if results.is_empty() { @@ -192,18 +227,34 @@ fn print_grep_results( let width = get_terminal_width(); for (key, value) in results { - let formatted = yaml_ops::format_result(&key, &value, width); - if let Some(filename) = filename { - println!("{filename}:{formatted}"); - } else { - println!("{formatted}"); - } + print_grep_result(filename, &key, &value, width, output_mode); } Ok(()) } -fn search_dir(dir: &Path, pattern: &str, show_filename: bool) -> AppResult<()> { +fn print_grep_result( + filename: Option<&str>, + key: &str, + value: &serde_yaml::Value, + width: usize, + output_mode: GrepOutputMode, +) { + let formatted = yaml_ops::format_result(key, value, width, output_mode); + + match (filename, output_mode) { + (Some(filename), GrepOutputMode::Inline) => println!("{filename}:{formatted}"), + (Some(filename), GrepOutputMode::Full) => println!("--- {filename} ---\n{formatted}"), + (None, _) => println!("{formatted}"), + } +} + +fn search_dir( + dir: &Path, + pattern: &str, + show_filename: bool, + output_mode: GrepOutputMode, +) -> AppResult<()> { let entries = fs::read_dir(dir).map_err(|error| AppError::read_dir(dir.display().to_string(), error))?; @@ -214,13 +265,13 @@ fn search_dir(dir: &Path, pattern: &str, show_filename: bool) -> AppResult<()> { let path = entry.path(); if path.is_dir() { - match search_dir(&path, pattern, show_filename) { + match search_dir(&path, pattern, show_filename, output_mode) { Ok(()) => found_any = true, Err(error) if is_no_matches_error(&error) => {} Err(error) => return Err(error), } } else if path.is_file() && should_process_file(&path) { - match grep_file(&path, pattern, show_filename) { + match grep_file(&path, pattern, show_filename, output_mode) { Ok(()) => found_any = true, Err(error) if is_no_matches_error(&error) => {} Err(error) => return Err(error), diff --git a/src/yaml_ops.rs b/src/yaml_ops.rs index 215f190..71f5bef 100644 --- a/src/yaml_ops.rs +++ b/src/yaml_ops.rs @@ -11,6 +11,12 @@ use crate::path::{PathSegment, YamlPath}; const PLACEHOLDER_KEY: &str = "__ym_placeholder__"; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GrepOutputMode { + Inline, + Full, +} + pub fn grep(value: &Value, pattern: &str) -> AppResult> { let regex = Regex::new(pattern)?; let mut results = Vec::new(); @@ -468,20 +474,18 @@ pub fn move_value( Ok(()) } -pub fn format_result(key: &str, value: &Value, terminal_width: usize) -> String { - match value { - Value::Mapping(_) => format_mapping_result(key, value), - Value::String(s) => truncate_if_needed(&format!("{key}: {s}"), terminal_width), - Value::Number(n) => truncate_if_needed(&format!("{key}: {n}"), terminal_width), - Value::Bool(b) => truncate_if_needed(&format!("{key}: {b}"), terminal_width), - Value::Null => format!("{key}: null"), - _ => { - let val_str = serde_yaml::to_string(value) - .unwrap_or_else(|_| "".to_string()) - .trim() - .to_string(); - truncate_if_needed(&format!("{key}: {val_str}"), terminal_width) +pub fn format_result( + key: &str, + value: &Value, + terminal_width: usize, + mode: GrepOutputMode, +) -> String { + match mode { + GrepOutputMode::Inline => { + let result = format!("{key}: {}", format_inline_value(value)); + truncate_if_needed(&result, terminal_width) } + GrepOutputMode::Full => format_full_result(key, value), } } @@ -493,25 +497,135 @@ fn truncate_if_needed(text: &str, terminal_width: usize) -> String { } } -fn format_mapping_result(key: &str, value: &Value) -> String { - let yaml_str = match serde_yaml::to_string(value) { - Ok(result) => result, - Err(_) => return format!("{key}: "), - }; +fn format_full_result(key: &str, value: &Value) -> String { + let mut rendered = render_yaml_with_indent(key, value, 0); + if rendered.ends_with('\n') { + rendered.pop(); + } + rendered +} - let indented = yaml_str - .lines() - .map(|line| { - if line.is_empty() { - line.to_string() - } else { - format!(" {line}") +fn format_inline_value(value: &Value) -> String { + match value { + Value::Mapping(map) => { + let entries = map + .iter() + .map(|(key, val)| { + format!("{}: {}", format_inline_key(key), format_inline_value(val)) + }) + .collect::>() + .join(", "); + format!("{{ {entries} }}") + } + Value::Sequence(seq) => { + let items = seq + .iter() + .map(format_inline_value) + .collect::>() + .join(", "); + format!("[{items}]") + } + Value::String(s) => s.clone(), + Value::Number(n) => n.to_string(), + Value::Bool(b) => b.to_string(), + Value::Null => "null".to_string(), + _ => serde_yaml::to_string(value) + .unwrap_or_else(|_| "".to_string()) + .trim() + .replace('\n', " "), + } +} + +fn format_inline_key(key: &Value) -> String { + match key { + Value::String(s) => s.clone(), + Value::Number(n) => n.to_string(), + Value::Bool(b) => b.to_string(), + Value::Null => "null".to_string(), + _ => serde_yaml::to_string(key) + .unwrap_or_else(|_| "".to_string()) + .trim() + .replace('\n', " "), + } +} + +fn render_yaml_with_indent(key: &str, value: &Value, indent: usize) -> String { + let prefix = " ".repeat(indent); + + match value { + Value::Mapping(map) => { + let mut output = format!("{prefix}{key}:\n"); + for (child_key, child_value) in map { + output.push_str(&render_yaml_with_indent( + &format_inline_key(child_key), + child_value, + indent + 1, + )); + } + output + } + Value::Sequence(seq) => { + let mut output = format!("{prefix}{key}:\n"); + for item in seq { + output.push_str(&render_sequence_item(item, indent + 1)); } - }) - .collect::>() - .join("\n"); + output + } + _ => format!("{prefix}{key}: {}\n", format_inline_value(value)), + } +} + +fn render_sequence_item(value: &Value, indent: usize) -> String { + let prefix = " ".repeat(indent); + + match value { + Value::Mapping(map) => { + let mut iter = map.iter(); + if let Some((first_key, first_value)) = iter.next() { + let mut output = format!("{prefix}-"); + match first_value { + Value::Mapping(_) | Value::Sequence(_) => { + output.push('\n'); + output.push_str(&render_yaml_with_indent( + &format_inline_key(first_key), + first_value, + indent + 1, + )); + } + _ => { + output.push_str(&format!( + " {}: {}\n", + format_inline_key(first_key), + format_inline_value(first_value) + )); + } + } - format!("{key}:\n{indented}") + for (key, val) in iter { + output.push_str(&render_yaml_with_indent( + &format_inline_key(key), + val, + indent + 1, + )); + } + output + } else { + format!("{prefix}- {{}}\n") + } + } + Value::Sequence(seq) => { + if seq.is_empty() { + format!("{prefix}- []\n") + } else { + let mut output = format!("{prefix}-\n"); + for item in seq { + output.push_str(&render_sequence_item(item, indent + 1)); + } + output + } + } + _ => format!("{prefix}- {}\n", format_inline_value(value)), + } } #[cfg(test)] @@ -527,7 +641,7 @@ mod tests { .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos(); - let dir = format!("{}_{}_{}", name, std::process::id(), unique); + let dir = format!("{name}_{}_{}", std::process::id(), unique); let _ = fs::remove_dir_all(&dir); fs::create_dir(&dir).unwrap(); dir @@ -695,8 +809,8 @@ mod tests { #[test] fn test_copy_value_handles_scalars_and_mappings() { let test_dir = temp_test_dir("test_copy_value"); - let source_file = format!("{}/source.yaml", test_dir); - let dest_file = format!("{}/dest.yaml", test_dir); + let source_file = format!("{test_dir}/source.yaml"); + let dest_file = format!("{test_dir}/dest.yaml"); fs::write( &source_file, @@ -719,8 +833,8 @@ mod tests { #[test] fn test_move_value_updates_destination_and_removes_source() { let test_dir = temp_test_dir("test_move_value"); - let source_file = format!("{}/source.yaml", test_dir); - let dest_file = format!("{}/dest.yaml", test_dir); + let source_file = format!("{test_dir}/source.yaml"); + let dest_file = format!("{test_dir}/dest.yaml"); fs::write( &source_file, @@ -748,22 +862,97 @@ mod tests { #[test] fn test_format_string_value() { let value = Value::String("hello".to_string()); - assert_eq!(format_result("message", &value, 80), "message: hello"); + assert_eq!( + format_result("message", &value, 80, GrepOutputMode::Inline), + "message: hello" + ); + } + + #[test] + fn test_format_number_value() { + let value = Value::Number(42.into()); + assert_eq!( + format_result("count", &value, 80, GrepOutputMode::Inline), + "count: 42" + ); + } + + #[test] + fn test_format_boolean_true() { + let value = Value::Bool(true); + assert_eq!( + format_result("enabled", &value, 80, GrepOutputMode::Inline), + "enabled: true" + ); + } + + #[test] + fn test_format_boolean_false() { + let value = Value::Bool(false); + assert_eq!( + format_result("enabled", &value, 80, GrepOutputMode::Inline), + "enabled: false" + ); + } + + #[test] + fn test_format_null_value() { + let value = Value::Null; + assert_eq!( + format_result("empty", &value, 80, GrepOutputMode::Inline), + "empty: null" + ); + } + + #[test] + fn test_format_mapping_value_inline() { + let value = parse_yaml("host: localhost\nport: 5432"); + let result = format_result("database", &value, 80, GrepOutputMode::Inline); + assert_eq!(result, "database: { host: localhost, port: 5432 }"); } #[test] - fn test_format_mapping_value() { + fn test_format_mapping_value_full() { let value = parse_yaml("host: localhost\nport: 5432"); - let result = format_result("database", &value, 80); - assert!(result.starts_with("database:\n")); - assert!(result.contains("host")); + let result = format_result("database", &value, 80, GrepOutputMode::Full); + assert_eq!(result, "database:\n host: localhost\n port: 5432"); } #[test] fn test_format_truncates_long_string() { let long_string = "a".repeat(100); let value = Value::String(long_string); - let result = format_result("key", &value, 20); + let result = format_result("key", &value, 20, GrepOutputMode::Inline); assert!(result.ends_with("...")); + assert!(result.len() <= 23); + } + + #[test] + fn test_format_does_not_truncate_short_string() { + let value = Value::String("short".to_string()); + let result = format_result("key", &value, 80, GrepOutputMode::Inline); + assert_eq!(result, "key: short"); + assert!(!result.ends_with("...")); + } + + #[test] + fn test_format_sequence_inline() { + let value = parse_yaml("- item1\n- item2\n- item3"); + let result = format_result("items", &value, 80, GrepOutputMode::Inline); + assert_eq!(result, "items: [item1, item2, item3]"); + } + + #[test] + fn test_format_nested_mapping_inline() { + let value = parse_yaml("subkey1: true\nsubkey2: false"); + let result = format_result("key", &value, 80, GrepOutputMode::Inline); + assert_eq!(result, "key: { subkey1: true, subkey2: false }"); + } + + #[test] + fn test_format_nested_mapping_full() { + let value = parse_yaml("subkey1: true\nsubkey2: false"); + let result = format_result("key", &value, 80, GrepOutputMode::Full); + assert_eq!(result, "key:\n subkey1: true\n subkey2: false"); } }