hub_kernel 0.18.0 → 0.20.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: 026174ef5963518769182562042464615e204f2a32f7e4260c9ed390fc5b2160
4
- data.tar.gz: 5d2fbdbb14248e8e145eb545246063e883fd196fb68b930180ff9b067a9c341c
3
+ metadata.gz: 1bb347a71c0f40d275dc0c2d504fb186b474538a1b4ee80c7e4fd1270f064926
4
+ data.tar.gz: be51e4ff8a3b09e83cd2904745c39f30088c7471c0127f66fcd8107bc9f6272b
5
5
  SHA512:
6
- metadata.gz: 9a7a2a568be438e982a2c0d688fdc61e5fd4ade3b49070d3ffccff71e662b6748185e2e19a1b1beedb6f3c2d14bf5e899faf12598ce7cbcf6e225d7a8c859389
7
- data.tar.gz: 5f189db08ded136cb2adc871613545ba62f8f163938b63587e47a7382a745d12cceff6606fb7a5f137b4b0db5ca2c09db321cd91dc600140a7c4edde1e2f4c1f
6
+ metadata.gz: db66d8c988b25b8639cde40e2b0212b8a651fb4b831cd675b0b615e2b7ee1a85e9bdbee002be16c03bc91d1cb7ad32a904139b384049281b83804d30636ad37c
7
+ data.tar.gz: 5524ff6a33af31e8e9c039568daa6f0eeb2e8e12202d8f032d78ed890f54f90b662ac771b0e58213e569e8f837f040a8947af61161e95c18a3314188c2438db4
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:
@@ -105,6 +135,11 @@ The test fails and names each port the app left unfilled, and each port filled w
105
135
  something that cannot be called.
106
136
 
107
137
  ### The exposed list
138
+ The exposed list, calling by name, the permission check, the account scope and the errors
139
+ they raise come from hub_kernel-interface, which hub_kernel depends on, so a hub keeps
140
+ writing `extend HubKernel::Exposes` with no other gem to add. Interface gems such as
141
+ hub_kernel-api depend on hub_kernel-interface alone.
142
+
108
143
  A hub names each method outside callers may reach, the values it takes, and whether it
109
144
  writes. A layer such as a JSON endpoint looks a method up by name and serves only what
110
145
  the hub exposes:
@@ -148,7 +183,7 @@ It refuses a missing person or account as a call by name does, and a hub that ex
148
183
  nothing, or a person allowed nothing, gets an empty list.
149
184
 
150
185
  ### The host's permission check and account scope
151
- Every call by name asks the host whether the person may take the action on the account,
186
+ hub_kernel-interface holds both settings. Every call by name asks the host whether the person may take the action on the account,
152
187
  and runs the method inside the host's scope for that account. The host sets both once
153
188
  for the whole app:
154
189
 
@@ -204,6 +239,11 @@ class HubCrossingsTest < ActiveSupport::TestCase
204
239
  end
205
240
  ```
206
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.
207
247
  - A constant inside another hub's class counts as naming that class.
208
248
  - A name no hub owns, such as a namespace, is not a crossing, and neither is a class on
209
249
  the `shared` list.
@@ -213,7 +253,9 @@ end
213
253
  - A view belongs to the hub that owns its controller, then to the hub that owns the
214
254
  record its folder is named after, and otherwise to the `host_layer`.
215
255
  - The host's layer may name any hub's interface module, and a hub naming another hub's
216
- 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`.
217
259
  - The test fails with "The crossing check found no files to read" when none of the files
218
260
  it read belongs to a hub or the host's layer, so a wrong `files:` pattern or an empty
219
261
  map does not pass unnoticed.
@@ -222,7 +264,14 @@ end
222
264
  own Shop::Receipt".
223
265
  - It fails naming each class the map names that no longer exists in the app, such as "The
224
266
  map names Shop::Ghost, which does not exist", so a renamed or removed class does not
225
- stay in the map.
267
+ stay in the map. A class whose file sits in a hub folder is never reported this way.
268
+ - While hubs move into folders, a class the map gives one hub whose file sits in another
269
+ hub's folder is owned by two hubs. A class the map still lists whose file already sits
270
+ in that hub's folder fails with "Shop::Purchase sits in its hub's folder, so take it off
271
+ the list".
272
+ - It fails naming each folder under `hubs/` that holds no module extending
273
+ `HubKernel::Ports` or `HubKernel::Exposes`, such as "hubs/suplies holds no hub module",
274
+ so a misspelled or half-made hub folder does not pass unnoticed.
226
275
 
227
276
  ## Installation
228
277
  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,6 +1,6 @@
1
- module HubKernel
2
- class MissingArgumentError < ArgumentError; end
1
+ require "hub_kernel/exposes"
3
2
 
3
+ module HubKernel
4
4
  module Action
5
5
  attr_reader :person, :account, :values
6
6
 
@@ -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,9 +1,13 @@
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
@@ -15,15 +19,36 @@ module HubKernel
15
19
  source.scan(/[A-Z]\w*(?:::[A-Z]\w*)*/).filter_map { |name| owned_class_in(name) }.uniq.select { |name| crossing?(name, hub) }
16
20
  end
17
21
 
22
+ def read_folders(paths)
23
+ @folder_classes.merge!(paths.to_h { |path| [ HubFolders.class_of(path), HubFolders.hub_of(path) ] })
24
+ self
25
+ end
26
+
27
+ def read_hub_modules(sources)
28
+ sources.each do |path, source|
29
+ next unless source.match?(HUB_MODULE) && path.match?(HubFolders::CODE_PATH)
30
+
31
+ @interfaces = @interfaces.merge(HubFolders.hub_of(path) => HubFolders.class_of(path))
32
+ end
33
+ self
34
+ end
35
+
18
36
  def belongs?(path) = !owner_of_file(path).nil?
19
37
 
20
38
  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) }
39
+ 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
40
  end
23
41
 
24
- def claimed_twice = @owners.values.flatten.tally.select { |_name, hubs| hubs > 1 }.keys
42
+ def claimed_twice
43
+ listed = @owners.values.flatten
44
+ listed.tally.select { |_name, hubs| hubs > 1 }.keys | @owners.flat_map { |hub, classes| classes.select { |name| @folder_classes.fetch(name, hub) != hub } }
45
+ end
46
+
47
+ def listed_in_their_folders = @owners.flat_map { |hub, classes| classes.select { |name| @folder_classes[name] == hub } }
25
48
 
26
- def missing_classes = @owners.values.flatten.uniq.reject(&:safe_constantize)
49
+ def folders_without_hub_module = @folder_classes.values.uniq.reject { |hub| @interfaces.key?(hub) }.map { |hub| HubFolders.folder(hub) }
50
+
51
+ def missing_classes = @owners.values.flatten.uniq.reject { |name| @folder_classes.key?(name) || name.safe_constantize }
27
52
 
28
53
  private
29
54
 
@@ -46,13 +71,15 @@ module HubKernel
46
71
  space && "#{space}::#{name.delete_prefix("#{space}::").split("::").first}"
47
72
  end
48
73
 
49
- def listed_owner_of(class_name) = @interfaces.key(class_name) || @owners.find { |_hub, classes| classes.include?(class_name) }&.first
74
+ def listed_owner_of(class_name) = @interfaces.key(class_name) || @folder_classes[class_name] || @owners.find { |_hub, classes| classes.include?(class_name) }&.first
50
75
 
51
76
  def owner_of(class_name) = listed_owner_of(class_name) || owner_of_namespace(class_name)
52
77
 
53
78
  def owner_of_namespace(class_name) = @namespaces.find { |_hub, spaces| spaces.any? { |space| class_name.start_with?("#{space}::") } }&.first
54
79
 
55
80
  def owner_of_file(path)
81
+ return HubFolders.hub_of(path) if HubFolders.hub_of(path)
82
+
56
83
  return owner_of_view_folder(File.dirname(path.delete_prefix("app/views/")).camelize) if path.start_with?("app/views/")
57
84
 
58
85
  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,14 +1,17 @@
1
1
  require "hub_kernel/authz"
2
2
  require "hub_kernel/context"
3
+ require "hub_kernel/interface/exposing_hubs"
3
4
 
4
5
  module HubKernel
5
6
  module Hubs
6
7
  def self.add(hub)
7
- list.reject! { |listed| listed.name == hub.name }
8
- list << hub
8
+ registered.reject! { |listed| listed.name == hub.name }
9
+ registered << hub
9
10
  end
10
11
 
11
- def self.list = @list ||= []
12
+ def self.registered = @registered ||= []
13
+
14
+ def self.list = (registered + Interface::ExposingHubs.list).uniq { |hub| hub.name || hub }
12
15
 
13
16
  def self.check!
14
17
  unwired = list.select { |hub| hub.respond_to?(:unwired_ports) }.flat_map(&:unwired_ports) + unfilled_host_answers
@@ -1,9 +1,9 @@
1
1
  require "active_support/core_ext/string/inflections"
2
2
  require "hub_kernel/hubs"
3
3
 
4
- module HubKernel
5
- class UnwiredPortError < StandardError; end
4
+ require "hub_kernel/exposes"
6
5
 
6
+ module HubKernel
7
7
  module Ports
8
8
  def port(name, as:)
9
9
  Hubs.add(self)
@@ -1,3 +1,3 @@
1
1
  module HubKernel
2
- VERSION = "0.18.0"
2
+ VERSION = "0.20.0"
3
3
  end
data/lib/hub_kernel.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require "hub_kernel-interface"
1
2
  require "hub_kernel/version"
2
3
  require "hub_kernel/engine"
3
4
  require "hub_kernel/action"
@@ -5,7 +6,6 @@ require "hub_kernel/answer"
5
6
  require "hub_kernel/follow_up"
6
7
  require "hub_kernel/markup"
7
8
  require "hub_kernel/ports"
8
- require "hub_kernel/exposes"
9
9
 
10
10
  module HubKernel
11
11
  end
@@ -164,10 +164,7 @@ hub_kernel lets a hub, one domain area written as a module, state what it needs
164
164
  - Each entry is the same one `exposed` answers.
165
165
  - A hub that lists nothing answers `[]` from both, and a person allowed nothing answers `[]` from `exposures_for`.
166
166
  - `exposures_for` asks the permission check once per entry with the same action name `call_exposed` uses, such as `"supplies:record_purchase"`, and runs no hub method.
167
- - `exposures_for` fails in this order, and each failure stops it:
168
- 1. `HubKernel::MissingArgumentError` when `person:` or `account:` is `nil`.
169
- 2. An error naming the host's permission check when the host has not set it and the hub lists at least one method.
170
- 3. `HubKernel::NonBooleanAnswerError` when the permission check answers anything but `true` or `false`.
167
+ - `exposures_for` raises `HubKernel::MissingArgumentError` when `person:` or `account:` is `nil`, before asking the permission check.
171
168
  - Pass the same `person:` and `account:` the interface passes to `call_exposed`.
172
169
  - Ask the developer whether the interface shows every exposed method or only those the person may call. Do not pick yourself.
173
170
 
@@ -11,7 +11,7 @@ This local explains hub_kernel and the words it uses. It makes no changes and gi
11
11
 
12
12
  hub_kernel splits a Rails app into hubs, where each hub is one area of the domain, such as supplies or finance, usually shipped in its own domain gem. A hub states what it needs from outside itself as named ports, and the host app decides what fills each one, so a hub never names another hub's code directly.
13
13
 
14
- A hub also lists the methods outside callers may reach by name, with the values each takes and whether it writes. An interface such as a JSON API calls through that list, and every call is checked against the host's permission rule and run inside the host's account scope. The same list can be read back whole, or cut down to only the methods one person is allowed to call on one account, so an interface can show a person what they may do before they try it. Reach for hub_kernel when an app has several domain areas that should talk to each other only through declared entry points, and you want the app to refuse to start, or the suite to fail, when that is not true.
14
+ A hub also lists the methods outside callers may reach by name, with the values each takes and whether it writes. An interface such as a JSON API calls through that list, and every call is checked against the host's permission rule and run inside the host's account scope. The same list can be read back whole, or cut down to only the methods one person is allowed to call on one account, so an interface can show a person what they may do before they try it. That list, calling by name, the permission check and the account scope come from hub_kernel-interface, which hub_kernel depends on, so a hub that lists methods needs no second gem. Reach for hub_kernel when an app has several domain areas that should talk to each other only through declared entry points, and you want the app to refuse to start, or the suite to fail, when that is not true.
15
15
 
16
16
  ## Interface
17
17
 
@@ -29,7 +29,8 @@ Decide which side you are on:
29
29
 
30
30
  ## Conventions
31
31
 
32
- - **Hub** — one domain area, written as a module. In a domain gem it sits under the gem's namespace, such as Billing::Ledger. A hub is known to the app once it declares a port or lists a method callable by name.
32
+ - **Hub** — one domain area, written as a module. In a domain gem it sits under the gem's namespace, such as Billing::Ledger. A hub is known to the app once it declares a port or lists a method callable by name, and the start check covers both kinds.
33
+ - **hub_kernel-interface** — the smaller gem hub_kernel builds on, holding the methods-callable-by-name list, the permission check and the account scope. Interface gems such as hub_kernel-api depend on it alone, while hubs and host apps keep depending on hub_kernel.
33
34
  - **Ports** — what a hub needs from outside. Each has a name and the method the hub's own code calls, such as a spend recorder called as record_spend.
34
35
  - **Filling ports** — the host app sets each of a hub's ports to any callable, usually another hub's method, in its setup that runs again on every code reload. Ports are "wired" once they are filled.
35
36
  - **Start check** — the app refuses to start while any hub's ports are not wired, and names each one, such as "Supplies' spend recorder is not wired". While any hub lists a method callable by name, it also refuses to start until the permission check and the account scope are both set to something that can be called, and names whichever is missing or cannot be called. An app whose hubs list no such methods starts without either.
@@ -14,7 +14,7 @@ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to sta
14
14
  - `gem "hub_kernel"` — the Gemfile line that adds the gem to the host app.
15
15
  - `HubKernel::Hubs.check!` — raises `HubKernel::UnwiredPortError` naming every unfilled port of every loaded hub, one per line, such as "Supplies' spend recorder is not wired", and, while any loaded hub exposes a method, the permission check or the account scope when either is unset or cannot be called.
16
16
  - `HubKernel::Hubs.list` — the hubs that have loaded and declared at least one port or exposed at least one method.
17
- - `HubKernel::UnwiredPortError` — raised by the start check, by calling an unfilled port, by a call by name while the permission check or the account scope is unset, and by listing what a person may call from a hub that exposes at least one method while the permission check is unset.
17
+ - `HubKernel::UnwiredPortError` — raised by the start check, by calling an unfilled port, and by a call by name while the permission check or the account scope is unset.
18
18
  - `HubKernel::Authz.check` — the host-wide permission rule, a callable taking a person, an action and an account and answering `true` or `false`.
19
19
  - `HubKernel::Context.scope` — the host-wide account scope, a callable taking an account and a block and running the block inside that account.
20
20
  - `HubKernel::Conformance::Hub` — a test module that fails naming each of one hub's ports left unfilled or filled with something that cannot be called.
@@ -28,6 +28,8 @@ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to sta
28
28
  gem "hub_kernel"
29
29
  ```
30
30
 
31
+ `bundle install` also installs hub_kernel-interface, which holds the permission check and the account scope, so add no other gem for them.
32
+
31
33
  2. List the hubs the app uses and each port they declare. Ask the developer what fills each port, such as another hub's method. Do not pick a filler yourself.
32
34
 
33
35
  3. Fill every port and run the start check in a `to_prepare` block in an initializer, such as `config/initializers/hubs.rb`, so both run again after every code reload:
@@ -57,7 +59,6 @@ hub_kernel lets a Rails host app fill the ports each hub declares, refuse to sta
57
59
 
58
60
  - While any loaded hub exposes a method, the start check refuses to start until both are set to something that responds to `call`.
59
61
  - The permission check answers both a call by name and a listing of what a person may call, so the two always agree.
60
- - Listing what a person may call needs only the permission check, while a call by name needs both.
61
62
  - The action is a string made of the hub's own name, without its namespace and in snake case, then the method, such as `"supplies:record_purchase"` for `Shop::Supplies`.
62
63
  - The permission check must answer exactly `true` or `false`, and any other answer makes the call or the listing raise an error.
63
64
  - The account scope must take the block and run it, or the hub method never runs.
@@ -32,10 +32,7 @@ sources:
32
32
  - lib/hub_kernel.rb
33
33
  - lib/hub_kernel/ports.rb
34
34
  - lib/hub_kernel/hubs.rb
35
- - lib/hub_kernel/exposes.rb
36
35
  - lib/hub_kernel/action.rb
37
- - lib/hub_kernel/authz.rb
38
- - lib/hub_kernel/context.rb
39
36
  - lib/hub_kernel/crossings.rb
40
37
  - lib/hub_kernel/conformance/hub.rb
41
38
  - lib/hub_kernel/conformance/exposed.rb
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.18.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: 8.1.3
26
+ - !ruby/object:Gem::Dependency
27
+ name: hub_kernel-interface
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.3'
26
40
  description: 'hub_kernel wraps a domain and connects it to a host app''s infrastructure
27
41
  through named ports. It ships no domain of its own: each hub is hub_kernel wrapped
28
42
  around one domain.'
@@ -40,17 +54,15 @@ files:
40
54
  - lib/hub_kernel.rb
41
55
  - lib/hub_kernel/action.rb
42
56
  - lib/hub_kernel/answer.rb
43
- - lib/hub_kernel/authz.rb
44
57
  - lib/hub_kernel/conformance/action.rb
45
58
  - lib/hub_kernel/conformance/crossings.rb
46
59
  - lib/hub_kernel/conformance/exposed.rb
47
60
  - lib/hub_kernel/conformance/hub.rb
48
61
  - lib/hub_kernel/conformance/partial.rb
49
- - lib/hub_kernel/context.rb
50
62
  - lib/hub_kernel/crossings.rb
51
63
  - lib/hub_kernel/engine.rb
52
- - lib/hub_kernel/exposes.rb
53
64
  - lib/hub_kernel/follow_up.rb
65
+ - lib/hub_kernel/hub_folders.rb
54
66
  - lib/hub_kernel/hubs.rb
55
67
  - lib/hub_kernel/markup.rb
56
68
  - lib/hub_kernel/ports.rb
@@ -1,8 +0,0 @@
1
- module HubKernel
2
- class NonBooleanAnswerError < StandardError; end
3
- class NotAllowed < StandardError; end
4
-
5
- module Authz
6
- singleton_class.attr_accessor :check
7
- end
8
- end
@@ -1,5 +0,0 @@
1
- module HubKernel
2
- module Context
3
- singleton_class.attr_accessor :scope
4
- end
5
- end
@@ -1,92 +0,0 @@
1
- require "hub_kernel/action"
2
- require "hub_kernel/authz"
3
- require "hub_kernel/context"
4
- require "hub_kernel/ports"
5
- require "hub_kernel/hubs"
6
-
7
- module HubKernel
8
- class UnexposedMethodError < StandardError; end
9
- class Refused < StandardError; end
10
-
11
- module Exposes
12
- Exposed = Data.define(:name, :takes, :writes)
13
-
14
- def exposes(name, takes:, writes:)
15
- Hubs.add(self)
16
- exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes)
17
- end
18
-
19
- def exposed(name) = exposed_methods[name.to_s]
20
-
21
- def exposures = exposed_methods.values
22
-
23
- def exposures_for(person:, account:)
24
- refuse_without_caller(person, account)
25
-
26
- exposures.select { |exposure| allowed?(exposure, person, account) }
27
- end
28
-
29
- def call_exposed(name, values:, person:, account:)
30
- refuse_without_caller(person, account)
31
-
32
- exposure = exposed(name) || raise(UnexposedMethodError, "#{exposing_hub} does not expose #{name}")
33
- refuse_unless_allowed(exposure, person, account)
34
- refuse_missing_values(exposure, values)
35
- within_account(account) { public_send(exposure.name, **values.slice(*exposure.takes)) }
36
- end
37
-
38
- def exposure_problems
39
- exposed_methods.values.filter_map { |exposure| exposure_problem(exposure) }
40
- end
41
-
42
- private
43
-
44
- def exposed_methods = @exposed_methods ||= {}
45
-
46
- def exposing_hub = name.demodulize
47
-
48
- def refuse_without_caller(person, account)
49
- raise MissingArgumentError, "A call by name needs a person" if person.nil?
50
- raise MissingArgumentError, "A call by name needs an account" if account.nil?
51
- end
52
-
53
- def refuse_unless_allowed(exposure, person, account)
54
- raise NotAllowed, "#{exposing_hub} #{exposure.name}" unless allowed?(exposure, person, account)
55
- end
56
-
57
- def allowed?(exposure, person, account)
58
- raise UnwiredPortError, "hub_kernel's permission check is not filled" unless Authz.check
59
-
60
- answer = Authz.check.call(person, "#{exposing_hub.underscore}:#{exposure.name}", account)
61
- raise NonBooleanAnswerError, "The permission check must answer true or false, got #{answer.inspect}" unless [ true, false ].include?(answer)
62
-
63
- answer
64
- end
65
-
66
- def within_account(account, &call)
67
- raise UnwiredPortError, "hub_kernel's account scope is not filled" unless Context.scope
68
-
69
- Context.scope.call(account, &call)
70
- end
71
-
72
- def refuse_missing_values(exposure, values)
73
- missing = keywords(exposure, :keyreq) - values.keys
74
- raise MissingArgumentError, "Give #{missing.join(", ")}" if missing.any?
75
- end
76
-
77
- def exposure_problem(exposure)
78
- return "#{exposing_hub} exposes #{exposure.name}, which it has no method for" unless respond_to?(exposure.name)
79
- return if takes_listed_values?(exposure)
80
-
81
- "#{exposing_hub} exposes #{exposure.name} with #{exposure.takes.join(", ")}, but it takes #{keywords(exposure, :keyreq, :key).join(", ")}"
82
- end
83
-
84
- def takes_listed_values?(exposure)
85
- (keywords(exposure, :keyreq) - exposure.takes).empty? && (takes_any_values?(exposure) || (exposure.takes - keywords(exposure, :keyreq, :key)).empty?)
86
- end
87
-
88
- def takes_any_values?(exposure) = method(exposure.name).parameters.any? { |kind, _| kind == :keyrest }
89
-
90
- def keywords(exposure, *kinds) = method(exposure.name).parameters.filter_map { |kind, value| value if kinds.include?(kind) }
91
- end
92
- end