Skip to content

Commit 572249d

Browse files
martinemdeivy
andcommitted
Implement json output format option
Provides a schema to describe the output format. Co-authored-by: Ivy Evans <ivy.evans@gusto.com>
1 parent 217bb95 commit 572249d

File tree

5 files changed

+297
-0
lines changed

5 files changed

+297
-0
lines changed

schema/check-output.json

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "pks check JSON output",
4+
"type": "object",
5+
"required": ["violations", "stale_todos", "summary"],
6+
"additionalProperties": false,
7+
"properties": {
8+
"violations": {
9+
"type": "array",
10+
"items": { "$ref": "#/$defs/Violation" }
11+
},
12+
"stale_todos": {
13+
"type": "array",
14+
"items": { "$ref": "#/$defs/StaleTodo" }
15+
},
16+
"summary": {
17+
"type": "object",
18+
"required": [
19+
"violation_count",
20+
"stale_todo_count",
21+
"strict_violation_count",
22+
"success"
23+
],
24+
"additionalProperties": false,
25+
"properties": {
26+
"violation_count": { "type": "integer", "minimum": 0 },
27+
"stale_todo_count": { "type": "integer", "minimum": 0 },
28+
"strict_violation_count": {
29+
"type": "integer",
30+
"minimum": 0,
31+
"description": "Count of violations where strict=true (subset of violation_count)"
32+
},
33+
"success": { "type": "boolean" }
34+
}
35+
}
36+
},
37+
"$defs": {
38+
"ViolationType": {
39+
"type": "string",
40+
"enum": ["dependency", "privacy", "visibility", "layer", "folder_privacy"]
41+
},
42+
"Violation": {
43+
"type": "object",
44+
"required": [
45+
"violation_type",
46+
"file",
47+
"constant_name",
48+
"referencing_pack_name",
49+
"defining_pack_name",
50+
"strict",
51+
"message"
52+
],
53+
"additionalProperties": false,
54+
"properties": {
55+
"violation_type": { "$ref": "#/$defs/ViolationType" },
56+
"file": { "type": "string" },
57+
"constant_name": { "type": "string" },
58+
"referencing_pack_name": { "type": "string" },
59+
"defining_pack_name": { "type": "string" },
60+
"strict": { "type": "boolean" },
61+
"message": { "type": "string" }
62+
}
63+
},
64+
"StaleTodo": {
65+
"type": "object",
66+
"required": [
67+
"violation_type",
68+
"file",
69+
"constant_name",
70+
"referencing_pack_name",
71+
"defining_pack_name"
72+
],
73+
"additionalProperties": false,
74+
"properties": {
75+
"violation_type": { "$ref": "#/$defs/ViolationType" },
76+
"file": { "type": "string" },
77+
"constant_name": { "type": "string" },
78+
"referencing_pack_name": { "type": "string" },
79+
"defining_pack_name": { "type": "string" }
80+
}
81+
}
82+
}
83+
}

src/packs.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub(crate) mod configuration;
1111
pub(crate) mod constant_resolver;
1212
pub(crate) mod creator;
1313
pub(crate) mod csv;
14+
pub(crate) mod json;
1415
pub(crate) mod dependencies;
1516
pub(crate) mod ignored;
1617
pub(crate) mod monkey_patch_detection;
@@ -86,6 +87,9 @@ pub fn check(
8687
OutputFormat::CSV => {
8788
csv::write_csv(&result, std::io::stdout())?;
8889
}
90+
OutputFormat::JSON => {
91+
json::write_json(&result, std::io::stdout())?;
92+
}
8993
}
9094

9195
Ok(())

src/packs/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ enum Command {
159159
pub enum OutputFormat {
160160
Packwerk,
161161
CSV,
162+
JSON,
162163
}
163164

164165
#[derive(Debug, Args)]

src/packs/json.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use itertools::chain;
2+
use serde::Serialize;
3+
4+
use super::checker::{build_strict_violation_message, CheckAllResult};
5+
6+
#[derive(Serialize)]
7+
struct JsonOutput<'a> {
8+
violations: Vec<JsonViolation<'a>>,
9+
stale_todos: Vec<JsonStaleTodo<'a>>,
10+
summary: JsonSummary,
11+
}
12+
13+
#[derive(Serialize)]
14+
struct JsonViolation<'a> {
15+
violation_type: &'a str,
16+
file: &'a str,
17+
constant_name: &'a str,
18+
referencing_pack_name: &'a str,
19+
defining_pack_name: &'a str,
20+
strict: bool,
21+
message: String,
22+
}
23+
24+
#[derive(Serialize)]
25+
struct JsonStaleTodo<'a> {
26+
violation_type: &'a str,
27+
file: &'a str,
28+
constant_name: &'a str,
29+
referencing_pack_name: &'a str,
30+
defining_pack_name: &'a str,
31+
}
32+
33+
#[derive(Serialize)]
34+
struct JsonSummary {
35+
violation_count: usize,
36+
stale_todo_count: usize,
37+
strict_violation_count: usize,
38+
success: bool,
39+
}
40+
41+
pub fn write_json<W: std::io::Write>(
42+
result: &CheckAllResult,
43+
writer: W,
44+
) -> anyhow::Result<()> {
45+
let all_violations = chain!(
46+
&result.reportable_violations,
47+
&result.strict_mode_violations
48+
);
49+
50+
let violations: Vec<JsonViolation> = all_violations
51+
.map(|v| {
52+
let message = if v.identifier.strict {
53+
build_strict_violation_message(&v.identifier)
54+
} else {
55+
v.message.clone()
56+
};
57+
JsonViolation {
58+
violation_type: &v.identifier.violation_type,
59+
file: &v.identifier.file,
60+
constant_name: &v.identifier.constant_name,
61+
referencing_pack_name: &v.identifier.referencing_pack_name,
62+
defining_pack_name: &v.identifier.defining_pack_name,
63+
strict: v.identifier.strict,
64+
message,
65+
}
66+
})
67+
.collect();
68+
69+
let stale_todos: Vec<JsonStaleTodo> = result
70+
.stale_violations
71+
.iter()
72+
.map(|v| JsonStaleTodo {
73+
violation_type: &v.violation_type,
74+
file: &v.file,
75+
constant_name: &v.constant_name,
76+
referencing_pack_name: &v.referencing_pack_name,
77+
defining_pack_name: &v.defining_pack_name,
78+
})
79+
.collect();
80+
81+
let violation_count = violations.len();
82+
let stale_todo_count = stale_todos.len();
83+
let strict_violation_count = result.strict_mode_violations.len();
84+
let success =
85+
violation_count == 0 && stale_todo_count == 0 && strict_violation_count == 0;
86+
87+
let output = JsonOutput {
88+
violations,
89+
stale_todos,
90+
summary: JsonSummary {
91+
violation_count,
92+
stale_todo_count,
93+
strict_violation_count,
94+
success,
95+
},
96+
};
97+
98+
serde_json::to_writer(writer, &output)?;
99+
Ok(())
100+
}

tests/check_test.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,112 @@ fn test_check_contents_ignoring_recorded_violations(
406406
common::teardown();
407407
Ok(())
408408
}
409+
410+
#[test]
411+
fn test_check_with_json_output_format_violations() -> Result<(), Box<dyn Error>> {
412+
let output = Command::cargo_bin("pks")?
413+
.arg("--project-root")
414+
.arg("tests/fixtures/simple_app")
415+
.arg("check")
416+
.arg("-o")
417+
.arg("json")
418+
.assert()
419+
.success()
420+
.get_output()
421+
.stdout
422+
.clone();
423+
424+
let json_output: serde_json::Value =
425+
serde_json::from_slice(&output).expect("Output should be valid JSON");
426+
427+
assert_eq!(json_output["summary"]["violation_count"], 2);
428+
assert_eq!(json_output["summary"]["success"], false);
429+
430+
let violations = json_output["violations"].as_array().unwrap();
431+
assert_eq!(violations.len(), 2);
432+
433+
for violation in violations {
434+
assert!(violation["violation_type"].as_str().is_some());
435+
assert!(violation["file"].as_str().is_some());
436+
assert!(violation["constant_name"].as_str().is_some());
437+
assert!(violation["referencing_pack_name"].as_str().is_some());
438+
assert!(violation["defining_pack_name"].as_str().is_some());
439+
assert!(violation["strict"].as_bool().is_some());
440+
assert!(violation["message"].as_str().is_some());
441+
}
442+
443+
let violation_types: Vec<&str> = violations
444+
.iter()
445+
.map(|v| v["violation_type"].as_str().unwrap())
446+
.collect();
447+
assert!(violation_types.contains(&"dependency"));
448+
assert!(violation_types.contains(&"privacy"));
449+
450+
common::teardown();
451+
Ok(())
452+
}
453+
454+
#[test]
455+
fn test_check_with_json_output_format_stale_todos() -> Result<(), Box<dyn Error>> {
456+
let output = Command::cargo_bin("pks")?
457+
.arg("--project-root")
458+
.arg("tests/fixtures/contains_stale_violations")
459+
.arg("check")
460+
.arg("-o")
461+
.arg("json")
462+
.assert()
463+
.success()
464+
.get_output()
465+
.stdout
466+
.clone();
467+
468+
let json_output: serde_json::Value =
469+
serde_json::from_slice(&output).expect("Output should be valid JSON");
470+
471+
assert!(
472+
json_output["summary"]["stale_todo_count"].as_i64().unwrap() > 0,
473+
"Expected at least one stale todo"
474+
);
475+
assert_eq!(json_output["summary"]["success"], false);
476+
477+
let stale_todos = json_output["stale_todos"].as_array().unwrap();
478+
assert!(!stale_todos.is_empty(), "Expected stale_todos array to be non-empty");
479+
480+
let stale_todo = &stale_todos[0];
481+
assert!(stale_todo["violation_type"].as_str().is_some());
482+
assert!(stale_todo["file"].as_str().is_some());
483+
assert!(stale_todo["constant_name"].as_str().is_some());
484+
assert!(stale_todo["referencing_pack_name"].as_str().is_some());
485+
assert!(stale_todo["defining_pack_name"].as_str().is_some());
486+
487+
common::teardown();
488+
Ok(())
489+
}
490+
491+
#[test]
492+
fn test_check_with_json_output_format_empty() -> Result<(), Box<dyn Error>> {
493+
let output = Command::cargo_bin("pks")?
494+
.arg("--project-root")
495+
.arg("tests/fixtures/contains_package_todo")
496+
.arg("check")
497+
.arg("-o")
498+
.arg("json")
499+
.assert()
500+
.success()
501+
.get_output()
502+
.stdout
503+
.clone();
504+
505+
let json_output: serde_json::Value =
506+
serde_json::from_slice(&output).expect("Output should be valid JSON");
507+
508+
assert_eq!(json_output["summary"]["violation_count"], 0);
509+
assert_eq!(json_output["summary"]["stale_todo_count"], 0);
510+
assert_eq!(json_output["summary"]["strict_violation_count"], 0);
511+
assert_eq!(json_output["summary"]["success"], true);
512+
assert!(json_output["violations"].as_array().unwrap().is_empty());
513+
assert!(json_output["stale_todos"].as_array().unwrap().is_empty());
514+
515+
common::teardown();
516+
Ok(())
517+
}

0 commit comments

Comments
 (0)