packwerk-extensions 0.0.5 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4dbf14484c64b3fe953df421fad052d504988b6ea4f7b325baf53baa4e869b4b
4
- data.tar.gz: b34ab360d4b87eb2a30f5f32e6057fc3088674768cdefb3d787b80dfa52931e1
3
+ metadata.gz: 558107be67a748dfb9a175b9d90be2acc373913f36a66c5bccbb3401e80cd85f
4
+ data.tar.gz: fb98b8c64bb3043d267aab3d966d404934a5a530020dc18e3e599a13153fe121
5
5
  SHA512:
6
- metadata.gz: 2b1b7b4f9665f2bd39cec93f5ccd4a650f8b2f89b217cc2141a7e66cd1b43591f1b948c7bc9ebaf5f7297df5f058a8a197cec32217e2d751adfe73f5197700e7
7
- data.tar.gz: fb813bb156391ed80b5fb7a6bb3886b48e1ba5204db4f64c1b88d82cf4e5a6a654384cf3de632db6839d76bca730c8a04de34535673b81cb8702ff321756f2e0
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
- #### Enforcing privacy boundary
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
- There are two ways you can enforce privacy for your package:
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 # will make everything private that is not in
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
- 2. Enforce privacy for specific constants
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
- ##### Defining your own public folder
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
- ##### Suggestions
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
@@ -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.5
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-19 00:00:00.000000000 Z
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