packwerk-extensions 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +41 -23
- data/lib/packwerk/architecture/checker.rb +96 -0
- data/lib/packwerk/architecture/layers.rb +38 -0
- data/lib/packwerk/architecture/package.rb +54 -0
- data/lib/packwerk/architecture/validator.rb +112 -0
- data/lib/packwerk-extensions.rb +4 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 558107be67a748dfb9a175b9d90be2acc373913f36a66c5bccbb3401e80cd85f
|
4
|
+
data.tar.gz: fb98b8c64bb3043d267aab3d966d404934a5a530020dc18e3e599a13153fe121
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6beb350fd3089406f7a0ec0409b79f261136b89ffe0ede267215893ca7abb9f5613a7aaf44248f1c0b53560a0596a78739568f85bd2a081edf4b3d0f69fe0d0
|
7
|
+
data.tar.gz: 706bc3779b3af6ce3d6780ba9106a8bb4fc84cd81ddd93c7809b62f2663de1baf4c906f961abc6a4982c8815142478d839054b27c504639e1eb5dc88e0b267e3
|
data/README.md
CHANGED
@@ -2,40 +2,29 @@
|
|
2
2
|
|
3
3
|
`packwerk-extensions` is a home for checker extensions for packwerk.
|
4
4
|
|
5
|
-
|
5
|
+
Currently, it ships the following checkers to help improve the boundaries between packages. These checkers are:
|
6
|
+
- A `privacy` checker that ensures other packages are using your package's public API
|
7
|
+
- A `visibility` checker that allows packages to be private except to an explicit group of other packages.
|
8
|
+
- An experimental `architecture` checker that allows packages to specify their "layer" and requires that each layer only communicate with layers below it.
|
6
9
|
|
10
|
+
## Privacy Checker
|
7
11
|
The privacy checker extension was originally extracted from [packwerk](https://github.com/Shopify/packwerk).
|
8
12
|
|
9
13
|
A package's privacy boundary is violated when there is a reference to the package's private constants from a source outside the package.
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
1. Enforce privacy for all external sources
|
15
|
+
To enforce privacy for your package, set `enforce_privacy` to `true` on your pack:
|
14
16
|
|
15
17
|
```yaml
|
16
18
|
# components/merchandising/package.yml
|
17
|
-
enforce_privacy: true
|
18
|
-
# the components/merchandising/app/public folder
|
19
|
+
enforce_privacy: true
|
19
20
|
```
|
20
21
|
|
21
22
|
Setting `enforce_privacy` to true will make all references to private constants in your package a violation.
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
```yaml
|
26
|
-
# components/merchandising/package.yml
|
27
|
-
enforce_privacy:
|
28
|
-
- "::Merchandising::Product"
|
29
|
-
- "::SomeNamespace" # enforces privacy for the namespace and
|
30
|
-
# everything nested in it
|
31
|
-
```
|
32
|
-
|
33
|
-
It will be a privacy violation when a file outside of the `components/merchandising` package tries to reference `Merchandising::Product`.
|
34
|
-
|
35
|
-
##### Using public folders
|
24
|
+
### Using public folders
|
36
25
|
You may enforce privacy either way mentioned above and still expose a public API for your package by placing constants in the public folder, which by default is `app/public`. The constants in the public folder will be made available for use by the rest of the application.
|
37
26
|
|
38
|
-
|
27
|
+
### Defining your own public folder
|
39
28
|
|
40
29
|
You may prefer to override the default public folder, you can do so on a per-package basis by defining a `public_path`.
|
41
30
|
|
@@ -48,8 +37,6 @@ public_path: my/custom/path/
|
|
48
37
|
### Package Privacy violation
|
49
38
|
A constant that is private to its package has been referenced from outside of the package. Constants are declared private in their package’s `package.yml`.
|
50
39
|
|
51
|
-
See: [USAGE.md - Enforcing privacy boundary](USAGE.md#Enforcing-privacy-boundary)
|
52
|
-
|
53
40
|
#### Interpreting Privacy violation
|
54
41
|
|
55
42
|
> /Users/JaneDoe/src/github.com/sample-project/user/app/controllers/labels_controller.rb:170:30
|
@@ -60,7 +47,38 @@ See: [USAGE.md - Enforcing privacy boundary](USAGE.md#Enforcing-privacy-boundary
|
|
60
47
|
|
61
48
|
There has been a privacy violation of the package `billing` in the package `user`, through the use of the constant `Billing::CarrierInvoiceTransaction` in the file `user/app/controllers/labels_controller.rb`.
|
62
49
|
|
63
|
-
|
50
|
+
#### Suggestions
|
64
51
|
You may be accessing the implementation of a piece of functionality that is supposed to be accessed through a public interface on the package. Try to use the public interface instead. A package’s public interface should be defined in its `app/public` folder and documented.
|
65
52
|
|
66
53
|
The functionality you’re looking for may not be intended to be reused across packages at all. If there is no public interface for it but you have a good reason to use it from outside of its package, find the people responsible for the package and discuss a solution with them.
|
54
|
+
|
55
|
+
## Visibility Checker
|
56
|
+
The visibility checker can be used to allow a package to be private implementation detail of other packages.
|
57
|
+
|
58
|
+
To enforce visibility for your package, set `enforce_visibility` to `true` on your pack and specify `visible_to` for other packages that can use your package.
|
59
|
+
|
60
|
+
```yaml
|
61
|
+
# components/merchandising/package.yml
|
62
|
+
enforce_visibility: true
|
63
|
+
visible_to:
|
64
|
+
- components/other_package
|
65
|
+
```
|
66
|
+
|
67
|
+
## Architecture Checker
|
68
|
+
The architecture checker can be used to enforce constraints on what can depend on what.
|
69
|
+
|
70
|
+
To enforce architecture for your package, first define the `architecture_layers` in `packwerk.yml`, for example:
|
71
|
+
```
|
72
|
+
architecture_layers:
|
73
|
+
- package
|
74
|
+
- utility
|
75
|
+
```
|
76
|
+
|
77
|
+
Then, turn on the checker in your package:
|
78
|
+
```yaml
|
79
|
+
# components/merchandising/package.yml
|
80
|
+
enforce_architecture: true
|
81
|
+
layer: utility
|
82
|
+
```
|
83
|
+
|
84
|
+
Now this pack can only depend on other utility packages.
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'packwerk/architecture/layers'
|
5
|
+
|
6
|
+
module Packwerk
|
7
|
+
module Architecture
|
8
|
+
# This enforces "layered architecture," which allows each class to be designated as one of N layers
|
9
|
+
# configured by the client in `packwerk.yml`, for example:
|
10
|
+
#
|
11
|
+
# architecture_layers:
|
12
|
+
# - orchestrator
|
13
|
+
# - business_domain
|
14
|
+
# - platform
|
15
|
+
# - utility
|
16
|
+
# - specification
|
17
|
+
#
|
18
|
+
# Then a package can configure:
|
19
|
+
# enforce_architecture: true | false | strict
|
20
|
+
# layer: utility
|
21
|
+
#
|
22
|
+
# This is intended to provide:
|
23
|
+
# A) Direction for which dependency violations to tackle
|
24
|
+
# B) What dependencies should or should not exist
|
25
|
+
# C) A potential sequencing for modularizing a system (starting with lower layers first).
|
26
|
+
#
|
27
|
+
class Checker
|
28
|
+
extend T::Sig
|
29
|
+
include Packwerk::Checker
|
30
|
+
|
31
|
+
VIOLATION_TYPE = T.let('architecture', String)
|
32
|
+
|
33
|
+
sig { override.returns(String) }
|
34
|
+
def violation_type
|
35
|
+
VIOLATION_TYPE
|
36
|
+
end
|
37
|
+
|
38
|
+
sig do
|
39
|
+
override
|
40
|
+
.params(reference: Packwerk::Reference)
|
41
|
+
.returns(T::Boolean)
|
42
|
+
end
|
43
|
+
def invalid_reference?(reference)
|
44
|
+
constant_package = Package.from(reference.constant.package, layers)
|
45
|
+
referencing_package = Package.from(reference.package, layers)
|
46
|
+
!referencing_package.can_depend_on?(constant_package, layers: layers)
|
47
|
+
end
|
48
|
+
|
49
|
+
sig do
|
50
|
+
override
|
51
|
+
.params(listed_offense: Packwerk::ReferenceOffense)
|
52
|
+
.returns(T::Boolean)
|
53
|
+
end
|
54
|
+
def strict_mode_violation?(listed_offense)
|
55
|
+
constant_package = listed_offense.reference.package
|
56
|
+
constant_package.config['enforce_architecture'] == 'strict'
|
57
|
+
end
|
58
|
+
|
59
|
+
sig do
|
60
|
+
override
|
61
|
+
.params(reference: Packwerk::Reference)
|
62
|
+
.returns(String)
|
63
|
+
end
|
64
|
+
def message(reference)
|
65
|
+
constant_package = Package.from(reference.constant.package, layers)
|
66
|
+
referencing_package = Package.from(reference.package, layers)
|
67
|
+
|
68
|
+
message = <<~MESSAGE
|
69
|
+
Architecture layer violation: '#{reference.constant.name}' belongs to '#{reference.constant.package}', whose architecture layer type is "#{constant_package.layer}."
|
70
|
+
This constant cannot be referenced by '#{reference.package}', whose architecture layer type is "#{referencing_package.layer}."
|
71
|
+
Can we organize our code logic to respect the layers of these packs? See all layers in packwerk.yml.
|
72
|
+
|
73
|
+
#{standard_help_message(reference)}
|
74
|
+
MESSAGE
|
75
|
+
|
76
|
+
message.chomp
|
77
|
+
end
|
78
|
+
|
79
|
+
# TODO: Extract this out into a common helper, can call it StandardViolationHelpMessage.new(...) and implements .to_s
|
80
|
+
sig { params(reference: Reference).returns(String) }
|
81
|
+
def standard_help_message(reference)
|
82
|
+
standard_message = <<~MESSAGE.chomp
|
83
|
+
Inference details: this is a reference to #{reference.constant.name} which seems to be defined in #{reference.constant.location}.
|
84
|
+
To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations
|
85
|
+
MESSAGE
|
86
|
+
|
87
|
+
standard_message.chomp
|
88
|
+
end
|
89
|
+
|
90
|
+
sig { returns(Layers) }
|
91
|
+
def layers
|
92
|
+
@layers ||= T.let(Layers.new, T.nilable(Packwerk::Architecture::Layers))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Packwerk
|
5
|
+
module Architecture
|
6
|
+
class Layers
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { void }
|
10
|
+
def initialize
|
11
|
+
@names = T.let(@names, T.nilable(T::Set[String]))
|
12
|
+
@names_list = T.let(@names_list, T.nilable(T::Array[String]))
|
13
|
+
end
|
14
|
+
|
15
|
+
sig { params(layer: String).returns(Integer) }
|
16
|
+
def index_of(layer)
|
17
|
+
index = names_list.reverse.find_index(layer)
|
18
|
+
if index.nil?
|
19
|
+
raise "Layer #{layer} not find, please run `bin/packwerk validate`"
|
20
|
+
end
|
21
|
+
|
22
|
+
index
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { returns(T::Set[String]) }
|
26
|
+
def names
|
27
|
+
@names ||= Set.new(names_list)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
sig { returns(T::Array[String]) }
|
33
|
+
def names_list
|
34
|
+
@names_list ||= YAML.load_file('packwerk.yml')['architecture_layers'] || []
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Packwerk
|
5
|
+
module Architecture
|
6
|
+
class Package < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
const :layer, T.nilable(String)
|
10
|
+
const :enforcement_setting, T.nilable(T.any(T::Boolean, String, T::Array[String]))
|
11
|
+
const :config, T::Hash[T.untyped, T.untyped]
|
12
|
+
|
13
|
+
sig { returns(T::Boolean) }
|
14
|
+
def enforces?
|
15
|
+
enforcement_setting == true || enforcement_setting == 'strict'
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { params(other_package: Package, layers: Layers).returns(T::Boolean) }
|
19
|
+
def can_depend_on?(other_package, layers:)
|
20
|
+
return true if !enforces?
|
21
|
+
|
22
|
+
flow_sensitive_layer = layer
|
23
|
+
flow_sensitive_other_layer = other_package.layer
|
24
|
+
return true if flow_sensitive_layer.nil?
|
25
|
+
return true if flow_sensitive_other_layer.nil?
|
26
|
+
|
27
|
+
layers.index_of(flow_sensitive_layer) >= layers.index_of(flow_sensitive_other_layer)
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
extend T::Sig
|
32
|
+
|
33
|
+
sig { params(package: ::Packwerk::Package, layers: Layers).returns(Package) }
|
34
|
+
def from(package, layers)
|
35
|
+
config = package.config
|
36
|
+
|
37
|
+
# This allows the layer to be inferred based on the package root
|
38
|
+
package_root = package.name.split('/').first
|
39
|
+
if package_root && layers.names.include?(package_root)
|
40
|
+
layer = package_root
|
41
|
+
else
|
42
|
+
layer = config['layer']
|
43
|
+
end
|
44
|
+
|
45
|
+
Package.new(
|
46
|
+
layer: layer,
|
47
|
+
enforcement_setting: config['enforce_architecture'],
|
48
|
+
config: config
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Packwerk
|
5
|
+
module Architecture
|
6
|
+
class Validator
|
7
|
+
extend T::Sig
|
8
|
+
include Packwerk::Validator
|
9
|
+
|
10
|
+
Result = Packwerk::Validator::Result
|
11
|
+
|
12
|
+
sig { override.params(package_set: PackageSet, configuration: Configuration).returns(Result) }
|
13
|
+
def call(package_set, configuration)
|
14
|
+
results = T.let([], T::Array[Result])
|
15
|
+
|
16
|
+
package_set.each do |package|
|
17
|
+
config = package.config
|
18
|
+
f = Pathname.new(package.name).join('package.yml').to_s
|
19
|
+
next if !config
|
20
|
+
|
21
|
+
result = check_enforce_architecture_setting(f, config['enforce_architecture'])
|
22
|
+
results << result
|
23
|
+
next if !result.ok?
|
24
|
+
|
25
|
+
result = check_layer_setting(config, f, config['layer'])
|
26
|
+
results << result
|
27
|
+
next if !result.ok?
|
28
|
+
|
29
|
+
package = Package.from(package, layers)
|
30
|
+
results += check_dependencies_setting(package_set, package, f)
|
31
|
+
end
|
32
|
+
|
33
|
+
merge_results(results, separator: "\n---\n")
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { returns(Layers) }
|
37
|
+
def layers
|
38
|
+
@layers ||= T.let(Layers.new, T.nilable(Packwerk::Architecture::Layers))
|
39
|
+
end
|
40
|
+
|
41
|
+
sig { override.returns(T::Array[String]) }
|
42
|
+
def permitted_keys
|
43
|
+
%w[enforce_architecture layer]
|
44
|
+
end
|
45
|
+
|
46
|
+
sig do
|
47
|
+
params(package_set: PackageSet, package: Package, config_file_path: String).returns(T::Array[Result])
|
48
|
+
end
|
49
|
+
def check_dependencies_setting(package_set, package, config_file_path)
|
50
|
+
results = T.let([], T::Array[Result])
|
51
|
+
package.config.fetch('dependencies', []).each do |dependency|
|
52
|
+
other_packwerk_package = package_set.fetch(dependency)
|
53
|
+
next if other_packwerk_package.nil?
|
54
|
+
|
55
|
+
other_package = Package.from(other_packwerk_package, layers)
|
56
|
+
next if package.can_depend_on?(other_package, layers: layers)
|
57
|
+
|
58
|
+
results << Result.new(
|
59
|
+
ok: false,
|
60
|
+
error_value: "Invalid 'dependencies' in #{config_file_path.inspect}. '#{config_file_path}' has a layer type of '#{package.layer},' which cannot rely on '#{other_packwerk_package.name},' which has a layer type of '#{other_package.layer}.' `architecture_layers` can be found in packwerk.yml."
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
results
|
65
|
+
end
|
66
|
+
|
67
|
+
sig do
|
68
|
+
params(config: T::Hash[T.untyped, T.untyped], config_file_path: String, layer: T.untyped).returns(Result)
|
69
|
+
end
|
70
|
+
def check_layer_setting(config, config_file_path, layer)
|
71
|
+
enforce_architecture = config['enforce_architecture']
|
72
|
+
enforce_architecture_enabled = !(enforce_architecture.nil? || enforce_architecture == false)
|
73
|
+
valid_layer = layer.nil? || layers.names.include?(layer)
|
74
|
+
|
75
|
+
if layer.nil? && enforce_architecture_enabled
|
76
|
+
Result.new(
|
77
|
+
ok: false,
|
78
|
+
error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{layer.inspect}. `layer` must be set if `enforce_architecture` is on."
|
79
|
+
)
|
80
|
+
elsif valid_layer
|
81
|
+
Result.new(ok: true)
|
82
|
+
else
|
83
|
+
Result.new(
|
84
|
+
ok: false,
|
85
|
+
error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{layer.inspect}. Must be one of #{layers.names.to_a.inspect}"
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
sig do
|
91
|
+
params(config_file_path: String, setting: T.untyped).returns(Result)
|
92
|
+
end
|
93
|
+
def check_enforce_architecture_setting(config_file_path, setting)
|
94
|
+
valid_value = [true, nil, false, 'strict'].include?(setting)
|
95
|
+
layers_set = layers.names.any?
|
96
|
+
if valid_value && layers_set
|
97
|
+
Result.new(ok: true)
|
98
|
+
elsif valid_value
|
99
|
+
Result.new(
|
100
|
+
ok: false,
|
101
|
+
error_value: "Cannot set 'enforce_architecture' option in #{config_file_path.inspect} until `architectural_layers` have been specified in `packwerk.yml`"
|
102
|
+
)
|
103
|
+
else
|
104
|
+
Result.new(
|
105
|
+
ok: false,
|
106
|
+
error_value: "Invalid 'enforce_architecture' option in #{config_file_path.inspect}: #{setting.inspect}"
|
107
|
+
)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/packwerk-extensions.rb
CHANGED
@@ -12,6 +12,10 @@ require 'packwerk/visibility/checker'
|
|
12
12
|
require 'packwerk/visibility/package'
|
13
13
|
require 'packwerk/visibility/validator'
|
14
14
|
|
15
|
+
require 'packwerk/architecture/checker'
|
16
|
+
require 'packwerk/architecture/package'
|
17
|
+
require 'packwerk/architecture/validator'
|
18
|
+
|
15
19
|
module Packwerk
|
16
20
|
module Extensions
|
17
21
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: packwerk-extensions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gusto Engineers
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-12-
|
11
|
+
date: 2022-12-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: packwerk
|
@@ -173,6 +173,10 @@ extra_rdoc_files: []
|
|
173
173
|
files:
|
174
174
|
- README.md
|
175
175
|
- lib/packwerk-extensions.rb
|
176
|
+
- lib/packwerk/architecture/checker.rb
|
177
|
+
- lib/packwerk/architecture/layers.rb
|
178
|
+
- lib/packwerk/architecture/package.rb
|
179
|
+
- lib/packwerk/architecture/validator.rb
|
176
180
|
- lib/packwerk/privacy/checker.rb
|
177
181
|
- lib/packwerk/privacy/package.rb
|
178
182
|
- lib/packwerk/privacy/validator.rb
|