Skip to content

Commit 07227f7

Browse files
authored
Merge pull request #34 from sirbrillig/add-json-output
Add json output
2 parents bc4fdb9 + bfc326b commit 07227f7

File tree

6 files changed

+167
-16
lines changed

6 files changed

+167
-16
lines changed

Cargo.lock

+26-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "grepdef"
3-
version = "3.0.0"
3+
version = "3.1.0"
44
edition = "2021"
55
repository = "https://github.com/sirbrillig/grepdef"
66
homepage = "https://github.com/sirbrillig/grepdef"
@@ -16,6 +16,8 @@ ignore = "0.4.22"
1616
memchr = "2.7.4"
1717
regex = "1.10.5"
1818
rstest = "0.21.0"
19+
serde = { version = "1.0.204", features = ["derive"] }
20+
serde_json = "1.0.120"
1921
strum = "0.26.3"
2022
strum_macros = "0.26.4"
2123

src/lib.rs

+46-11
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,22 @@
2020
//!
2121
//! ```text
2222
//! $ grepdef parseQuery ./src
23-
//! // ./src/queries.js:function parseQuery {
23+
//! ./src/queries.js:function parseQuery {
2424
//! ```
2525
//!
2626
//! Just like `grep`, you can add the `-n` option to include line numbers.
2727
//!
2828
//! ```text
2929
//! $ grepdef -n parseQuery ./src
30-
//! // ./src/queries.js:17:function parseQuery {
30+
//! ./src/queries.js:17:function parseQuery {
3131
//! ```
3232
//!
3333
//! The search will be faster if you specify what type of file you are searching for using the
3434
//! `--type` option.
3535
//!
3636
//! ```text
3737
//! $ grepdef --type js -n parseQuery ./src
38-
//! // ./src/queries.js:17:function parseQuery {
38+
//! ./src/queries.js:17:function parseQuery {
3939
//! ```
4040
//!
4141
//! To use the crate from other Rust code, use [Searcher].
@@ -52,6 +52,7 @@ use clap::Parser;
5252
use colored::Colorize;
5353
use ignore::Walk;
5454
use regex::Regex;
55+
use serde::Serialize;
5556
use std::error::Error;
5657
use std::fs;
5758
use std::io::{self, BufRead, Seek};
@@ -119,6 +120,10 @@ pub struct Args {
119120
/// (Advanced) The number of threads to use
120121
#[arg(short = 'j', long = "threads")]
121122
pub threads: Option<NonZero<usize>>,
123+
124+
/// The output format; defaults to 'grep'
125+
#[arg(long = "format")]
126+
pub format: Option<SearchResultFormat>,
122127
}
123128

124129
impl Args {
@@ -192,6 +197,9 @@ struct Config {
192197

193198
/// The number of threads to use for searching files
194199
num_threads: NonZero<usize>,
200+
201+
/// The output format
202+
format: SearchResultFormat,
195203
}
196204

197205
impl Config {
@@ -224,6 +232,7 @@ impl Config {
224232
no_color: args.no_color,
225233
search_method: args.search_method.unwrap_or_default(),
226234
num_threads,
235+
format: args.format.unwrap_or_default(),
227236
};
228237
debug(&config, format!("Created config {:?}", config).as_str());
229238
Ok(config)
@@ -298,12 +307,21 @@ impl FileType {
298307
}
299308
}
300309

301-
/// A result from calling [Searcher::search]
302-
///
303-
/// The `line_number` will be set only if [Args::line_number] is true when calling [Searcher::search].
310+
/// The output format of [SearchResult::to_string]
311+
#[derive(clap::ValueEnum, Clone, Default, Debug, EnumString, PartialEq, Display, Copy)]
312+
pub enum SearchResultFormat {
313+
/// grep-like output; colon-separated path, line number, and text
314+
#[default]
315+
Grep,
316+
317+
/// JSON output; one document per match
318+
JsonPerMatch,
319+
}
320+
321+
/// A result from calling [Searcher::search] or [Searcher::search_and_format]
304322
///
305-
/// See [SearchResult::to_grep] as the most common formatting output.
306-
#[derive(Debug, PartialEq, Clone)]
323+
/// Note that `line_number` will be set only if [Args::line_number] is true when searching.
324+
#[derive(Debug, PartialEq, Clone, Serialize)]
307325
pub struct SearchResult {
308326
/// The path to the file containing the symbol definition
309327
pub file_path: String,
@@ -339,6 +357,11 @@ impl SearchResult {
339357
None => format!("{}:{}", self.file_path.magenta(), self.text),
340358
}
341359
}
360+
361+
/// Return a formatted string for output in the "JSON_PER_MATCH" format
362+
pub fn to_json_per_match(&self) -> String {
363+
serde_json::to_string(self).unwrap_or_default()
364+
}
342365
}
343366

344367
/// A struct that can perform a search
@@ -356,8 +379,8 @@ impl SearchResult {
356379
/// true
357380
/// ))
358381
/// .unwrap();
359-
/// for result in searcher.search().unwrap() {
360-
/// println!("{}", result.to_grep());
382+
/// for result in searcher.search_and_format().unwrap() {
383+
/// println!("{}", result);
361384
/// }
362385
/// ```
363386
pub struct Searcher {
@@ -371,7 +394,19 @@ impl Searcher {
371394
Ok(Searcher { config })
372395
}
373396

374-
/// Perform the search this struct was built to do
397+
/// Perform the search and return formatted strings
398+
pub fn search_and_format(&self) -> Result<Vec<String>, Box<dyn Error>> {
399+
let results = self.search()?;
400+
Ok(results
401+
.iter()
402+
.map(|result| match self.config.format {
403+
SearchResultFormat::Grep => result.to_grep(),
404+
SearchResultFormat::JsonPerMatch => result.to_json_per_match(),
405+
})
406+
.collect())
407+
}
408+
409+
/// Perform the search and return [SearchResult] structs
375410
pub fn search(&self) -> Result<Vec<SearchResult>, Box<dyn Error>> {
376411
// Don't try to even calculate elapsed time if we are not going to print it
377412
let start: Option<time::Instant> = if self.config.debug {

src/main.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ fn main() {
88
eprintln!("{err}");
99
process::exit(exitcode::USAGE);
1010
});
11-
match searcher.search() {
11+
match searcher.search_and_format() {
1212
Ok(results) => {
1313
for line in results {
14-
println!("{}", line.to_grep());
14+
println!("{}", line);
1515
}
1616
}
1717
Err(err) => {

tests/common/mod.rs

+8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub fn make_args(
1717
debug: false,
1818
no_color: false,
1919
threads: None,
20+
format: None,
2021
}
2122
}
2223

@@ -25,6 +26,13 @@ pub fn do_search(args: Args) -> Vec<SearchResult> {
2526
searcher.search().expect("Search failed for test")
2627
}
2728

29+
pub fn do_search_format(args: Args) -> Vec<String> {
30+
let searcher = Searcher::new(args).unwrap();
31+
searcher
32+
.search_and_format()
33+
.expect("Search failed for test")
34+
}
35+
2836
pub fn get_default_fixture_for_file_type_string(file_type_string: &str) -> Result<String, String> {
2937
match file_type_string {
3038
"js" => Ok(String::from("./tests/fixtures/by-language/js-fixture.js")),

tests/integration_test.rs

+82-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use grepdef::{Args, SearchResult};
1+
use grepdef::{Args, SearchResult, SearchResultFormat};
22
use rstest::rstest;
33
use std::num::NonZero;
44

@@ -76,6 +76,87 @@ fn to_grep_formats_message_with_number() {
7676
}
7777
}
7878

79+
#[rstest]
80+
fn search_and_format_returns_formatted_string_for_grep_with_number() {
81+
let file_path = common::get_default_fixture_for_file_type_string("js").unwrap();
82+
let query = String::from("parseQuery");
83+
let expected_result = common::get_expected_search_result_for_file_type("js");
84+
let expected = format!(
85+
"{}:{}:{}",
86+
expected_result.file_path,
87+
expected_result.line_number.unwrap(),
88+
expected_result.text
89+
);
90+
let file_type_string = String::from("js");
91+
let mut args = common::make_args(query, Some(file_path), Some(file_type_string));
92+
args.line_number = true;
93+
args.no_color = true;
94+
args.format = Some(SearchResultFormat::Grep);
95+
let actual = common::do_search_format(args);
96+
for result in actual {
97+
assert_eq!(expected, result);
98+
}
99+
}
100+
101+
#[rstest]
102+
fn search_and_format_returns_formatted_string_for_grep_without_number() {
103+
let file_path = common::get_default_fixture_for_file_type_string("js").unwrap();
104+
let query = String::from("parseQuery");
105+
let expected_result = common::get_expected_search_result_for_file_type("js");
106+
let expected = format!("{}:{}", expected_result.file_path, expected_result.text);
107+
let file_type_string = String::from("js");
108+
let mut args = common::make_args(query, Some(file_path), Some(file_type_string));
109+
args.line_number = false;
110+
args.no_color = true;
111+
args.format = Some(SearchResultFormat::Grep);
112+
let actual = common::do_search_format(args);
113+
for result in actual {
114+
assert_eq!(expected, result);
115+
}
116+
}
117+
118+
#[rstest]
119+
fn search_and_format_returns_formatted_string_for_json_per_match_with_number() {
120+
let file_path = common::get_default_fixture_for_file_type_string("js").unwrap();
121+
let query = String::from("parseQuery");
122+
let expected_result = common::get_expected_search_result_for_file_type("js");
123+
let expected = format!(
124+
"{{\"file_path\":\"{}\",\"line_number\":{},\"text\":\"{}\"}}",
125+
expected_result.file_path,
126+
expected_result.line_number.unwrap(),
127+
expected_result.text
128+
);
129+
let file_type_string = String::from("js");
130+
let mut args = common::make_args(query, Some(file_path), Some(file_type_string));
131+
args.line_number = true;
132+
args.no_color = true;
133+
args.format = Some(SearchResultFormat::JsonPerMatch);
134+
let actual = common::do_search_format(args);
135+
for result in actual {
136+
assert_eq!(expected, result);
137+
}
138+
}
139+
140+
#[rstest]
141+
fn search_and_format_returns_formatted_string_for_json_per_match_without_number() {
142+
let file_path = common::get_default_fixture_for_file_type_string("js").unwrap();
143+
let query = String::from("parseQuery");
144+
let expected_result = common::get_expected_search_result_for_file_type("js");
145+
let expected = format!(
146+
"{{\"file_path\":\"{}\",\"line_number\":null,\"text\":\"{}\"}}",
147+
expected_result.file_path, expected_result.text
148+
);
149+
let file_type_string = String::from("js");
150+
let mut args = common::make_args(query, Some(file_path), Some(file_type_string));
151+
args.line_number = false;
152+
args.no_color = true;
153+
args.format = Some(SearchResultFormat::JsonPerMatch);
154+
let actual = common::do_search_format(args);
155+
for result in actual {
156+
assert_eq!(expected, result);
157+
}
158+
}
159+
79160
#[rstest]
80161
fn search_returns_matching_js_function_line_with_two_files() {
81162
let file_path = format!(

0 commit comments

Comments
 (0)