-
Notifications
You must be signed in to change notification settings - Fork 0
Add package_todo.yml format validation to validate command
#20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
23a6fe3
bed9c13
d8bb42b
64730b8
03fa194
08291ab
6dca901
8139c56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,130 @@ | ||||||
| //! Package todo format validation checker. | ||||||
| //! | ||||||
| //! This module provides validation to ensure that package_todo.yml files maintain | ||||||
| //! the correct serialization format. This prevents issues where manual edits | ||||||
| //! (such as mass search-replace operations when renaming packs) result in | ||||||
| //! incorrectly formatted files that create noise when running `pks update`. | ||||||
|
|
||||||
| use std::fs; | ||||||
| use std::path::Path; | ||||||
|
|
||||||
| use crate::packs::bin_locater; | ||||||
| use crate::packs::{Configuration, PackageTodo}; | ||||||
| use anyhow::{anyhow, Result}; | ||||||
| use rayon::prelude::*; | ||||||
|
|
||||||
| use super::ValidatorInterface; | ||||||
|
|
||||||
| /// Validator that checks package_todo.yml files for correct serialization format. | ||||||
| /// | ||||||
| /// This validator ensures that existing package_todo.yml files match their expected | ||||||
| /// serialized format by: | ||||||
| /// 1. Reading the current file content | ||||||
| /// 2. Deserializing it to a PackageTodo struct | ||||||
| /// 3. Re-serializing it using the standard serialization logic | ||||||
| /// 4. Comparing the result with the original file content | ||||||
| /// | ||||||
| /// If differences are found, it reports validation errors with context-aware | ||||||
| /// suggestions for the correct command to run based on packs_first_mode. | ||||||
| pub struct Checker; | ||||||
|
|
||||||
| impl ValidatorInterface for Checker { | ||||||
| /// Validates that all existing package_todo.yml files are correctly formatted. | ||||||
| /// | ||||||
| /// Iterates through all packs and checks their package_todo.yml files (if they exist) | ||||||
| /// to ensure they match the expected serialization format. | ||||||
| /// | ||||||
| /// # Returns | ||||||
| /// - `None` if all files are correctly formatted | ||||||
| /// - `Some(Vec<String>)` containing error messages for incorrectly formatted files | ||||||
| fn validate(&self, configuration: &Configuration) -> Option<Vec<String>> { | ||||||
| let validation_errors: Vec<String> = configuration | ||||||
| .pack_set | ||||||
| .packs | ||||||
| .par_iter() | ||||||
| .filter_map(|pack| { | ||||||
| let package_todo_path = pack | ||||||
| .yml | ||||||
| .parent() | ||||||
| .expect("Pack yml file should have a parent directory") | ||||||
| .join("package_todo.yml"); | ||||||
|
|
||||||
| // Skip packs that don't have package_todo.yml files | ||||||
| if !package_todo_path.exists() { | ||||||
| return None; | ||||||
| } | ||||||
|
|
||||||
| validate_package_todo_format( | ||||||
| &package_todo_path, | ||||||
| &pack.name, | ||||||
| configuration.packs_first_mode, | ||||||
| ) | ||||||
| .err() | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If using
Suggested change
|
||||||
| .map(|e| e.to_string()) | ||||||
| }) | ||||||
| .collect(); | ||||||
|
|
||||||
| if validation_errors.is_empty() { | ||||||
| None | ||||||
| } else { | ||||||
| Some(validation_errors) | ||||||
| } | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is totally legit and there's nothing wrong with it, but I would prefer pattern matching in general. match &validation_errors[..] {
[] => None,
_ => Some(validation_errors)
}I whipped up a more complete example of matching over slices for you. To be 100% clear, I don't actually have a problem with the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes! This is exactly the kind of feedback I'm looking for. Thank you! |
||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Validates the format of a single package_todo.yml file. | ||||||
| /// | ||||||
| /// This function implements the core validation logic: | ||||||
| /// 1. Reads the current file content | ||||||
| /// 2. Deserializes it to ensure it's valid YAML and matches PackageTodo structure | ||||||
| /// 3. Re-serializes it using the standard serialization logic | ||||||
| /// 4. Compares the result with the original content | ||||||
| /// | ||||||
| /// # Arguments | ||||||
| /// * `package_todo_path` - Path to the package_todo.yml file to validate | ||||||
| /// * `pack_name` - Name of the pack (used for generating the correct header) | ||||||
| /// * `packs_first_mode` - Whether the project uses packs.yml (affects command suggestions) | ||||||
| /// | ||||||
| /// # Returns | ||||||
| /// * `Ok(())` if the file is correctly formatted | ||||||
| /// * `Err(String)` with a descriptive error message if validation fails | ||||||
| /// | ||||||
| /// # Common causes of validation failures | ||||||
| /// - Missing `---` separator after header comments | ||||||
| /// - Incorrect ordering of violations or files (should be alphabetically sorted) | ||||||
| /// - Manual edits that break the standard serialization format | ||||||
| /// - Wrong header comment (should match packs_first_mode setting) | ||||||
| fn validate_package_todo_format( | ||||||
| package_todo_path: &Path, | ||||||
| pack_name: &str, | ||||||
| packs_first_mode: bool, | ||||||
| ) -> Result<()> { | ||||||
| // Read the current file content | ||||||
| let current_content = fs::read_to_string(package_todo_path)?; | ||||||
|
|
||||||
| // Deserialize to ensure the file is valid and can be parsed | ||||||
| let package_todo: PackageTodo = serde_yaml::from_str(¤t_content)?; | ||||||
|
|
||||||
| // Re-serialize using the standard serialization logic to get the expected format | ||||||
| let expected_content = crate::packs::package_todo::serialize_package_todo( | ||||||
| pack_name, | ||||||
| &package_todo, | ||||||
| packs_first_mode, | ||||||
| ); | ||||||
|
|
||||||
| // Compare the current content with the expected serialized format | ||||||
| if current_content != expected_content { | ||||||
| let command = if packs_first_mode { | ||||||
| format!("{} update", bin_locater::packs_bin_name()) | ||||||
| } else { | ||||||
| "bin/packwerk update-todo".to_string() | ||||||
| }; | ||||||
| return Err(anyhow!( | ||||||
| "Package todo file {} is not in the expected format. Please run `{}` to fix it.", | ||||||
| package_todo_path.display(), | ||||||
| command | ||||||
| )); | ||||||
| } | ||||||
|
|
||||||
| Ok(()) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a place I would definitely prefer pattern matching over an match current_content {
expected_content => Ok(()),
_ => {
Err(/*...*/)
}
} |
||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -171,8 +171,35 @@ pub fn write_violations_to_disk( | |
| debug!("Finished writing violations to disk"); | ||
| } | ||
|
|
||
| fn serialize_package_todo( | ||
| responsible_pack_name: &String, | ||
| /// Serializes a PackageTodo struct into the standard package_todo.yml format. | ||
| /// | ||
| /// This function generates the canonical serialized representation of a package_todo.yml file, | ||
| /// including the standard header comments and properly formatted YAML content. The output | ||
| /// format is deterministic and consistent, which is critical for format validation. | ||
| /// | ||
| /// # Arguments | ||
| /// * `responsible_pack_name` - The name of the pack that owns this package_todo.yml file | ||
| /// * `package_todo` - The PackageTodo struct containing violation data to serialize | ||
| /// * `packs_first_mode` - Whether the project uses packs.yml (affects header command) | ||
| /// | ||
| /// # Returns | ||
| /// A String containing the complete package_todo.yml file content, including: | ||
| /// - Standard header comments explaining the file's purpose | ||
| /// - Appropriate regeneration command based on packs_first_mode | ||
| /// - YAML separator (`---`) | ||
| /// - Sorted violation data in the standard format | ||
| /// | ||
| /// # Format Details | ||
| /// The serialized format ensures: | ||
| /// - Violations are sorted alphabetically by defining pack, then constant name | ||
| /// - Files within each violation are sorted alphabetically | ||
| /// - Constant names are properly quoted (using a hack to work around serde_yaml limitations) | ||
| /// - The header matches the project's configuration (packs_first_mode) | ||
| /// | ||
| /// This function is used both for writing new package_todo.yml files and for | ||
| /// format validation to ensure existing files match the expected format. | ||
| pub(crate) fn serialize_package_todo( | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not super familiar with Rust's modules. Is this a code smell? I didn't want to dive into the "how should we organize and expose these types/methods" rabbit hole just yet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems reasonable, if you don't want the method to be part of the public API. If you really want to limit the usage you can declare |
||
| responsible_pack_name: &str, | ||
| package_todo: &PackageTodo, | ||
| packs_first_mode: bool, | ||
| ) -> String { | ||
|
|
@@ -223,7 +250,7 @@ fn delete_package_todo_from_disk(responsible_pack: &Pack) { | |
| } | ||
| } | ||
|
|
||
| fn header(responsible_pack_name: &String, packs_first_mode: bool) -> String { | ||
| fn header(responsible_pack_name: &str, packs_first_mode: bool) -> String { | ||
| let command = if packs_first_mode { | ||
| "pks update" | ||
| } else { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| enforce_privacy: false | ||
| enforce_dependencies: false |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| enforce_privacy: true | ||
| enforce_dependencies: true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # This file is incorrectly formatted (not ordered properly) | ||
| --- | ||
| packs/bar: | ||
| "::Baz": | ||
| violations: | ||
| - privacy | ||
| - dependency | ||
| files: | ||
| - packs/foo/app/services/foo.rb | ||
| "::Bar": | ||
| violations: | ||
| - dependency | ||
| files: | ||
| - packs/foo/app/services/foo.rb |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| include: | ||
| - "**/*.rb" | ||
| - "**/*.rake" | ||
| exclude: | ||
| - "{bin,node_modules,script,tmp,vendor}/**/*" | ||
| packs_first_mode: false |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be nice to have a test that ensures packs without a package todo file are correctly skipped. I wouldn't consider this a blocker though! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| //! Tests for package_todo.yml format validation. | ||
| //! | ||
| //! These tests verify that the `pks validate` command correctly identifies | ||
| //! when package_todo.yml files are not in the expected serialization format | ||
| //! and provides appropriate error messages and suggestions. | ||
|
|
||
| use assert_cmd::prelude::*; | ||
| use predicates::prelude::*; | ||
| use std::{error::Error, process::Command}; | ||
|
|
||
| mod common; | ||
|
|
||
| /// Tests that validation fails for incorrectly formatted package_todo.yml files. | ||
| /// | ||
| /// This test uses a fixture with a package_todo.yml file that has violations in | ||
| /// the wrong order (::Baz should come after ::Bar when sorted alphabetically). | ||
| /// The validation should fail and suggest running the appropriate update command. | ||
| #[test] | ||
| fn test_validate_incorrectly_formatted_package_todo( | ||
| ) -> Result<(), Box<dyn Error>> { | ||
| Command::cargo_bin("pks") | ||
| .unwrap() | ||
| .arg("--project-root") | ||
| .arg("tests/fixtures/incorrectly_formatted_package_todo") | ||
| .arg("validate") | ||
| .assert() | ||
| .failure() | ||
| .stdout(predicate::str::contains("1 validation error(s) detected:")) | ||
| .stdout(predicate::str::contains("is not in the expected format")) | ||
| .stdout(predicate::str::contains("bin/packwerk update-todo")); | ||
|
|
||
| common::teardown(); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Tests that validation passes for correctly formatted package_todo.yml files. | ||
| /// | ||
| /// This test uses an existing fixture that has a properly formatted package_todo.yml | ||
| /// file (with correct ordering, headers, and format). The validation should succeed. | ||
| #[test] | ||
| fn test_validate_correctly_formatted_package_todo() -> Result<(), Box<dyn Error>> | ||
| { | ||
| Command::cargo_bin("pks") | ||
| .unwrap() | ||
| .arg("--project-root") | ||
| .arg("tests/fixtures/contains_package_todo") | ||
| .arg("validate") | ||
| .assert() | ||
| .success() | ||
| .stdout(predicate::str::contains("Packwerk validate succeeded!")); | ||
|
|
||
| common::teardown(); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Tests that validation passes when violations exist but no package_todo.yml files. | ||
| /// | ||
| /// This test uses a fixture with actual privacy violations but no package_todo.yml files | ||
| /// and verifies that the todo format validator still succeeds (it only validates existing | ||
| /// todo files, not whether violations should have todo files). | ||
| #[test] | ||
| fn test_validate_passes_when_violations_exist_but_no_todo_file( | ||
| ) -> Result<(), Box<dyn Error>> { | ||
| Command::cargo_bin("pks") | ||
| .unwrap() | ||
| .arg("--project-root") | ||
| .arg("tests/fixtures/privacy_violation_overrides") | ||
| .arg("validate") | ||
| .assert() | ||
| .success() | ||
| .stdout(predicate::str::contains("Packwerk validate succeeded!")); | ||
|
|
||
| common::teardown(); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Tests that validation succeeds when there are no violations and no package_todo.yml files. | ||
| /// | ||
| /// This test uses a clean fixture with no violations and no package_todo.yml files | ||
| /// to verify the validator succeeds in the simplest case. | ||
| #[test] | ||
| fn test_validate_succeeds_when_no_violations_and_no_todo_files( | ||
| ) -> Result<(), Box<dyn Error>> { | ||
| Command::cargo_bin("pks") | ||
| .unwrap() | ||
| .arg("--project-root") | ||
| .arg("tests/fixtures/simple_app") | ||
| .arg("validate") | ||
| .assert() | ||
| .success() | ||
| .stdout(predicate::str::contains("Packwerk validate succeeded!")); | ||
|
|
||
| common::teardown(); | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see now. We've introduced a new
package_todomodule when there was already an existing one in thecheckernamespace. That's why you're having namespace collisions.