Skip to content

Commit

Permalink
Update packs to read from configuration of automatic_namespaces gem (#…
Browse files Browse the repository at this point in the history
…214)

* add failing test for automatic namespaces

* add comment

* initial implementation

* make test pass

* bump version

* readme updates
  • Loading branch information
alexevanczuk authored Sep 25, 2024
1 parent 98349c9 commit 4b8fcb3
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 28 deletions.
48 changes: 24 additions & 24 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[package]
name = "pks"
version = "0.2.13"
version = "0.2.14"
edition = "2021"
description = "Welcome! Please see https://github.com/alexevanczuk/packs for more information!"
license = "MIT"
Expand All @@ -28,33 +28,33 @@ name = "packs"
path = "src/lib.rs"

[dependencies]
anyhow = { version = "1.0.75", features = [] } # for error handling
clap = { version = "4.2.1", features = ["derive"] } # cli
clap_derive = "4.2.0" # cli
itertools = "0.13.0" # tools for iterating over iterable things
jwalk = "0.8.1" # for walking the file tree
path-clean = "1.0.1" # Pathname#cleaname in Ruby
rayon = "1.7.0" # for parallel iteration
anyhow = { version = "1.0.75", features = [] } # for error handling
clap = { version = "4.2.1", features = ["derive"] } # cli
clap_derive = "4.2.0" # cli
itertools = "0.13.0" # tools for iterating over iterable things
jwalk = "0.8.1" # for walking the file tree
path-clean = "1.0.1" # Pathname#cleaname in Ruby
rayon = "1.7.0" # for parallel iteration
regex = "1.7.3"
serde = { version = "~1", features = ["derive"] } # de(serialization)
serde_yaml = "0.9.19" # de(serialization)
serde_json = "1.0.96" # de(serialization)
serde_magnus = "0.7.0" # permits a ruby gem to interface with this library
tracing = "0.1.37" # logging
serde = { version = "~1", features = ["derive"] } # de(serialization)
serde_yaml = "0.9.19" # de(serialization)
serde_json = "1.0.96" # de(serialization)
serde_magnus = "0.7.0" # permits a ruby gem to interface with this library
tracing = "0.1.37" # logging
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } # logging
glob = "0.3.1" # globbing
globset = "0.4.10" # globbing
lib-ruby-parser = "4.0.6" # ruby parser
md5 = "0.7.0" # md5 hashing to take and compare md5 digests of file contents to ensure cache validity
line-col = "0.2.1" # for creating source maps of violations
ruby_inflector = '0.0.8' # for inflecting strings, e.g. turning `has_many :companies` into `Company`
petgraph = "0.6.3" # for running graph algorithms (e.g. does the dependency graph contain a cycle?)
glob = "0.3.1" # globbing
globset = "0.4.10" # globbing
lib-ruby-parser = "4.0.6" # ruby parser
md5 = "0.7.0" # md5 hashing to take and compare md5 digests of file contents to ensure cache validity
line-col = "0.2.1" # for creating source maps of violations
ruby_inflector = '0.0.8' # for inflecting strings, e.g. turning `has_many :companies` into `Company`
petgraph = "0.6.3" # for running graph algorithms (e.g. does the dependency graph contain a cycle?)
fnmatch-regex2 = "0.3.0"
strip-ansi-escapes = "0.2.0"

[dev-dependencies]
assert_cmd = "2.0.10" # testing CLI
rusty-hook = "^0.11.2" # git hooks
predicates = "3.0.2" # kind of like rspec assertions
assert_cmd = "2.0.10" # testing CLI
rusty-hook = "^0.11.2" # git hooks
predicates = "3.0.2" # kind of like rspec assertions
pretty_assertions = "1.3.0" # Shows a more readable diff when comparing objects
serial_test = "3.1.1" # Run specific tests in serial
serial_test = "3.1.1" # Run specific tests in serial
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,15 @@ There are still some known behavioral differences between `packs` and `packwerk`
- A `**` in `package_paths` is supported, but is not a substitute for a single `*`, e.g. `packs/**` is supported and will match `packs/*/*/package.yml`, but will not match `packs/*/package.yml`. `packs/*` must be used to match that.

## Default Namespaces
`packs` supports Zeitwerk default namespaces. However, since it doesn't have access to the Rails runtime, you need to explicitly specify the namespaces in `packwerk.yml`.
`packs` supports Zeitwerk default namespaces.

For example, if you're using [`packs-rails`](https://github.com/rubyatscale/packs-rails) and [`automatic_namespaces`](https://github.com/gap777/automatic_namespaces) to configure your default namespaces, and you have
- `packs/foo/app/models/bar.rb` which is configured to define `Foo::Bar`
- `packs/foo/app/domain/baz.rb` which is configured to define `Foo::Baz`

You'll need to specify the default namespaces in `packwerk.yml` like so:
then `packs` will automatically read the configuration as specified in the `automatic_namespaces` gem and should interpret the namespaces correctly. Please file an issue if you find any problems. There is a known limitation here where acronym-based automatic namespaces are not yet supported (feel free to open an issue if you need this).

If you are not using `automatic_namespaces`, you can also explicitly specify the namespaces in `packwerk.yml`, like so:
```yml
autoload_roots:
packs/foo/app/models: "::Foo"
Expand Down
4 changes: 4 additions & 0 deletions src/packs/pack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ impl CheckerSetting {
}

impl Pack {
pub fn last_name(&self) -> &str {
self.name.split('/').last().unwrap()
}

pub fn all_violations(&self) -> Vec<ViolationIdentifier> {
let mut violations = Vec::new();
let violations_by_pack = &self.package_todo.violations_by_defining_pack;
Expand Down
106 changes: 104 additions & 2 deletions src/packs/parsing/ruby/zeitwerk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::packs::{
ConstantDefinition, ConstantResolver, ConstantResolverConfiguration,
},
file_utils::expand_glob,
pack::Pack,
parsing::ruby::rails_utils::get_acronyms_from_disk,
PackSet,
};
Expand All @@ -32,16 +33,117 @@ pub fn get_zeitwerk_constant_resolver(
ZeitwerkConstantResolver::create(constants)
}

#[derive(Debug)]
struct PackNamespaceSettings {
automatic_pack_namespace: bool,
automatic_pack_namespace_exclusions: HashSet<PathBuf>,
}

fn get_pack_namespace_settings(pack: &Pack) -> PackNamespaceSettings {
pack.client_keys
.get("metadata")
.and_then(|metadata| {
if let serde_yaml::Value::Mapping(map) = metadata {
// Extract automatic_pack_namespace
let automatic_pack_namespace = map
.get(&serde_yaml::Value::String(
"automatic_pack_namespace".to_string(),
))
.and_then(|val| match val {
serde_yaml::Value::Bool(b) => Some(*b),
_ => None,
})
.unwrap_or(false); // Default to false if not found or not a boolean

// Extract automatic_pack_namespace_exclusions and combine with pack.yml
let automatic_pack_namespace_exclusions: HashSet<PathBuf> = map
.get(&serde_yaml::Value::String(
"automatic_pack_namespace_exclusions".to_string(),
))
.and_then(|val| match val {
serde_yaml::Value::Sequence(seq) => Some(
seq.iter()
.filter_map(|v| {
v.as_str().map(|s| {
// Combine pack.yml with the exclusion path to form the full absolute path
let mut full_path = pack.yml.clone();
full_path.pop(); // Remove the last component (usually the filename like "pack.yml")
full_path.push(s); // Add the exclusion path
full_path
})
})
.collect(),
),
_ => None,
})
.unwrap_or_default(); // Default to empty set if not found or not a sequence

Some(PackNamespaceSettings {
automatic_pack_namespace,
automatic_pack_namespace_exclusions,
})
} else {
None
}
})
.unwrap_or(PackNamespaceSettings {
automatic_pack_namespace: false,
automatic_pack_namespace_exclusions: HashSet::new(),
}) // Default to false and empty set if metadata doesn't exist
}

fn inferred_constants_from_pack_set(
pack_set: &PackSet,
configuration: &ConstantResolverConfiguration,
) -> Vec<ConstantDefinition> {
// build the full list of default autoload roots from the pack set, using the default namespace for each.
// There is one exception to using the default namespace:
// Each pack may have metadata that takes this shape:
// metadata:
// automatic_pack_namespace: true
// automatic_pack_namespace_exclusions:
// - app/models # Exclude models
// For packs that have this configuration, if the autoload root is not in the list of automatic_pack_namespace_exclusions,
// set the namespace associated with that root to inflector_shim::camelize(pack.name).
let mut full_autoload_roots: HashMap<PathBuf, String> = pack_set
.packs
.iter()
.flat_map(|pack| pack.default_autoload_roots())
.map(|path| (path, String::from("")))
.flat_map(|pack| {
let default_roots = pack.default_autoload_roots();

// Check if metadata exists and automatic_pack_namespace is set to true

let PackNamespaceSettings {
automatic_pack_namespace,
automatic_pack_namespace_exclusions,
} = get_pack_namespace_settings(pack);

// Build the autoload roots
default_roots.into_iter().map(move |path| {
if automatic_pack_namespace
&& !automatic_pack_namespace_exclusions.contains(&path)
{
// Pass an empty set of acronyms as the second argument
// NOTE: This is not the correct implementation – if we want automatic namespacing to work with
// acronym-based pack names, we need to pull from the file, preferably from the cache.
let empty_acronyms = HashSet::new();

// Camelized pack namespace based on pack name with leading double colon:
// e.g. pack name "packs/my_pack" -> "::MyPack"
let namespace = format!(
"::{}",
inflector_shim::camelize(
pack.last_name(),
&empty_acronyms,
)
);

(path, namespace)
} else {
(path, String::from("")) // default namespace handling
}
})
})
.collect();

// override the default autoload roots with any that may have been explicitly specified.
Expand Down
44 changes: 44 additions & 0 deletions tests/automatic_namespaces_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::{error::Error, process::Command};
mod common;

#[test]
fn test_automatic_namespaces_with_zeitwerk_parser() -> Result<(), Box<dyn Error>>
{
Command::cargo_bin("packs")?
.arg("--project-root")
.arg("tests/fixtures/app_with_automatic_namespaces")
.arg("--debug")
.arg("list-definitions")
.assert()
.success()
.stdout(predicate::str::contains(
"\"::FooRecord\" is defined at \"packs/foo/app/models/foo_record.rb\""
))
.stdout(predicate::str::contains(
"\"::Foo::Creator\" is defined at \"packs/foo/app/services/creator.rb\""
));
Ok(())
}

#[test]
fn test_automatic_namespaces_with_experimental_parser(
) -> Result<(), Box<dyn Error>> {
Command::cargo_bin("packs")?
.arg("--project-root")
.arg("tests/fixtures/app_with_automatic_namespaces")
.arg("--debug")
// Experimental parser works without issues
.arg("--experimental-parser")
.arg("list-definitions")
.assert()
.success()
.stdout(predicate::str::contains(
"\"::FooRecord\" is defined at \"packs/foo/app/models/foo_record.rb\""
))
.stdout(predicate::str::contains(
"\"::Foo::Creator\" is defined at \"packs/foo/app/services/creator.rb\""
));
Ok(())
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class FooRecord
sig { returns(String) }
def self.other_method
"other_method"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Foo
class Creator
sig { returns(String) }
def self.build_foo
"foo"
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
enforce_dependencies: true
enforce_privacy: true
metadata:
automatic_pack_namespace: true
automatic_pack_namespace_exclusions:
- app/models # Exclude models
22 changes: 22 additions & 0 deletions tests/fixtures/app_with_automatic_namespaces/packwerk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# See: Setting up the configuration file
# https://github.com/Shopify/packwerk/blob/main/USAGE.md#setting-up-the-configuration-file

# List of patterns for folder paths to include
# include:
# - "**/*.{rb,rake,erb}"

# List of patterns for folder paths to exclude
exclude:
- "{rubydir_stub,gemdir_stub}/**/*"

# Patterns to find package configuration files
# package_paths: "**/"

# List of custom associations, if any
# custom_associations:
# - "cache_belongs_to"

# Whether or not you want the cache enabled (disabled by default)
cache: false
# Where you want the cache to be stored (default below)
# cache_directory: 'tmp/cache/packwerk'

0 comments on commit 4b8fcb3

Please sign in to comment.