Skip to content

Commit d55cce0

Browse files
authored
CSV output for pks check (#11)
* adding csv support
1 parent 8f8e87d commit d55cce0

File tree

6 files changed

+122
-10
lines changed

6 files changed

+122
-10
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[package]
44
name = "pks"
5-
version = "0.2.21"
5+
version = "0.2.22"
66
edition = "2021"
77
description = "Welcome! Please see https://github.com/rubyatscale/pks for more information!"
88
license = "MIT"
@@ -27,6 +27,7 @@ path = "src/lib.rs"
2727
anyhow = { version = "1.0.75", features = [] } # for error handling
2828
clap = { version = "4.2.1", features = ["derive"] } # cli
2929
clap_derive = "4.2.0" # cli
30+
csv = "1.3.0" # csv de/serialize
3031
itertools = "0.13.0" # tools for iterating over iterable things
3132
jwalk = "0.8.1" # for walking the file tree
3233
path-clean = "1.0.1" # Pathname#cleaname in Ruby

src/packs.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub(crate) mod checker_configuration;
1010
pub(crate) mod configuration;
1111
pub(crate) mod constant_resolver;
1212
pub(crate) mod creator;
13+
pub(crate) mod csv;
1314
pub(crate) mod dependencies;
1415
pub(crate) mod ignored;
1516
pub(crate) mod monkey_patch_detection;
@@ -38,6 +39,7 @@ pub(crate) use self::parsing::ruby::zeitwerk::get_zeitwerk_constant_resolver;
3839
pub(crate) use self::parsing::ParsedDefinition;
3940
pub(crate) use self::parsing::UnresolvedReference;
4041
use anyhow::bail;
42+
use cli::OutputFormat;
4143
pub(crate) use configuration::Configuration;
4244
pub(crate) use package_todo::PackageTodo;
4345

@@ -68,14 +70,24 @@ pub fn create(
6870

6971
pub fn check(
7072
configuration: &Configuration,
73+
output_format: OutputFormat,
7174
files: Vec<String>,
7275
) -> anyhow::Result<()> {
7376
let result = checker::check_all(configuration, files)
7477
.context("Failed to check files")?;
75-
println!("{}", result);
76-
if result.has_violations() {
77-
bail!("Violations found!")
78+
79+
match output_format {
80+
OutputFormat::Packwerk => {
81+
println!("{}", result);
82+
if result.has_violations() {
83+
bail!("Violations found!")
84+
}
85+
}
86+
OutputFormat::CSV => {
87+
csv::write_csv(&result, std::io::stdout())?;
88+
}
7889
}
90+
7991
Ok(())
8092
}
8193

src/packs/checker.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ pub(crate) trait ValidatorInterface {
6464

6565
#[derive(Debug, PartialEq)]
6666
pub struct CheckAllResult {
67-
reportable_violations: HashSet<Violation>,
68-
stale_violations: Vec<ViolationIdentifier>,
69-
strict_mode_violations: HashSet<Violation>,
67+
pub reportable_violations: HashSet<Violation>,
68+
pub stale_violations: Vec<ViolationIdentifier>,
69+
pub strict_mode_violations: HashSet<Violation>,
7070
}
7171

7272
impl CheckAllResult {

src/packs/cli.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::packs;
22

33
use crate::packs::file_utils::get_absolute_path;
4-
use clap::{Parser, Subcommand};
4+
use clap::{Parser, Subcommand, ValueEnum};
55
use clap_derive::Args;
66
use std::path::PathBuf;
77
use tracing::debug;
@@ -70,6 +70,9 @@ enum Command {
7070
#[arg(long)]
7171
ignore_recorded_violations: bool,
7272

73+
#[arg(short, long, default_value = "packwerk")]
74+
output_format: OutputFormat,
75+
7376
files: Vec<String>,
7477
},
7578

@@ -152,6 +155,12 @@ enum Command {
152155
ListDefinitions(ListDefinitionsArgs),
153156
}
154157

158+
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
159+
pub enum OutputFormat {
160+
Packwerk,
161+
CSV,
162+
}
163+
155164
#[derive(Debug, Args)]
156165
struct ListDefinitionsArgs {
157166
/// Show constants with multiple definitions only
@@ -242,11 +251,12 @@ pub fn run() -> anyhow::Result<()> {
242251
Command::ListIncludedFiles => packs::list_included_files(configuration),
243252
Command::Check {
244253
ignore_recorded_violations,
254+
output_format,
245255
files,
246256
} => {
247257
configuration.ignore_recorded_violations =
248258
ignore_recorded_violations;
249-
packs::check(&configuration, files)
259+
packs::check(&configuration, output_format, files)
250260
}
251261
Command::CheckContents {
252262
ignore_recorded_violations,
@@ -257,7 +267,7 @@ pub fn run() -> anyhow::Result<()> {
257267

258268
let absolute_path = get_absolute_path(file.clone(), &configuration);
259269
configuration.stdin_file_path = Some(absolute_path);
260-
packs::check(&configuration, vec![file])
270+
packs::check(&configuration, OutputFormat::Packwerk, vec![file])
261271
}
262272
Command::Update => packs::update(&configuration),
263273
Command::Validate => {

src/packs/csv.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use itertools::chain;
2+
3+
use super::checker::{build_strict_violation_message, CheckAllResult};
4+
5+
pub fn write_csv<W: std::io::Write>(
6+
result: &CheckAllResult,
7+
writer: W,
8+
) -> anyhow::Result<()> {
9+
let mut wtr = csv::Writer::from_writer(writer);
10+
wtr.write_record([
11+
"Violation",
12+
"Strict?",
13+
"File",
14+
"Constant",
15+
"Referencing Pack",
16+
"Defining Pack",
17+
"Message",
18+
])?;
19+
20+
if !&result.reportable_violations.is_empty()
21+
|| !&result.strict_mode_violations.is_empty()
22+
{
23+
let all = chain!(
24+
&result.reportable_violations,
25+
&result.strict_mode_violations
26+
);
27+
28+
for violation in all {
29+
let identifier = &violation.identifier;
30+
let message = if violation.identifier.strict {
31+
build_strict_violation_message(&violation.identifier)
32+
} else {
33+
violation.message.to_string()
34+
};
35+
wtr.serialize((
36+
&identifier.violation_type,
37+
&identifier.strict,
38+
&identifier.file,
39+
&identifier.constant_name,
40+
&identifier.referencing_pack_name,
41+
&identifier.defining_pack_name,
42+
&message,
43+
))?;
44+
}
45+
} else {
46+
wtr.serialize(("No violations detected!", "", "", "", "", "", ""))?;
47+
}
48+
wtr.flush()?;
49+
Ok(())
50+
}

tests/check_test.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,25 @@ fn test_check_with_package_todo_file() -> Result<(), Box<dyn Error>> {
166166
Ok(())
167167
}
168168

169+
#[test]
170+
fn test_check_with_package_todo_file_csv() -> Result<(), Box<dyn Error>> {
171+
Command::cargo_bin("pks")?
172+
.arg("--project-root")
173+
.arg("tests/fixtures/contains_package_todo")
174+
.arg("--debug")
175+
.arg("check")
176+
.arg("-o")
177+
.arg("csv")
178+
.assert()
179+
.success()
180+
.stdout(predicate::str::contains("Violation,Strict?,File,Constant,Referencing Pack,Defining Pack,Message"))
181+
.stdout(predicate::str::contains("No violations detected!"));
182+
183+
common::teardown();
184+
185+
Ok(())
186+
}
187+
169188
#[test]
170189
fn test_check_with_package_todo_file_ignoring_recorded_violations(
171190
) -> Result<(), Box<dyn Error>> {
@@ -308,6 +327,26 @@ fn test_check_with_strict_mode() -> Result<(), Box<dyn Error>> {
308327
Ok(())
309328
}
310329

330+
#[test]
331+
fn test_check_with_strict_mode_output_csv() -> Result<(), Box<dyn Error>> {
332+
Command::cargo_bin("pks")
333+
.unwrap()
334+
.arg("--project-root")
335+
.arg("tests/fixtures/uses_strict_mode")
336+
.arg("check")
337+
.arg("-o")
338+
.arg("csv")
339+
.assert()
340+
.stdout(predicate::str::contains("Violation,Strict?,File,Constant,Referencing Pack,Defining Pack,Message"))
341+
.stdout(predicate::str::contains("privacy,true,packs/foo/app/services/foo.rb,::Bar,packs/foo,packs/bar,packs/foo cannot have privacy violations on packs/bar because strict mode is enabled for privacy violations in the enforcing pack\'s package.yml file"))
342+
.stdout(predicate::str::contains(
343+
"privacy,true,packs/foo/app/services/foo.rb,::Bar,packs/foo,packs/bar,packs/foo cannot have privacy violations on packs/bar because strict mode is enabled for privacy violations in the enforcing pack\'s package.yml file",
344+
));
345+
346+
common::teardown();
347+
Ok(())
348+
}
349+
311350
#[test]
312351
fn test_check_contents() -> Result<(), Box<dyn Error>> {
313352
let project_root = "tests/fixtures/simple_app";

0 commit comments

Comments
 (0)