Skip to content

Commit 1c71490

Browse files
committed
json for-file
1 parent 9c0296d commit 1c71490

File tree

13 files changed

+309
-59
lines changed

13 files changed

+309
-59
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "codeowners"
3-
version = "0.2.16"
3+
version = "0.2.17"
44
edition = "2024"
55

66
[profile.release]

src/cache/file.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ impl Caching for GlobalCache {
6262

6363
fn delete_cache(&self) -> Result<(), Error> {
6464
let cache_path = self.get_cache_path();
65-
dbg!("deleting", &cache_path);
65+
tracing::debug!("Deleting cache file: {}", cache_path.display());
6666
fs::remove_file(cache_path).change_context(Error::Io)
6767
}
6868
}

src/cli.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ enum Command {
1717
help = "Find the owner from the CODEOWNERS file and just return the team name and yml path"
1818
)]
1919
from_codeowners: bool,
20+
#[arg(short, long, default_value = "false", help = "Output the result in JSON format")]
21+
json: bool,
2022
name: String,
2123
},
2224

23-
#[clap(about = "Finds code ownership information for a given team ", visible_alias = "t")]
25+
#[clap(about = "Finds code ownership information for a given team", visible_alias = "t")]
2426
ForTeam { name: String },
2527

2628
#[clap(
@@ -113,7 +115,11 @@ pub fn cli() -> Result<RunResult, RunnerError> {
113115
Command::Validate => runner::validate(&run_config, vec![]),
114116
Command::Generate { skip_stage } => runner::generate(&run_config, !skip_stage),
115117
Command::GenerateAndValidate { skip_stage } => runner::generate_and_validate(&run_config, vec![], !skip_stage),
116-
Command::ForFile { name, from_codeowners } => runner::for_file(&run_config, &name, from_codeowners),
118+
Command::ForFile {
119+
name,
120+
from_codeowners,
121+
json,
122+
} => runner::for_file(&run_config, &name, from_codeowners, json),
117123
Command::ForTeam { name } => runner::for_team(&run_config, &name),
118124
Command::DeleteCache => runner::delete_cache(&run_config),
119125
Command::CrosscheckOwners => runner::crosscheck_owners(&run_config),

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ fn maybe_print_errors(result: RunResult) -> Result<(), RunnerError> {
2626
for msg in result.validation_errors {
2727
println!("{}", msg);
2828
}
29-
process::exit(-1);
29+
process::exit(1);
3030
}
3131

3232
Ok(())

src/ownership.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,21 @@ impl TeamOwnership {
5959
impl Display for FileOwner {
6060
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6161
let sources = if self.sources.is_empty() {
62-
"Unowned".to_string()
62+
"".to_string()
6363
} else {
64-
self.sources
64+
let sources_str = self
65+
.sources
6566
.iter()
6667
.sorted_by_key(|source| source.to_string())
6768
.map(|source| source.to_string())
6869
.collect::<Vec<_>>()
69-
.join("\n- ")
70+
.join("\n- ");
71+
format!("\n- {}", sources_str)
7072
};
7173

7274
write!(
7375
f,
74-
"Team: {}\nGithub Team: {}\nTeam YML: {}\nDescription:\n- {}",
76+
"Team: {}\nGithub Team: {}\nTeam YML: {}\nDescription:{}",
7577
self.team.name, self.team.github_team, self.team_config_file_path, sources
7678
)
7779
}

src/project.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,9 @@ pub enum Error {
158158
impl fmt::Display for Error {
159159
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
160160
match self {
161-
Error::Io => fmt.write_str("Error::Io"),
162-
Error::SerdeYaml => fmt.write_str("Error::SerdeYaml"),
163-
Error::SerdeJson => fmt.write_str("Error::SerdeJson"),
161+
Error::Io => fmt.write_str("IO operation failed"),
162+
Error::SerdeYaml => fmt.write_str("YAML serialization/deserialization failed"),
163+
Error::SerdeJson => fmt.write_str("JSON serialization/deserialization failed"),
164164
}
165165
}
166166
}

src/project_builder.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ impl<'a> ProjectBuilder<'a> {
5252
}
5353
}
5454

55-
#[instrument(level = "debug", skip_all)]
55+
#[instrument(level = "debug", skip_all, fields(base_path = %self.base_path.display()))]
5656
pub fn build(&mut self) -> Result<Project, Error> {
57+
tracing::info!("Starting project build");
5758
let mut builder = WalkBuilder::new(&self.base_path);
5859
builder.hidden(false);
5960
builder.follow_links(false);
@@ -277,6 +278,13 @@ impl<'a> ProjectBuilder<'a> {
277278
.flat_map(|team| vec![(team.name.clone(), team.clone()), (team.github_team.clone(), team.clone())])
278279
.collect();
279280

281+
tracing::info!(
282+
files_count = %project_files.len(),
283+
teams_count = %teams.len(),
284+
packages_count = %packages.len(),
285+
"Project build completed successfully"
286+
);
287+
280288
Ok(Project {
281289
base_path: self.base_path.to_owned(),
282290
files: project_files,

src/runner.rs

Lines changed: 168 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{path::Path, process::Command};
22

33
use error_stack::{Result, ResultExt};
4+
use serde::Serialize;
45

56
use crate::{
67
cache::{Cache, Caching, file::GlobalCache, noop::NoopCache},
@@ -181,61 +182,163 @@ impl Runner {
181182
Ok(owners)
182183
}
183184

184-
pub fn for_file_optimized(&self, file_path: &str) -> RunResult {
185+
pub fn for_file_derived(&self, file_path: &str, json: bool) -> RunResult {
185186
let file_owners = match self.owners_for_file(file_path) {
186187
Ok(v) => v,
187188
Err(err) => {
188-
return RunResult {
189-
io_errors: vec![err.to_string()],
190-
..Default::default()
191-
};
189+
return RunResult::from_io_error(Error::Io(err.to_string()), json);
192190
}
193191
};
194192

195-
let info_messages: Vec<String> = match file_owners.len() {
196-
0 => vec![format!("{}", FileOwner::default())],
197-
1 => vec![format!("{}", file_owners[0])],
198-
_ => {
193+
match file_owners.as_slice() {
194+
[] => RunResult::from_file_owner(&FileOwner::default(), json),
195+
[owner] => RunResult::from_file_owner(owner, json),
196+
many => {
199197
let mut error_messages = vec!["Error: file is owned by multiple teams!".to_string()];
200-
for file_owner in file_owners {
201-
error_messages.push(format!("\n{}", file_owner));
198+
for owner in many {
199+
error_messages.push(format!("\n{}", owner));
202200
}
203-
return RunResult {
204-
validation_errors: error_messages,
205-
..Default::default()
206-
};
201+
RunResult::from_validation_errors(error_messages, json)
207202
}
208-
};
209-
RunResult {
210-
info_messages,
211-
..Default::default()
212203
}
213204
}
214205

215-
pub fn for_file_codeowners_only(&self, file_path: &str) -> RunResult {
206+
pub fn for_file_codeowners_only(&self, file_path: &str, json: bool) -> RunResult {
216207
match team_for_file_from_codeowners(&self.run_config, file_path) {
217208
Ok(Some(team)) => {
218-
let relative_team_path = crate::path_utils::relative_to(&self.run_config.project_root, team.path.as_path())
209+
let team_yml = crate::path_utils::relative_to(&self.run_config.project_root, team.path.as_path())
219210
.to_string_lossy()
220211
.to_string();
221-
RunResult {
222-
info_messages: vec![format!(
223-
"Team: {}\nGithub Team: {}\nTeam YML: {}\nDescription:\n- Owner inferred from codeowners file",
224-
team.name, team.github_team, relative_team_path
225-
)],
226-
..Default::default()
212+
let result = ForFileResult {
213+
team_name: team.name.clone(),
214+
github_team: team.github_team.clone(),
215+
team_yml,
216+
description: vec!["Owner inferred from codeowners file".to_string()],
217+
};
218+
if json {
219+
RunResult::json_info(result)
220+
} else {
221+
RunResult {
222+
info_messages: vec![format!(
223+
"Team: {}\nGithub Team: {}\nTeam YML: {}\nDescription:\n- {}",
224+
result.team_name,
225+
result.github_team,
226+
result.team_yml,
227+
result.description.join("\n- ")
228+
)],
229+
..Default::default()
230+
}
231+
}
232+
}
233+
Ok(None) => RunResult::from_file_owner(&FileOwner::default(), json),
234+
Err(err) => {
235+
if json {
236+
RunResult::json_io_error(Error::Io(err.to_string()))
237+
} else {
238+
RunResult {
239+
io_errors: vec![err.to_string()],
240+
..Default::default()
241+
}
227242
}
228243
}
229-
Ok(None) => RunResult::default(),
230-
Err(err) => RunResult {
231-
io_errors: vec![err.to_string()],
232-
..Default::default()
233-
},
234244
}
235245
}
236246
}
237247

238-
// removed free functions for for_file_* variants in favor of Runner methods
248+
#[derive(Debug, Clone, Serialize)]
249+
pub struct ForFileResult {
250+
pub team_name: String,
251+
pub github_team: String,
252+
pub team_yml: String,
253+
pub description: Vec<String>,
254+
}
255+
256+
impl RunResult {
257+
pub fn has_errors(&self) -> bool {
258+
!self.validation_errors.is_empty() || !self.io_errors.is_empty()
259+
}
260+
261+
fn from_io_error(error: Error, json: bool) -> Self {
262+
if json {
263+
Self::json_io_error(error)
264+
} else {
265+
Self {
266+
io_errors: vec![error.to_string()],
267+
..Default::default()
268+
}
269+
}
270+
}
271+
272+
fn from_file_owner(file_owner: &FileOwner, json: bool) -> Self {
273+
if json {
274+
let description: Vec<String> = if file_owner.sources.is_empty() {
275+
vec![]
276+
} else {
277+
file_owner.sources.iter().map(|source| source.to_string()).collect()
278+
};
279+
Self::json_info(ForFileResult {
280+
team_name: file_owner.team.name.clone(),
281+
github_team: file_owner.team.github_team.clone(),
282+
team_yml: file_owner.team_config_file_path.clone(),
283+
description,
284+
})
285+
} else {
286+
Self {
287+
info_messages: vec![format!("{}", file_owner)],
288+
..Default::default()
289+
}
290+
}
291+
}
292+
293+
fn from_validation_errors(validation_errors: Vec<String>, json: bool) -> Self {
294+
if json {
295+
Self::json_validation_error(validation_errors)
296+
} else {
297+
Self {
298+
validation_errors,
299+
..Default::default()
300+
}
301+
}
302+
}
303+
304+
pub fn json_info(result: ForFileResult) -> Self {
305+
let json = match serde_json::to_string_pretty(&result) {
306+
Ok(json) => json,
307+
Err(e) => return Self::json_io_error(Error::Io(e.to_string())),
308+
};
309+
Self {
310+
info_messages: vec![json],
311+
..Default::default()
312+
}
313+
}
314+
315+
pub fn json_io_error(error: Error) -> Self {
316+
let message = match error {
317+
Error::Io(msg) => msg,
318+
Error::ValidationFailed => "Error::ValidationFailed".to_string(),
319+
};
320+
let json = match serde_json::to_string(&serde_json::json!({"error": message})).map_err(|e| Error::Io(e.to_string())) {
321+
Ok(json) => json,
322+
Err(e) => return Self::json_io_error(Error::Io(e.to_string())),
323+
};
324+
Self {
325+
io_errors: vec![json],
326+
..Default::default()
327+
}
328+
}
329+
330+
pub fn json_validation_error(validation_errors: Vec<String>) -> Self {
331+
let json_obj = serde_json::json!({"validation_errors": validation_errors});
332+
let json = match serde_json::to_string_pretty(&json_obj) {
333+
Ok(json) => json,
334+
Err(e) => return Self::json_io_error(Error::Io(e.to_string())),
335+
};
336+
Self {
337+
validation_errors: vec![json],
338+
..Default::default()
339+
}
340+
}
341+
}
239342

240343
#[cfg(test)]
241344
mod tests {
@@ -245,4 +348,36 @@ mod tests {
245348
fn test_version() {
246349
assert_eq!(version(), env!("CARGO_PKG_VERSION").to_string());
247350
}
351+
#[test]
352+
fn test_json_info() {
353+
let result = ForFileResult {
354+
team_name: "team1".to_string(),
355+
github_team: "team1".to_string(),
356+
team_yml: "config/teams/team1.yml".to_string(),
357+
description: vec!["file annotation".to_string()],
358+
};
359+
let result = RunResult::json_info(result);
360+
assert_eq!(result.info_messages.len(), 1);
361+
assert_eq!(
362+
result.info_messages[0],
363+
"{\n \"team_name\": \"team1\",\n \"github_team\": \"team1\",\n \"team_yml\": \"config/teams/team1.yml\",\n \"description\": [\n \"file annotation\"\n ]\n}"
364+
);
365+
}
366+
367+
#[test]
368+
fn test_json_io_error() {
369+
let result = RunResult::json_io_error(Error::Io("unable to find file".to_string()));
370+
assert_eq!(result.io_errors.len(), 1);
371+
assert_eq!(result.io_errors[0], "{\"error\":\"unable to find file\"}");
372+
}
373+
374+
#[test]
375+
fn test_json_validation_error() {
376+
let result = RunResult::json_validation_error(vec!["file has multiple owners".to_string()]);
377+
assert_eq!(result.validation_errors.len(), 1);
378+
assert_eq!(
379+
result.validation_errors[0],
380+
"{\n \"validation_errors\": [\n \"file has multiple owners\"\n ]\n}"
381+
);
382+
}
248383
}

src/runner/api.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ use crate::project::Team;
66

77
use super::{Error, RunConfig, RunResult, Runner, config_from_path, run};
88

9-
pub fn for_file(run_config: &RunConfig, file_path: &str, from_codeowners: bool) -> RunResult {
9+
pub fn for_file(run_config: &RunConfig, file_path: &str, from_codeowners: bool, json: bool) -> RunResult {
1010
run(run_config, |runner| {
1111
if from_codeowners {
12-
runner.for_file_codeowners_only(file_path)
12+
runner.for_file_codeowners_only(file_path, json)
1313
} else {
14-
runner.for_file_optimized(file_path)
14+
runner.for_file_derived(file_path, json)
1515
}
1616
})
1717
}

0 commit comments

Comments
 (0)