diff --git a/Cargo.lock b/Cargo.lock index ae7fc40..04c4f8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,7 +291,7 @@ dependencies = [ [[package]] name = "grepdef" -version = "3.0.0" +version = "3.1.0" dependencies = [ "clap", "colored", @@ -300,6 +300,8 @@ dependencies = [ "memchr", "regex", "rstest", + "serde", + "serde_json", "strum", "strum_macros", ] @@ -348,6 +350,12 @@ version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "lazy_static" version = "1.5.0" @@ -485,6 +493,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "same-file" version = "1.0.6" @@ -520,6 +534,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 685b1bc..40d83e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "grepdef" -version = "3.0.0" +version = "3.1.0" edition = "2021" repository = "https://github.com/sirbrillig/grepdef" homepage = "https://github.com/sirbrillig/grepdef" @@ -16,6 +16,8 @@ ignore = "0.4.22" memchr = "2.7.4" regex = "1.10.5" rstest = "0.21.0" +serde = { version = "1.0.204", features = ["derive"] } +serde_json = "1.0.120" strum = "0.26.3" strum_macros = "0.26.4" diff --git a/src/lib.rs b/src/lib.rs index f72568e..ae6edba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,14 +20,14 @@ //! //! ```text //! $ grepdef parseQuery ./src -//! // ./src/queries.js:function parseQuery { +//! ./src/queries.js:function parseQuery { //! ``` //! //! Just like `grep`, you can add the `-n` option to include line numbers. //! //! ```text //! $ grepdef -n parseQuery ./src -//! // ./src/queries.js:17:function parseQuery { +//! ./src/queries.js:17:function parseQuery { //! ``` //! //! The search will be faster if you specify what type of file you are searching for using the @@ -35,7 +35,7 @@ //! //! ```text //! $ grepdef --type js -n parseQuery ./src -//! // ./src/queries.js:17:function parseQuery { +//! ./src/queries.js:17:function parseQuery { //! ``` //! //! To use the crate from other Rust code, use [Searcher]. @@ -52,6 +52,7 @@ use clap::Parser; use colored::Colorize; use ignore::Walk; use regex::Regex; +use serde::Serialize; use std::error::Error; use std::fs; use std::io::{self, BufRead, Seek}; @@ -119,6 +120,10 @@ pub struct Args { /// (Advanced) The number of threads to use #[arg(short = 'j', long = "threads")] pub threads: Option>, + + /// The output format; defaults to 'grep' + #[arg(long = "format")] + pub format: Option, } impl Args { @@ -192,6 +197,9 @@ struct Config { /// The number of threads to use for searching files num_threads: NonZero, + + /// The output format + format: SearchResultFormat, } impl Config { @@ -224,6 +232,7 @@ impl Config { no_color: args.no_color, search_method: args.search_method.unwrap_or_default(), num_threads, + format: args.format.unwrap_or_default(), }; debug(&config, format!("Created config {:?}", config).as_str()); Ok(config) @@ -298,12 +307,21 @@ impl FileType { } } -/// A result from calling [Searcher::search] -/// -/// The `line_number` will be set only if [Args::line_number] is true when calling [Searcher::search]. +/// The output format of [SearchResult::to_string] +#[derive(clap::ValueEnum, Clone, Default, Debug, EnumString, PartialEq, Display, Copy)] +pub enum SearchResultFormat { + /// grep-like output; colon-separated path, line number, and text + #[default] + Grep, + + /// JSON output; one document per match + JsonPerMatch, +} + +/// A result from calling [Searcher::search] or [Searcher::search_and_format] /// -/// See [SearchResult::to_grep] as the most common formatting output. -#[derive(Debug, PartialEq, Clone)] +/// Note that `line_number` will be set only if [Args::line_number] is true when searching. +#[derive(Debug, PartialEq, Clone, Serialize)] pub struct SearchResult { /// The path to the file containing the symbol definition pub file_path: String, @@ -339,6 +357,11 @@ impl SearchResult { None => format!("{}:{}", self.file_path.magenta(), self.text), } } + + /// Return a formatted string for output in the "JSON_PER_MATCH" format + pub fn to_json_per_match(&self) -> String { + serde_json::to_string(self).unwrap_or_default() + } } /// A struct that can perform a search @@ -356,8 +379,8 @@ impl SearchResult { /// true /// )) /// .unwrap(); -/// for result in searcher.search().unwrap() { -/// println!("{}", result.to_grep()); +/// for result in searcher.search_and_format().unwrap() { +/// println!("{}", result); /// } /// ``` pub struct Searcher { @@ -371,7 +394,19 @@ impl Searcher { Ok(Searcher { config }) } - /// Perform the search this struct was built to do + /// Perform the search and return formatted strings + pub fn search_and_format(&self) -> Result, Box> { + let results = self.search()?; + Ok(results + .iter() + .map(|result| match self.config.format { + SearchResultFormat::Grep => result.to_grep(), + SearchResultFormat::JsonPerMatch => result.to_json_per_match(), + }) + .collect()) + } + + /// Perform the search and return [SearchResult] structs pub fn search(&self) -> Result, Box> { // Don't try to even calculate elapsed time if we are not going to print it let start: Option = if self.config.debug { diff --git a/src/main.rs b/src/main.rs index 263e7a5..f018e17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,10 +8,10 @@ fn main() { eprintln!("{err}"); process::exit(exitcode::USAGE); }); - match searcher.search() { + match searcher.search_and_format() { Ok(results) => { for line in results { - println!("{}", line.to_grep()); + println!("{}", line); } } Err(err) => { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 51089b1..e7b6be2 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -17,6 +17,7 @@ pub fn make_args( debug: false, no_color: false, threads: None, + format: None, } } @@ -25,6 +26,13 @@ pub fn do_search(args: Args) -> Vec { searcher.search().expect("Search failed for test") } +pub fn do_search_format(args: Args) -> Vec { + let searcher = Searcher::new(args).unwrap(); + searcher + .search_and_format() + .expect("Search failed for test") +} + pub fn get_default_fixture_for_file_type_string(file_type_string: &str) -> Result { match file_type_string { "js" => Ok(String::from("./tests/fixtures/by-language/js-fixture.js")), diff --git a/tests/integration_test.rs b/tests/integration_test.rs index b6c3ee6..9fa79ae 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,4 +1,4 @@ -use grepdef::{Args, SearchResult}; +use grepdef::{Args, SearchResult, SearchResultFormat}; use rstest::rstest; use std::num::NonZero; @@ -76,6 +76,87 @@ fn to_grep_formats_message_with_number() { } } +#[rstest] +fn search_and_format_returns_formatted_string_for_grep_with_number() { + let file_path = common::get_default_fixture_for_file_type_string("js").unwrap(); + let query = String::from("parseQuery"); + let expected_result = common::get_expected_search_result_for_file_type("js"); + let expected = format!( + "{}:{}:{}", + expected_result.file_path, + expected_result.line_number.unwrap(), + expected_result.text + ); + let file_type_string = String::from("js"); + let mut args = common::make_args(query, Some(file_path), Some(file_type_string)); + args.line_number = true; + args.no_color = true; + args.format = Some(SearchResultFormat::Grep); + let actual = common::do_search_format(args); + for result in actual { + assert_eq!(expected, result); + } +} + +#[rstest] +fn search_and_format_returns_formatted_string_for_grep_without_number() { + let file_path = common::get_default_fixture_for_file_type_string("js").unwrap(); + let query = String::from("parseQuery"); + let expected_result = common::get_expected_search_result_for_file_type("js"); + let expected = format!("{}:{}", expected_result.file_path, expected_result.text); + let file_type_string = String::from("js"); + let mut args = common::make_args(query, Some(file_path), Some(file_type_string)); + args.line_number = false; + args.no_color = true; + args.format = Some(SearchResultFormat::Grep); + let actual = common::do_search_format(args); + for result in actual { + assert_eq!(expected, result); + } +} + +#[rstest] +fn search_and_format_returns_formatted_string_for_json_per_match_with_number() { + let file_path = common::get_default_fixture_for_file_type_string("js").unwrap(); + let query = String::from("parseQuery"); + let expected_result = common::get_expected_search_result_for_file_type("js"); + let expected = format!( + "{{\"file_path\":\"{}\",\"line_number\":{},\"text\":\"{}\"}}", + expected_result.file_path, + expected_result.line_number.unwrap(), + expected_result.text + ); + let file_type_string = String::from("js"); + let mut args = common::make_args(query, Some(file_path), Some(file_type_string)); + args.line_number = true; + args.no_color = true; + args.format = Some(SearchResultFormat::JsonPerMatch); + let actual = common::do_search_format(args); + for result in actual { + assert_eq!(expected, result); + } +} + +#[rstest] +fn search_and_format_returns_formatted_string_for_json_per_match_without_number() { + let file_path = common::get_default_fixture_for_file_type_string("js").unwrap(); + let query = String::from("parseQuery"); + let expected_result = common::get_expected_search_result_for_file_type("js"); + let expected = format!( + "{{\"file_path\":\"{}\",\"line_number\":null,\"text\":\"{}\"}}", + expected_result.file_path, expected_result.text + ); + let file_type_string = String::from("js"); + let mut args = common::make_args(query, Some(file_path), Some(file_type_string)); + args.line_number = false; + args.no_color = true; + args.format = Some(SearchResultFormat::JsonPerMatch); + let actual = common::do_search_format(args); + for result in actual { + assert_eq!(expected, result); + } +} + #[rstest] fn search_returns_matching_js_function_line_with_two_files() { let file_path = format!(