hub_kernel 0.19.0 → 0.21.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: 3efd66220ac58f67d7c6018b2e552f70641539e11da6ef13810d274cb1daea52
4
- data.tar.gz: 803ca1aa3d1258ae639ae2a6beb842b8e969f213b3d604e07d7acff7b9dc6523
3
+ metadata.gz: 5abfdc116c5675e15edad5aa0d08ee0ebf20d123a1cdc0f28f8c5776e5253cb7
4
+ data.tar.gz: 6a2917d8cdfbf55633baa557d51ecb7a77c0b3b84b3fe3db3db42ce2d41f6105
5
5
  SHA512:
6
- metadata.gz: 111dfbefdd5d3fbb62241dc8142a5b16b8626f9af8d227df56033c60205a2d2bbd9db4a27fa39e487f31ea891dd1041a18c76519e5ba88e2a7a7279659e667cc
7
- data.tar.gz: bac66dfb083524ae2f30f2db97c637df7d88a70f19ce59f264acdbc40baa24e39fa3218ef19d472713e3c3c6ba7df8cf959078f2993ceddbc0b9790b44badff1
6
+ metadata.gz: def104f8e708974248c0b199bfee92b80ba04a571f3db6de7080361ca26be1a9a4d98fabf91cd4eb99adbd2d0291915a9329c79199373f4f9ec81cf90bf91750
7
+ data.tar.gz: 7c47c64d8f1f4a5e73667b5c5f00f779c33d8da2438f6035bed67cbee572a4ae5068c0cad23c2063249862f30a64fc73bacbb2b8659db5e750dee3fc9bf8c34d
data/README.md CHANGED
@@ -18,6 +18,36 @@ Inside a Rails engine gem it writes the hub under the gem's namespace, such as
18
18
  gemspec. It writes no hub check into the gem's tests, since the gem never fills its own
19
19
  ports.
20
20
 
21
+ Run in an app, it writes the hub into its own folder, named after the last part of the
22
+ hub's name: `Console::Hubs::Supplies` goes to
23
+ `hubs/supplies/app/models/console/hubs/supplies.rb`, and its hub check to
24
+ `test/hubs/supplies/console/hubs/supplies_test.rb`, which the app's normal test run
25
+ reads.
26
+
27
+ ### A hub folder in the host app
28
+ A hub that lives in the host app can keep its code in its own folder,
29
+ `hubs/<hub>/app/<kind>/`, laid out like the app's own `app/` folder:
30
+
31
+ ```
32
+ hubs/billing/app/models/billing/invoice.rb
33
+ hubs/billing/app/jobs/billing/reminder_job.rb
34
+ hubs/billing/app/event_definitions/billing/invoice_sent.rb
35
+ ```
36
+
37
+ hub_kernel adds each of those folders to the host's load paths, so a class there loads
38
+ by its full name, `Billing::Invoice`, just as it would from `app/models/`. Moving a
39
+ class into its hub's folder changes nothing for the code that calls it. A namespace
40
+ module may be defined in `app/models/` or in another hub's folder. A host with no
41
+ `hubs/` folder loads exactly what it loads today.
42
+
43
+ Files under a hub's `lib/` folder, such as its generated event pack, are not loaded.
44
+
45
+ A hub folder's `views` are searched after the host's `app/views`, so a controller in the
46
+ hub renders its templates from there, any view can render a hub partial by its usual
47
+ path, and a template in `app/views` wins over one at the same path in a hub folder. A
48
+ hub folder's `helpers` are added to the host's helper paths, so they are available
49
+ wherever every helper is.
50
+
21
51
  ### Using a hub from a gem
22
52
  The app that installs the gem fills each of the hub's ports in its own setup, then
23
53
  runs the start check:
@@ -209,6 +239,11 @@ class HubCrossingsTest < ActiveSupport::TestCase
209
239
  end
210
240
  ```
211
241
 
242
+ - A file under a hub's folder, `hubs/<hub>/`, belongs to that hub, and so does each class
243
+ defined there, without any entry in `owners`. `owners` is only asked about files and
244
+ classes outside every hub folder, and may be left out once every hub has its folder.
245
+ - By default the test reads every `.rb` and `.erb` file under `app/` and under each hub
246
+ folder's `app/`, helpers and services included.
212
247
  - A constant inside another hub's class counts as naming that class.
213
248
  - A name no hub owns, such as a namespace, is not a crossing, and neither is a class on
214
249
  the `shared` list.
@@ -218,7 +253,10 @@ end
218
253
  - A view belongs to the hub that owns its controller, then to the hub that owns the
219
254
  record its folder is named after, and otherwise to the `host_layer`.
220
255
  - The host's layer may name any hub's interface module, and a hub naming another hub's
221
- interface module is a crossing.
256
+ interface module is a crossing. A hub's interface module is found in its folder, as the
257
+ module there that extends `HubKernel::Ports` or `HubKernel::Exposes`, so a hub in a
258
+ folder needs no entry in `interfaces`. A folder may hold several such modules, such as
259
+ the modules of hubs merged into one area, and every one of them counts.
222
260
  - The test fails with "The crossing check found no files to read" when none of the files
223
261
  it read belongs to a hub or the host's layer, so a wrong `files:` pattern or an empty
224
262
  map does not pass unnoticed.
@@ -227,7 +265,14 @@ end
227
265
  own Shop::Receipt".
228
266
  - It fails naming each class the map names that no longer exists in the app, such as "The
229
267
  map names Shop::Ghost, which does not exist", so a renamed or removed class does not
230
- stay in the map.
268
+ stay in the map. A class whose file sits in a hub folder is never reported this way.
269
+ - While hubs move into folders, a class the map gives one hub whose file sits in another
270
+ hub's folder is owned by two hubs. A class the map still lists whose file already sits
271
+ in that hub's folder fails with "Shop::Purchase sits in its hub's folder, so take it off
272
+ the list".
273
+ - It fails naming each folder under `hubs/` that holds no module extending
274
+ `HubKernel::Ports` or `HubKernel::Exposes`, such as "hubs/suplies holds no hub module",
275
+ so a misspelled or half-made hub folder does not pass unnoticed.
231
276
 
232
277
  ## Installation
233
278
  Add this line to your application's Gemfile:
@@ -1,4 +1,5 @@
1
1
  require "rails/generators/named_base"
2
+ require "hub_kernel/hub_folders"
2
3
 
3
4
  module HubKernel
4
5
  module Generators
@@ -6,7 +7,7 @@ module HubKernel
6
7
  argument :ports, type: :array, default: [], banner: "port:method port:method"
7
8
 
8
9
  def create_hub
9
- create_file "app/models/#{file_path}.rb", <<~RUBY
10
+ create_file "#{code_root}/models/#{file_path}.rb", <<~RUBY
10
11
  module #{class_name}
11
12
  extend HubKernel::Ports
12
13
  #{port_lines}end
@@ -16,7 +17,7 @@ module HubKernel
16
17
  def create_hub_test
17
18
  return if gemspec
18
19
 
19
- create_file "test/models/#{file_path}_test.rb", <<~RUBY
20
+ create_file "#{HubKernel::HubFolders.test_folder(hub_folder)}/#{file_path}_test.rb", <<~RUBY
20
21
  require "test_helper"
21
22
  require "hub_kernel/conformance/hub"
22
23
 
@@ -38,6 +39,10 @@ module HubKernel
38
39
 
39
40
  def gemspec = Dir.glob("*.gemspec", base: destination_root).first
40
41
 
42
+ def code_root = gemspec ? "app" : "#{HubKernel::HubFolders.folder(hub_folder)}/#{HubKernel::HubFolders::CODE}"
43
+
44
+ def hub_folder = file_path.split("/").last
45
+
41
46
  def port_lines
42
47
  return "" if ports.empty?
43
48
 
@@ -1,5 +1,6 @@
1
1
  require "active_support/concern"
2
2
  require "hub_kernel/crossings"
3
+ require "hub_kernel/hub_folders"
3
4
 
4
5
  module HubKernel
5
6
  module Conformance
@@ -7,8 +8,8 @@ module HubKernel
7
8
  extend ActiveSupport::Concern
8
9
 
9
10
  class_methods do
10
- def crossings(root: nil, files: "app/**/*.{rb,erb}", except: [], &block)
11
- define_method(:the_crossings, &block)
11
+ def crossings(root: nil, files: "{app,#{HubKernel::HubFolders.code_glob}}/**/*.{rb,erb}", except: [], &block)
12
+ define_method(:built_crossings, &block)
12
13
  define_method(:crossing_root) { root || Rails.root }
13
14
  define_method(:crossing_files) { files }
14
15
  define_method(:crossing_exceptions) { except }
@@ -35,6 +36,16 @@ module HubKernel
35
36
  assert twice.empty?, twice.map { |name| "Two hubs own #{name}" }.join("\n")
36
37
  end
37
38
 
39
+ test "no class is both listed and in its hub's folder" do
40
+ listed = the_crossings.listed_in_their_folders
41
+ assert listed.empty?, listed.map { |name| "#{name} sits in its hub's folder, so take it off the list" }.join("\n")
42
+ end
43
+
44
+ test "every hub folder holds a hub module" do
45
+ bare = the_crossings.folders_without_hub_module
46
+ assert bare.empty?, bare.map { |folder| "#{folder} holds no hub module" }.join("\n")
47
+ end
48
+
38
49
  test "every class the map names exists" do
39
50
  missing = the_crossings.missing_classes
40
51
  assert missing.empty?, missing.map { |name| "The map names #{name}, which does not exist" }.join("\n")
@@ -42,6 +53,10 @@ module HubKernel
42
53
  end
43
54
 
44
55
  def crossing_paths = (Dir.glob(crossing_files, base: crossing_root) - crossing_exceptions).sort
56
+
57
+ def the_crossings = @the_crossings ||= built_crossings.read_folders(hub_files).read_hub_modules(hub_files.to_h { |path| [ path, File.read(File.join(crossing_root, path)) ] })
58
+
59
+ def hub_files = @hub_files ||= Dir.glob("#{HubKernel::HubFolders.code_glob}/**/*.rb", base: crossing_root)
45
60
  end
46
61
  end
47
62
  end
@@ -1,12 +1,17 @@
1
+ require "hub_kernel/hub_folders"
1
2
  require "active_support/core_ext/string/inflections"
2
3
 
3
4
  module HubKernel
4
5
  class Crossings
5
- def initialize(owners:, shared: [], host_layer: nil, interfaces: {}, namespaces: {})
6
+ HUB_MODULE = /extend HubKernel::(Ports|Exposes)\b/
7
+ def initialize(owners: {}, shared: [], host_layer: nil, interfaces: {}, namespaces: {}, folder_files: [])
6
8
  @owners = owners
9
+ @folder_classes = {}
10
+ read_folders(folder_files)
7
11
  @shared = shared
8
12
  @host_layer = host_layer
9
13
  @interfaces = interfaces
14
+ @folder_interfaces = {}
10
15
  @namespaces = namespaces
11
16
  end
12
17
 
@@ -15,15 +20,36 @@ module HubKernel
15
20
  source.scan(/[A-Z]\w*(?:::[A-Z]\w*)*/).filter_map { |name| owned_class_in(name) }.uniq.select { |name| crossing?(name, hub) }
16
21
  end
17
22
 
23
+ def read_folders(paths)
24
+ @folder_classes.merge!(paths.to_h { |path| [ HubFolders.class_of(path), HubFolders.hub_of(path) ] })
25
+ self
26
+ end
27
+
28
+ def read_hub_modules(sources)
29
+ sources.each do |path, source|
30
+ next unless source.match?(HUB_MODULE) && path.match?(HubFolders::CODE_PATH)
31
+
32
+ @folder_interfaces[HubFolders.class_of(path)] = HubFolders.hub_of(path)
33
+ end
34
+ self
35
+ end
36
+
18
37
  def belongs?(path) = !owner_of_file(path).nil?
19
38
 
20
39
  def unowned_classes(paths)
21
- paths.select { |path| path.end_with?(".rb") && !path.start_with?("app/views/") }.map { |path| class_of(path) }.reject { |name| owner_of(name) }
40
+ paths.select { |path| path.end_with?(".rb") && !path.start_with?("app/views/") && !HubFolders.hub_of(path) }.map { |path| class_of(path) }.reject { |name| owner_of(name) }
22
41
  end
23
42
 
24
- def claimed_twice = @owners.values.flatten.tally.select { |_name, hubs| hubs > 1 }.keys
43
+ def claimed_twice
44
+ listed = @owners.values.flatten
45
+ listed.tally.select { |_name, hubs| hubs > 1 }.keys | @owners.flat_map { |hub, classes| classes.select { |name| @folder_classes.fetch(name, hub) != hub } }
46
+ end
47
+
48
+ def listed_in_their_folders = @owners.flat_map { |hub, classes| classes.select { |name| @folder_classes[name] == hub } }
49
+
50
+ def folders_without_hub_module = @folder_classes.values.uniq.reject { |hub| @interfaces.key?(hub) || @folder_interfaces.value?(hub) }.map { |hub| HubFolders.folder(hub) }
25
51
 
26
- def missing_classes = @owners.values.flatten.uniq.reject(&:safe_constantize)
52
+ def missing_classes = @owners.values.flatten.uniq.reject { |name| @folder_classes.key?(name) || name.safe_constantize }
27
53
 
28
54
  private
29
55
 
@@ -32,7 +58,7 @@ module HubKernel
32
58
  def crossing?(name, hub)
33
59
  return false if @shared.include?(name) || owner_of(name) == hub
34
60
 
35
- !(hub == @host_layer && @interfaces.value?(name))
61
+ !(hub == @host_layer && interface?(name))
36
62
  end
37
63
 
38
64
  def owned_class_in(name)
@@ -46,13 +72,17 @@ module HubKernel
46
72
  space && "#{space}::#{name.delete_prefix("#{space}::").split("::").first}"
47
73
  end
48
74
 
49
- def listed_owner_of(class_name) = @interfaces.key(class_name) || @owners.find { |_hub, classes| classes.include?(class_name) }&.first
75
+ def interface?(name) = @interfaces.value?(name) || @folder_interfaces.key?(name)
76
+
77
+ def listed_owner_of(class_name) = @interfaces.key(class_name) || @folder_interfaces[class_name] || @folder_classes[class_name] || @owners.find { |_hub, classes| classes.include?(class_name) }&.first
50
78
 
51
79
  def owner_of(class_name) = listed_owner_of(class_name) || owner_of_namespace(class_name)
52
80
 
53
81
  def owner_of_namespace(class_name) = @namespaces.find { |_hub, spaces| spaces.any? { |space| class_name.start_with?("#{space}::") } }&.first
54
82
 
55
83
  def owner_of_file(path)
84
+ return HubFolders.hub_of(path) if HubFolders.hub_of(path)
85
+
56
86
  return owner_of_view_folder(File.dirname(path.delete_prefix("app/views/")).camelize) if path.start_with?("app/views/")
57
87
 
58
88
  owner_of(class_of(path))
@@ -1,5 +1,19 @@
1
+ require "hub_kernel/hub_folders"
2
+
1
3
  module HubKernel
2
4
  class Engine < ::Rails::Engine
3
5
  isolate_namespace HubKernel
6
+
7
+ initializer "hub_kernel.hub_folders", before: :set_load_path do |app|
8
+ folders = HubFolders.code_paths(app.root)
9
+ app.config.autoload_paths.concat(folders)
10
+ app.config.eager_load_paths.concat(folders)
11
+ app.config.helpers_paths.concat(HubFolders.helper_paths(app.root))
12
+ end
13
+
14
+ initializer "hub_kernel.hub_views" do |app|
15
+ views = HubFolders.view_paths(app.root)
16
+ ActiveSupport.on_load(:action_controller) { append_view_path(views) }
17
+ end
4
18
  end
5
19
  end
@@ -0,0 +1,25 @@
1
+ module HubKernel
2
+ module HubFolders
3
+ ROOT = "hubs".freeze
4
+ CODE = "app".freeze
5
+
6
+ PATH = %r{\A#{ROOT}/([^/]+)/}
7
+ CODE_PATH = %r{\A#{ROOT}/[^/]+/#{CODE}/[^/]+/}
8
+
9
+ def self.folder(hub) = "#{ROOT}/#{hub}"
10
+
11
+ def self.test_folder(hub) = "test/#{folder(hub)}"
12
+
13
+ def self.code_glob = "#{ROOT}/*/#{CODE}"
14
+
15
+ def self.hub_of(path) = path[PATH, 1]&.to_sym
16
+
17
+ def self.class_of(path) = path.sub(CODE_PATH, "").delete_suffix(".rb").camelize
18
+
19
+ def self.code_paths(root) = Dir[root.join("#{code_glob}/*")]
20
+
21
+ def self.view_paths(root) = Dir[root.join("#{code_glob}/views")]
22
+
23
+ def self.helper_paths(root) = Dir[root.join("#{code_glob}/helpers")]
24
+ end
25
+ end
@@ -1,3 +1,3 @@
1
1
  module HubKernel
2
- VERSION = "0.19.0"
2
+ VERSION = "0.21.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.19.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider
@@ -62,6 +62,7 @@ files:
62
62
  - lib/hub_kernel/crossings.rb
63
63
  - lib/hub_kernel/engine.rb
64
64
  - lib/hub_kernel/follow_up.rb
65
+ - lib/hub_kernel/hub_folders.rb
65
66
  - lib/hub_kernel/hubs.rb
66
67
  - lib/hub_kernel/markup.rb
67
68
  - lib/hub_kernel/ports.rb