Skip to content
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

Renaming architecture to layer #53

Merged
merged 4 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ GIT
PATH
remote: .
specs:
packwerk-extensions (0.1.11)
packwerk-extensions (0.2.0)
packwerk (>= 2.2.1)
railties (>= 6.0.0)
sorbet-runtime
Expand Down
35 changes: 28 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Currently, it ships the following checkers to help improve the boundaries betwee
- A `privacy` checker that ensures other packages are using your package's public API
- A `visibility` checker that allows packages to be private except to an explicit group of other packages.
- A `folder_visibility` checker that allows packages to their sibling packs and parent pack (to be used in an application that uses folder packs)
- An `architecture` checker that allows packages to specify their "layer" and requires that each layer only communicate with layers below it.
- A `layer` (formerly `architecture`) checker that allows packages to specify their "layer" and requires that each layer only communicate with layers below it.

## Installation

Expand All @@ -26,7 +26,7 @@ require:
- packwerk/privacy/checker
- packwerk/visibility/checker
- packwerk/folder_visibility/checker
- packwerk/architecture/checker
- packwerk/layer/checker
```

## Privacy Checker
Expand Down Expand Up @@ -194,25 +194,46 @@ packs/b/packs/h OK (sibling)
packs/c VIOLATION
```

## Architecture Checker
The architecture checker can be used to enforce constraints on what can depend on what.
## Layer Checker
The layer checker can be used to enforce constraints on what can depend on what.

To enforce architecture for your package, first define the `architecture_layers` in `packwerk.yml`, for example:
To enforce layers for your package, first define the `layers` in `packwerk.yml`, for example:
```
architecture_layers:
layers:
- package
- utility
```

Then, turn on the checker in your package:
```yaml
# components/merchandising/package.yml
enforce_architecture: true
enforce_layers: true
layer: utility
```

Now this pack can only depend on other utility packages.

### Deprecated Architecture Checker
The "Layer Checker" was formerly named "Architecture Checker". The associated keys were:
- packwerk.yml `architecture_layers`, which is now `layers`
- package.yml `enforce_architecture`, which is now `enforce_layers`
- package.yml `layer` is still a valid key
- package_todo.yml - `architecture`, which is now `layer`

```bash
# script to migrate code from deprecated "architecture" violations to "layer" violations
# sed and ripgrep required

# replace 'architecture_layers' with 'layers' in packwerk.yml
sed -i '' 's/architecture_layers/layers/g' ./packwerk.yml

# replace 'enforce_architecture' with 'enforce_layers' in package.yml files
`rg -l 'enforce_architecture' -g 'package.yml' | xargs sed -i '' 's,enforce_architecture,enforce_layers,g'`

# replace '- architecture' with '- layer' in package_todo.yml files
`rg -l 'architecture' -g 'package_todo.yml' | xargs sed -i '' 's/- architecture/- layer/g'`
```
perryqh marked this conversation as resolved.
Show resolved Hide resolved


## Contributing

Expand Down
2 changes: 1 addition & 1 deletion lib/packwerk-extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
require 'packwerk/privacy/checker'
require 'packwerk/visibility/checker'
require 'packwerk/folder_visibility/checker'
require 'packwerk/architecture/checker'
require 'packwerk/layer/checker'

module Packwerk
module Extensions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
# typed: strict
# frozen_string_literal: true

require 'packwerk/architecture/layers'
require 'packwerk/architecture/package'
require 'packwerk/architecture/validator'
require 'packwerk/layer/config'
require 'packwerk/layer/layers'
require 'packwerk/layer/package'
require 'packwerk/layer/validator'

module Packwerk
module Architecture
module Layer
# This enforces "layered architecture," which allows each class to be designated as one of N layers
# configured by the client in `packwerk.yml`, for example:
#
# architecture_layers:
# layers:
# - orchestrator
# - business_domain
# - platform
# - utility
# - specification
#
# Then a package can configure:
# enforce_architecture: true | false | strict
# enforce_layers: true | false | strict
# layer: utility
#
# This is intended to provide:
Expand All @@ -30,11 +31,14 @@ class Checker
extend T::Sig
include Packwerk::Checker

VIOLATION_TYPE = T.let('architecture', String)
sig { void }
def initialize
@violation_type = T.let(@violation_type, T.nilable(String))
end

sig { override.returns(String) }
def violation_type
VIOLATION_TYPE
@violation_type ||= layer_config.violation_key
end

sig do
Expand All @@ -55,7 +59,7 @@ def invalid_reference?(reference)
end
def strict_mode_violation?(listed_offense)
constant_package = listed_offense.reference.package
constant_package.config['enforce_architecture'] == 'strict'
constant_package.config[layer_config.enforce_key] == 'strict'
end

sig do
Expand All @@ -68,9 +72,9 @@ def message(reference)
referencing_package = Package.from(reference.package, layers)

message = <<~MESSAGE
Architecture layer violation: '#{reference.constant.name}' belongs to '#{reference.constant.package}', whose architecture layer type is "#{constant_package.layer}."
This constant cannot be referenced by '#{reference.package}', whose architecture layer type is "#{referencing_package.layer}."
Packs in a lower layer may not access packs in a higher layer. See the `architecture_layers` in packwerk.yml. Current hierarchy:
Layer violation: '#{reference.constant.name}' belongs to '#{reference.constant.package}', whose layer type is "#{constant_package.layer}".
This constant cannot be referenced by '#{reference.package}', whose layer type is "#{referencing_package.layer}".
Packs in a lower layer may not access packs in a higher layer. See the `layers` in packwerk.yml. Current hierarchy:
- #{layers.names_list.join("\n- ")}

#{standard_help_message(reference)}
Expand All @@ -92,7 +96,12 @@ def standard_help_message(reference)

sig { returns(Layers) }
def layers
@layers ||= T.let(Layers.new, T.nilable(Packwerk::Architecture::Layers))
@layers ||= T.let(Layers.new, T.nilable(Packwerk::Layer::Layers))
end

sig { returns(Config) }
def layer_config
@layer_config ||= T.let(Config.new, T.nilable(Config))
end
end
end
Expand Down
46 changes: 46 additions & 0 deletions lib/packwerk/layer/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# typed: strict
# frozen_string_literal: true

module Packwerk
module Layer
class Config
extend T::Sig

ARCHITECTURE_VIOLATION_TYPE = T.let('architecture', String)
ARCHITECTURE_ENFORCE = T.let('enforce_architecture', String)
LAYER_VIOLATION_TYPE = T.let('layer', String)
LAYER_ENFORCE = T.let('enforce_layers', String)

sig { void }
def initialize
@layers_key_configured = T.let(@layers_key_configured, T.nilable(T::Boolean))
@layers_list = T.let(@layers_list, T.nilable(T::Array[String]))
end

sig { returns(T::Array[String]) }
def layers_list
@layers_list ||= YAML.load_file('packwerk.yml')[layers_key] || []
end

sig { returns(T::Boolean) }
def layers_key_configured?
@layers_key_configured ||= YAML.load_file('packwerk.yml')['architecture_layers'].nil?
end
perryqh marked this conversation as resolved.
Show resolved Hide resolved

sig { returns(String) }
def layers_key
layers_key_configured? ? 'layers' : 'architecture_layers'
end

sig { returns(String) }
def violation_key
layers_key_configured? ? LAYER_VIOLATION_TYPE : ARCHITECTURE_VIOLATION_TYPE
end

sig { returns(String) }
def enforce_key
layers_key_configured? ? LAYER_ENFORCE : ARCHITECTURE_ENFORCE
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# frozen_string_literal: true

module Packwerk
module Architecture
module Layer
class Layers
extend T::Sig

Expand All @@ -29,7 +29,7 @@ def names

sig { returns(T::Array[String]) }
def names_list
@names_list ||= YAML.load_file('packwerk.yml')['architecture_layers'] || []
@names_list ||= Config.new.layers_list
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# frozen_string_literal: true

module Packwerk
module Architecture
module Layer
class Package < T::Struct
extend T::Sig

Expand Down Expand Up @@ -46,7 +46,7 @@ def from(package, layers)

Package.new(
layer: layer,
enforcement_setting: config['enforce_architecture'],
enforcement_setting: config[Config.new.enforce_key],
config: config
)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# frozen_string_literal: true

module Packwerk
module Architecture
module Layer
class Validator
extend T::Sig
include Packwerk::Validator
Expand All @@ -16,13 +16,17 @@ def call(package_set, configuration)
package_set.each do |package|
config = package.config
f = Pathname.new(package.name).join('package.yml').to_s
package = Package.from(package, layers)

next if !config

result = check_enforce_architecture_setting(f, config['enforce_architecture'])
result = check_enforce_key(package, f, config)
results << result
next if !result.ok?

package = Package.from(package, layers)
result = check_enforce_layers_setting(f, config[layer_config.enforce_key])
results << result
next if !result.ok?

result = check_layer_setting(package, f)
results << result
Expand All @@ -34,12 +38,39 @@ def call(package_set, configuration)

sig { returns(Layers) }
def layers
@layers ||= T.let(Layers.new, T.nilable(Packwerk::Architecture::Layers))
@layers ||= T.let(Layers.new, T.nilable(Packwerk::Layer::Layers))
end

sig { returns(Config) }
def layer_config
@layer_config ||= T.let(Config.new, T.nilable(Config))
end

sig { override.returns(T::Array[String]) }
def permitted_keys
%w[enforce_architecture layer]
[layer_config.enforce_key, 'layer']
end

sig do
params(package: Package, config_file_path: String, config: T::Hash[T.untyped, T.untyped]).returns(Result)
end
def check_enforce_key(package, config_file_path, config)
enforce_layer_present = !config[Config::LAYER_ENFORCE].nil?
enforce_architecture_present = !config[Config::ARCHITECTURE_ENFORCE].nil?

if layer_config.enforce_key == Config::LAYER_ENFORCE && enforce_architecture_present
Result.new(
ok: false,
error_value: "Unexpected `enforce_architecture` option in #{config_file_path.inspect}. Did you mean `enforce_layers`?"
)
elsif layer_config.enforce_key == Config::ARCHITECTURE_ENFORCE && enforce_layer_present
Result.new(
ok: false,
error_value: "Unexpected `enforce_layers` option in #{config_file_path.inspect}. Did you mean `enforce_architecture`?"
)
else
Result.new(ok: true)
end
end

sig do
Expand All @@ -52,7 +83,7 @@ def check_layer_setting(package, config_file_path)
if layer.nil? && package.enforces?
Result.new(
ok: false,
error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{package.layer.inspect}. `layer` must be set if `enforce_architecture` is on."
error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{package.layer.inspect}. `layer` must be set if `#{layer_config.enforce_key}` is on."
)
elsif valid_layer
Result.new(ok: true)
Expand All @@ -67,19 +98,19 @@ def check_layer_setting(package, config_file_path)
sig do
params(config_file_path: String, setting: T.untyped).returns(Result)
end
def check_enforce_architecture_setting(config_file_path, setting)
def check_enforce_layers_setting(config_file_path, setting)
activated_value = [true, 'strict'].include?(setting)
valid_value = [true, nil, false, 'strict'].include?(setting)
layers_set = layers.names.any?
if !valid_value
Result.new(
ok: false,
error_value: "Invalid 'enforce_architecture' option in #{config_file_path.inspect}: #{setting.inspect}"
error_value: "Invalid '#{layer_config.enforce_key}' option in #{config_file_path.inspect}: #{setting.inspect}"
)
elsif activated_value && !layers_set
Result.new(
ok: false,
error_value: "Cannot set 'enforce_architecture' option in #{config_file_path.inspect} until `architectural_layers` have been specified in `packwerk.yml`"
error_value: "Cannot set '#{layer_config.enforce_key}' option in #{config_file_path.inspect} until `layers` have been specified in `packwerk.yml`"
)
else
Result.new(ok: true)
Expand Down
2 changes: 1 addition & 1 deletion lib/packwerk/visibility/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def call(package_set, configuration)
visible_settings = package_manifests_settings_for(configuration, 'visible_to')
results = T.let([], T::Array[Result])

all_package_names = package_set.map(&:name).to_set
all_package_names = package_set.to_set(&:name)

package_manifests_settings_for(configuration, 'enforce_visibility').each do |config, setting|
next if setting.nil?
Expand Down
4 changes: 2 additions & 2 deletions packwerk-extensions.gemspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Gem::Specification.new do |spec|
spec.name = 'packwerk-extensions'
spec.version = '0.1.11'
spec.version = '0.2.0'
spec.authors = ['Gusto Engineers']
spec.email = ['[email protected]']

Expand All @@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
'public gem pushes.'
end

spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
spec.required_ruby_version = Gem::Requirement.new('>= 2.7')
# Specify which files should be added to the gem when it is released.
spec.files = Dir['README.md', 'lib/**/*']

Expand Down
Loading