hub_kernel 0.8.0 → 0.10.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a20cf973f68397694b8434e58918a40c31619eb8936aad87f8a183122aad47f4
4
- data.tar.gz: 38a563f62a0c9bf630e72823b94c5cdba0cfe71bfe8c54d45d00a7129e8b5f32
3
+ metadata.gz: 269ad669e7c014af9175190d416dbf7be82bfb8d797eb3a25107bf87b99b1344
4
+ data.tar.gz: 808639ddaca46f8d8cbe9f187a7d59ae9d50b704f773bf4f255bae731d4339a7
5
5
  SHA512:
6
- metadata.gz: 13453ee9d42aa2ac1fe4d3e95ad216867f27d9d2b13f9e71cfa52fe244add5f32e7dbb1a43912af43aba99fa060f3d564746265bb5e973ab4064399beb01e7bd
7
- data.tar.gz: c55bce27f9ef2d71959cf39102a38a0c8b471f1d35665fedfa16c3325e96d2676cef36dc57f90c8da167cdfdd64d9bd248b029edf496f60f4e471b1d11e798f1
6
+ metadata.gz: 3c94024970cd0ae83bf695824439a959b6b03061eb39f92268f483ca769fbf323d4210710f805f052028590e65b87a84b43b8801cac1a9b927a9402205f1fd55
7
+ data.tar.gz: 307661997442f598ee6d2e931540336986a567794113298d496864f86d3ac19e2788ee091e75b8bc9434bd1c61bd3aa1df095b71af29d2479b7898869dfd416b
data/README.md CHANGED
@@ -116,6 +116,54 @@ Supplies.exposed("record_purchase").takes # => [:supplier_id, :bought_on, :lines
116
116
  Supplies.exposed("delete_everything") # => nil
117
117
  ```
118
118
 
119
+ ### The exposed-list check
120
+ A hub's own test can check its exposed list, so a listed method the hub lacks, or one
121
+ listed with values it does not take, shows in the suite rather than on a caller's
122
+ request:
123
+
124
+ ```ruby
125
+ require "hub_kernel/conformance/exposed"
126
+
127
+ class SuppliesHubTest < ActiveSupport::TestCase
128
+ include HubKernel::Conformance::Exposed
129
+
130
+ hub { Supplies }
131
+ end
132
+ ```
133
+
134
+ The test fails and names each problem. A method that takes `**` values accepts any
135
+ values it is listed with, as long as it is listed with each value it requires.
136
+
137
+ ### The crossing check
138
+ A host's test can check that no hub's code names a class another hub owns. The host
139
+ passes a map of which classes each hub owns, and the test fails listing each file and
140
+ the class it names:
141
+
142
+ ```ruby
143
+ require "hub_kernel/conformance/crossings"
144
+
145
+ class HubCrossingsTest < ActiveSupport::TestCase
146
+ include HubKernel::Conformance::Crossings
147
+
148
+ crossings files: "app/{models,controllers,jobs,views}/shop/**/*.{rb,erb}", except: [ "app/models/shop/hubs.rb" ] do
149
+ HubKernel::Crossings.new(
150
+ owners: { supplies: %w[Shop::Purchase Shop::PurchasesController], finance: %w[Shop::Expense], shop: %w[Shop::Home] },
151
+ shared: %w[Shop::BaseController],
152
+ host_layer: :shop,
153
+ interfaces: { supplies: "Shop::Hubs::Supplies", finance: "Shop::Hubs::Finance" }
154
+ )
155
+ end
156
+ end
157
+ ```
158
+
159
+ - A constant inside another hub's class counts as naming that class.
160
+ - A name no hub owns, such as a namespace, is not a crossing, and neither is a class on
161
+ the `shared` list.
162
+ - A view belongs to the hub that owns its controller, then to the hub that owns the
163
+ record its folder is named after, and otherwise to the `host_layer`.
164
+ - The host's layer may name any hub's interface module, and a hub naming another hub's
165
+ interface module is a crossing.
166
+
119
167
  ## Installation
120
168
  Add this line to your application's Gemfile:
121
169
 
@@ -0,0 +1,28 @@
1
+ require "active_support/concern"
2
+ require "hub_kernel/crossings"
3
+
4
+ module HubKernel
5
+ module Conformance
6
+ module Crossings
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def crossings(root: nil, files: "app/**/*.{rb,erb}", except: [], &block)
11
+ define_method(:the_crossings, &block)
12
+ define_method(:crossing_root) { root || Rails.root }
13
+ define_method(:crossing_files) { files }
14
+ define_method(:crossing_exceptions) { except }
15
+ end
16
+ end
17
+
18
+ included do
19
+ test "no file names a class another hub owns" do
20
+ found = (Dir.glob(crossing_files, base: crossing_root) - crossing_exceptions).sort.flat_map do |path|
21
+ the_crossings.in(path, File.read(File.join(crossing_root, path))).map { |name| "#{path} names #{name}" }
22
+ end
23
+ flunk found.join("\n") if found.any?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ require "active_support/concern"
2
+
3
+ module HubKernel
4
+ module Conformance
5
+ module Exposed
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def hub(&block)
10
+ define_method(:the_hub, &block)
11
+ end
12
+ end
13
+
14
+ included do
15
+ test "every method the hub exposes exists and takes the values it is listed with" do
16
+ problems = the_hub.exposure_problems
17
+ flunk problems.join("\n") if problems.any?
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,40 @@
1
+ require "active_support/core_ext/string/inflections"
2
+
3
+ module HubKernel
4
+ class Crossings
5
+ def initialize(owners:, shared: [], host_layer: nil, interfaces: {})
6
+ @owners = owners
7
+ @shared = shared
8
+ @host_layer = host_layer
9
+ @interfaces = interfaces
10
+ end
11
+
12
+ def in(path, source)
13
+ hub = owner_of_file(path)
14
+ source.scan(/[A-Z]\w*(?:::[A-Z]\w*)*/).filter_map { |name| owned_class_in(name) }.uniq.select { |name| crossing?(name, hub) }
15
+ end
16
+
17
+ private
18
+
19
+ def crossing?(name, hub)
20
+ return false if @shared.include?(name) || owner_of(name) == hub
21
+
22
+ !(hub == @host_layer && @interfaces.value?(name))
23
+ end
24
+
25
+ def owned_class_in(name)
26
+ parts = name.split("::")
27
+ parts.size.downto(1).map { |size| parts.first(size).join("::") }.find { |candidate| owner_of(candidate) }
28
+ end
29
+
30
+ def owner_of(class_name) = @interfaces.key(class_name) || @owners.find { |_hub, classes| classes.include?(class_name) }&.first
31
+
32
+ def owner_of_file(path)
33
+ return owner_of_view_folder(File.dirname(path.delete_prefix("app/views/")).camelize) if path.start_with?("app/views/")
34
+
35
+ owner_of(path.sub(%r{\Aapp/[^/]+/}, "").delete_suffix(".rb").camelize)
36
+ end
37
+
38
+ def owner_of_view_folder(folder) = owner_of("#{folder}Controller") || owner_of(folder) || @host_layer
39
+ end
40
+ end
@@ -8,8 +8,29 @@ module HubKernel
8
8
 
9
9
  def exposed(name) = exposed_methods[name.to_s]
10
10
 
11
+ def exposure_problems
12
+ exposed_methods.values.filter_map { |exposure| exposure_problem(exposure) }
13
+ end
14
+
11
15
  private
12
16
 
13
17
  def exposed_methods = @exposed_methods ||= {}
18
+
19
+ def exposing_hub = name.demodulize
20
+
21
+ def exposure_problem(exposure)
22
+ return "#{exposing_hub} exposes #{exposure.name}, which it has no method for" unless respond_to?(exposure.name)
23
+ return if takes_listed_values?(exposure)
24
+
25
+ "#{exposing_hub} exposes #{exposure.name} with #{exposure.takes.join(", ")}, but it takes #{keywords(exposure, :keyreq, :key).join(", ")}"
26
+ end
27
+
28
+ def takes_listed_values?(exposure)
29
+ (keywords(exposure, :keyreq) - exposure.takes).empty? && (takes_any_values?(exposure) || (exposure.takes - keywords(exposure, :keyreq, :key)).empty?)
30
+ end
31
+
32
+ def takes_any_values?(exposure) = method(exposure.name).parameters.any? { |kind, _| kind == :keyrest }
33
+
34
+ def keywords(exposure, *kinds) = method(exposure.name).parameters.filter_map { |kind, value| value if kinds.include?(kind) }
14
35
  end
15
36
  end
@@ -1,3 +1,3 @@
1
1
  module HubKernel
2
- VERSION = "0.8.0"
2
+ VERSION = "0.10.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hub_kernel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider
@@ -42,9 +42,12 @@ files:
42
42
  - lib/hub_kernel/answer.rb
43
43
  - lib/hub_kernel/authz.rb
44
44
  - lib/hub_kernel/conformance/action.rb
45
+ - lib/hub_kernel/conformance/crossings.rb
46
+ - lib/hub_kernel/conformance/exposed.rb
45
47
  - lib/hub_kernel/conformance/hub.rb
46
48
  - lib/hub_kernel/conformance/partial.rb
47
49
  - lib/hub_kernel/context.rb
50
+ - lib/hub_kernel/crossings.rb
48
51
  - lib/hub_kernel/engine.rb
49
52
  - lib/hub_kernel/exposes.rb
50
53
  - lib/hub_kernel/follow_up.rb