packwerk-extensions 0.0.4 → 0.0.6

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: 03a304b3c407316bea35a2939967a2cdcbdd30a2459652b96992acd40e49be5a
4
- data.tar.gz: 5703efe5318e73495845b5d6f5d7f8bf042e0066cf9b2122697fff6dd9cbfc34
3
+ metadata.gz: 3f20892b9d7d67a2164c7c632ac760c66859cacd2fa7af97c71a220a1d317893
4
+ data.tar.gz: a9a7f631ed8e6398a48e91fddc83fcff0e1d923aa16b75231e0a3296c5c4aa4a
5
5
  SHA512:
6
- metadata.gz: 5a680df43f37a36afc61a43ef2bc4e91fd5f8d3763aded52dc51cd3acb712d799079447ed5d0d6a9ac366b4d71edd51c13cc284962e320e1c69206533c7f56ed
7
- data.tar.gz: 6bcc586c29c90097340cd1bc3322a7e16f9b2b6b2ba8176284da216175333a97b98d80f6572ace6209cb3a333974dd81bd6bbed7928162a839b5f2c9a4a44220
6
+ metadata.gz: f66866b77e2727898fa951a05f716f8b959444553082f9c8999144013ee74d90820ee802a994b11887c3132799b47cbcc74a60cc6434a6172617ebb916748216
7
+ data.tar.gz: b6deec3d175813666076ce5797d29cc62df65ec233c552114cdf3492c92dd74791b53412201a8b3e56758fdb4bb9a6c7bed804225c99c192d92f9566a60a4a85
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)
45
+ referencing_package = Package.from(reference.package)
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)
66
+ referencing_package = Package.from(reference.package)
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,30 @@
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::Array[String]))
12
+ end
13
+
14
+ sig { params(layer: String).returns(Integer) }
15
+ def index_of(layer)
16
+ index = names.reverse.find_index(layer)
17
+ if index.nil?
18
+ raise "Layer #{layer} not find, please run `bin/packwerk validate`"
19
+ end
20
+
21
+ index
22
+ end
23
+
24
+ sig { returns(T::Array[String]) }
25
+ def names
26
+ @names ||= YAML.load_file('packwerk.yml')['architecture_layers'] || []
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,49 @@
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).returns(Package) }
34
+ def from(package)
35
+ from_config(package.config)
36
+ end
37
+
38
+ sig { params(config: T::Hash[T.untyped, T.untyped]).returns(Package) }
39
+ def from_config(config)
40
+ Package.new(
41
+ layer: config['layer'],
42
+ enforcement_setting: config['enforce_architecture'],
43
+ config: config
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,111 @@
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_manifests(configuration).each do |f|
17
+ config = YAML.load_file(File.join(f))
18
+ next if !config
19
+
20
+ result = check_enforce_architecture_setting(f, config['enforce_architecture'])
21
+ results << result
22
+ next if !result.ok?
23
+
24
+ result = check_layer_setting(config, f, config['layer'])
25
+ results << result
26
+ next if !result.ok?
27
+
28
+ package = Package.from_config(config)
29
+ results += check_dependencies_setting(package_set, package, f)
30
+ end
31
+
32
+ merge_results(results, separator: "\n---\n")
33
+ end
34
+
35
+ sig { returns(Layers) }
36
+ def layers
37
+ @layers ||= T.let(Layers.new, T.nilable(Packwerk::Architecture::Layers))
38
+ end
39
+
40
+ sig { override.returns(T::Array[String]) }
41
+ def permitted_keys
42
+ %w[enforce_architecture layer]
43
+ end
44
+
45
+ sig do
46
+ params(package_set: PackageSet, package: Package, config_file_path: String).returns(T::Array[Result])
47
+ end
48
+ def check_dependencies_setting(package_set, package, config_file_path)
49
+ results = T.let([], T::Array[Result])
50
+ package.config.fetch('dependencies', []).each do |dependency|
51
+ other_packwerk_package = package_set.fetch(dependency)
52
+ next if other_packwerk_package.nil?
53
+
54
+ other_package = Package.from(other_packwerk_package)
55
+ next if package.can_depend_on?(other_package, layers: layers)
56
+
57
+ results << Result.new(
58
+ ok: false,
59
+ 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."
60
+ )
61
+ end
62
+
63
+ results
64
+ end
65
+
66
+ sig do
67
+ params(config: T::Hash[T.untyped, T.untyped], config_file_path: String, layer: T.untyped).returns(Result)
68
+ end
69
+ def check_layer_setting(config, config_file_path, layer)
70
+ enforce_architecture = config['enforce_architecture']
71
+ enforce_architecture_enabled = !(enforce_architecture.nil? || enforce_architecture == false)
72
+ valid_layer = layer.nil? || layers.names.include?(layer)
73
+
74
+ if layer.nil? && enforce_architecture_enabled
75
+ Result.new(
76
+ ok: false,
77
+ error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{layer.inspect}. `layer` must be set if `enforce_architecture` is on."
78
+ )
79
+ elsif valid_layer
80
+ Result.new(ok: true)
81
+ else
82
+ Result.new(
83
+ ok: false,
84
+ error_value: "Invalid 'layer' option in #{config_file_path.inspect}: #{layer.inspect}. Must be one of #{layers.names.inspect}"
85
+ )
86
+ end
87
+ end
88
+
89
+ sig do
90
+ params(config_file_path: String, setting: T.untyped).returns(Result)
91
+ end
92
+ def check_enforce_architecture_setting(config_file_path, setting)
93
+ valid_value = [true, nil, false, 'strict'].include?(setting)
94
+ layers_set = layers.names.any?
95
+ if valid_value && layers_set
96
+ Result.new(ok: true)
97
+ elsif valid_value
98
+ Result.new(
99
+ ok: false,
100
+ error_value: "Cannot set 'enforce_architecture' option in #{config_file_path.inspect} until `architectural_layers` have been specified in `packwerk.yml`"
101
+ )
102
+ else
103
+ Result.new(
104
+ ok: false,
105
+ error_value: "Invalid 'enforce_architecture' option in #{config_file_path.inspect}: #{setting.inspect}"
106
+ )
107
+ end
108
+ end
109
+ end
110
+ end
111
+ 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.4
4
+ version: 0.0.6
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-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: packwerk
@@ -42,16 +42,16 @@ dependencies:
42
42
  name: sorbet-runtime
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 0.5.10520
47
+ version: '0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 0.5.10520
54
+ version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: zeitwerk
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -126,30 +126,30 @@ dependencies:
126
126
  name: sorbet
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - '='
129
+ - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: 0.5.10520
131
+ version: '0'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - '='
136
+ - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: 0.5.10520
138
+ version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: sorbet-static
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - '='
143
+ - - ">="
144
144
  - !ruby/object:Gem::Version
145
- version: 0.5.10520
145
+ version: '0'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - '='
150
+ - - ">="
151
151
  - !ruby/object:Gem::Version
152
- version: 0.5.10520
152
+ version: '0'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: tapioca
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -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