hub_kernel 0.24.0 → 0.25.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 +4 -4
- data/README.md +71 -3
- data/lib/hub_kernel/conformance/hub.rb +1 -1
- data/lib/hub_kernel/engine.rb +5 -0
- data/lib/hub_kernel/hubs.rb +1 -1
- data/lib/hub_kernel/port_associations.rb +33 -0
- data/lib/hub_kernel/ports.rb +32 -1
- data/lib/hub_kernel/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e8e24b52cfa8070ea79fc420ec714555b3e81377725ac9ea8e8a3a20ac280bb
|
|
4
|
+
data.tar.gz: 130ba409bb6db19a6cd0d1c34ffc535407ba03255f8e282bd2f333c53bef6000
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d159cfc7e9b03980d03de84433ca09d3f1b5a11d758b10a3412a54eff96b8636b1187d9b2e4d54d2582ef7610d43c8df0e63c8a99de1553f17cb698175b34e9a
|
|
7
|
+
data.tar.gz: 7e075af8fa7fbe2cc6b7952a45c5a710c7702a39686cf38b0b121166c39f89f06f53e3e9114e3543e606cb22e9628c42ca09f96ba70ceafe31922b74bf491dce
|
data/README.md
CHANGED
|
@@ -102,6 +102,71 @@ returns its answer. An error it raises reaches the caller unchanged. Calling a p
|
|
|
102
102
|
app left unfilled raises `HubKernel::UnwiredPortError`, such as "Supplies' spend
|
|
103
103
|
recorder is not wired".
|
|
104
104
|
|
|
105
|
+
### Record ports
|
|
106
|
+
A record port is a port the app fills with an Active Record model class, so a hub can
|
|
107
|
+
name a model another part of the app owns without naming its class. The hub declares it
|
|
108
|
+
by name:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
module Fulfillment
|
|
112
|
+
extend HubKernel::Ports
|
|
113
|
+
|
|
114
|
+
record_port :job_record
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The app fills it with the model, in the same setup as its other ports:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
Rails.application.config.to_prepare do
|
|
122
|
+
Fulfillment.job_record = Job
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`Fulfillment.job_record` then answers `Job`. A record port is on the hub's
|
|
127
|
+
`port_names`, so the start check, the hub check and kept ports all read it.
|
|
128
|
+
|
|
129
|
+
### Links through a record port
|
|
130
|
+
A hub's model links to a record another hub owns with `belongs_to_port`, naming its own
|
|
131
|
+
hub and one of that hub's record ports instead of the other model's class:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
module Sales
|
|
135
|
+
class Quote < ApplicationRecord
|
|
136
|
+
belongs_to_port :job, port: Sales, record: :job_record
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
hub_kernel defines an ordinary `belongs_to :job` whose class is the model the app filled
|
|
142
|
+
the port with. `quote.job` reads the job, and `includes`, `preload`, `joins`, `where` and
|
|
143
|
+
`order` work on the link as on any `belongs_to`. Every other `belongs_to` option, such as
|
|
144
|
+
`foreign_key:` or `optional:`, and a scope passed before the options, reach the
|
|
145
|
+
association unchanged. The class name comes from the port, so one passed is not used.
|
|
146
|
+
|
|
147
|
+
A link is always read-only, and no option makes it writable:
|
|
148
|
+
|
|
149
|
+
- A record read through the link, loaded or preloaded, raises
|
|
150
|
+
`ActiveRecord::ReadOnlyRecord` when it is saved or destroyed.
|
|
151
|
+
- `build_job`, `create_job` and `create_job!` raise `ActiveRecord::ReadOnlyRecord`, and so
|
|
152
|
+
does assigning an unsaved record to `job`.
|
|
153
|
+
- The records a scope loads are read-only too.
|
|
154
|
+
- `autosave`, `dependent`, `touch` and `counter_cache` each write to the linked record, so
|
|
155
|
+
passing any of them raises an `ArgumentError` naming it.
|
|
156
|
+
|
|
157
|
+
The model still sets which record it links to, by assigning a saved record or its id, and
|
|
158
|
+
saves itself. A change to the linked record goes through the hub that owns it.
|
|
159
|
+
|
|
160
|
+
The link is defined as soon as both the model has loaded and the port is filled, in
|
|
161
|
+
whichever order those happen. A model that loads after its port is filled has the link
|
|
162
|
+
when it loads. A model that loads first waits on its hub, and filling the port defines the
|
|
163
|
+
link. A boot with eager loading on defines it the same way. After a code reload in
|
|
164
|
+
development, the reloaded model has the link again, and it points at the reloaded model
|
|
165
|
+
the app fills the port with.
|
|
166
|
+
|
|
167
|
+
The model names only its own hub, so the crossing check passes it. The app's
|
|
168
|
+
`to_prepare` block that fills the port is the one place both models are named.
|
|
169
|
+
|
|
105
170
|
### The start check
|
|
106
171
|
Every hub that declares a port or exposes a method is on `HubKernel::Hubs.list`. Call
|
|
107
172
|
`HubKernel::Hubs.check!` after the app fills its ports, and the app refuses to start
|
|
@@ -115,7 +180,9 @@ end
|
|
|
115
180
|
```
|
|
116
181
|
|
|
117
182
|
It raises `HubKernel::UnwiredPortError` naming every unfilled port with its hub, one per
|
|
118
|
-
line.
|
|
183
|
+
line. It also names each record port filled with something that is not an Active Record
|
|
184
|
+
model, such as "Fulfillment's job record is filled with something that is not an Active
|
|
185
|
+
Record model". Running it inside `to_prepare` checks again after every code reload, and a hub
|
|
119
186
|
declared again on a reload stays on the list once.
|
|
120
187
|
|
|
121
188
|
While any hub exposes a method, it also names the permission check or the account scope
|
|
@@ -136,8 +203,9 @@ class SuppliesHubTest < ActiveSupport::TestCase
|
|
|
136
203
|
end
|
|
137
204
|
```
|
|
138
205
|
|
|
139
|
-
The test fails and names each port the app left unfilled,
|
|
140
|
-
something that cannot be called
|
|
206
|
+
The test fails and names each port the app left unfilled, each port filled with
|
|
207
|
+
something that cannot be called, and each record port filled with something that is not
|
|
208
|
+
an Active Record model.
|
|
141
209
|
|
|
142
210
|
### The exposed list
|
|
143
211
|
The exposed list, calling by name, the permission check, the account scope and the errors
|
|
@@ -13,7 +13,7 @@ module HubKernel
|
|
|
13
13
|
|
|
14
14
|
included do
|
|
15
15
|
test "every port of the hub is filled with something that can be called" do
|
|
16
|
-
problems = the_hub.unwired_ports + the_hub.uncallable_ports
|
|
16
|
+
problems = the_hub.unwired_ports + the_hub.uncallable_ports + the_hub.unmodelled_ports
|
|
17
17
|
flunk problems.join("\n") if problems.any?
|
|
18
18
|
end
|
|
19
19
|
end
|
data/lib/hub_kernel/engine.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "hub_kernel/hub_folders"
|
|
2
|
+
require "hub_kernel/port_associations"
|
|
2
3
|
|
|
3
4
|
module HubKernel
|
|
4
5
|
class Engine < ::Rails::Engine
|
|
@@ -16,6 +17,10 @@ module HubKernel
|
|
|
16
17
|
ActiveSupport.on_load(:action_controller) { append_view_path(views) }
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
initializer "hub_kernel.port_associations" do
|
|
21
|
+
ActiveSupport.on_load(:active_record) { extend HubKernel::PortAssociations }
|
|
22
|
+
end
|
|
23
|
+
|
|
19
24
|
config.to_prepare { HubFolders.hub_modules(Rails.root).each(&:constantize) }
|
|
20
25
|
end
|
|
21
26
|
end
|
data/lib/hub_kernel/hubs.rb
CHANGED
|
@@ -14,7 +14,7 @@ module HubKernel
|
|
|
14
14
|
def self.list = (registered + Interface::ExposingHubs.list).uniq { |hub| hub.name || hub }
|
|
15
15
|
|
|
16
16
|
def self.check!
|
|
17
|
-
unwired = list.select { |hub| hub.respond_to?(:unwired_ports) }.flat_map
|
|
17
|
+
unwired = list.select { |hub| hub.respond_to?(:unwired_ports) }.flat_map { |hub| hub.unwired_ports + hub.unmodelled_ports } + unfilled_host_answers
|
|
18
18
|
raise UnwiredPortError, unwired.join("\n") if unwired.any?
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module HubKernel
|
|
2
|
+
module PortAssociations
|
|
3
|
+
WRITING_OPTIONS = %i[autosave dependent touch counter_cache].freeze
|
|
4
|
+
|
|
5
|
+
def belongs_to_port(name, scope = nil, port:, record:, **options)
|
|
6
|
+
refuse_writing_options(name, options)
|
|
7
|
+
port.link_record(record, [ self.name, name ]) do |model|
|
|
8
|
+
belongs_to name, read_only(scope), **options, class_name: model.name
|
|
9
|
+
refuse_creating_through(name)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def refuse_writing_options(name, options)
|
|
16
|
+
writing = options.keys & WRITING_OPTIONS
|
|
17
|
+
raise ArgumentError, "#{name} is a read-only record port link, so it does not take #{writing.to_sentence}" if writing.any?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def read_only(scope)
|
|
21
|
+
return -> { readonly } unless scope
|
|
22
|
+
return -> { instance_exec(&scope).readonly } if scope.arity.zero?
|
|
23
|
+
|
|
24
|
+
->(owner) { instance_exec(owner, &scope).readonly }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def refuse_creating_through(name)
|
|
28
|
+
refusal = "#{self.name}'s #{name} link is read-only, so it cannot build or create a record"
|
|
29
|
+
%W[build_#{name} create_#{name} create_#{name}!].each { |method| define_method(method) { |*| raise ActiveRecord::ReadOnlyRecord, refusal } }
|
|
30
|
+
define_method(:"#{name}=") { |linked| linked&.new_record? ? raise(ActiveRecord::ReadOnlyRecord, refusal) : super(linked) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/hub_kernel/ports.rb
CHANGED
|
@@ -13,6 +13,20 @@ module HubKernel
|
|
|
13
13
|
define_singleton_method(as) { |*args, **options| public_send(name).call(*args, **options) }
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
def record_port(name)
|
|
17
|
+
Hubs.add(self)
|
|
18
|
+
declared_ports << name
|
|
19
|
+
record_ports << name
|
|
20
|
+
singleton_class.attr_reader name
|
|
21
|
+
define_singleton_method(:"#{name}=") { |model| fill_record_port(name, model) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def link_record(name, model_and_link, &link)
|
|
25
|
+
links_to(name)[model_and_link] = link
|
|
26
|
+
filling = public_send(name)
|
|
27
|
+
link.call(filling) if active_record_model?(filling)
|
|
28
|
+
end
|
|
29
|
+
|
|
16
30
|
def port_names = declared_ports.dup
|
|
17
31
|
|
|
18
32
|
def unwired_ports
|
|
@@ -20,14 +34,31 @@ module HubKernel
|
|
|
20
34
|
end
|
|
21
35
|
|
|
22
36
|
def uncallable_ports
|
|
23
|
-
declared_ports.select { |name| instance_variable_get(:"@#{name}")&.respond_to?(:call) == false }
|
|
37
|
+
(declared_ports - record_ports).select { |name| instance_variable_get(:"@#{name}")&.respond_to?(:call) == false }
|
|
24
38
|
.map { |name| "#{hub_name} #{name.to_s.tr("_", " ")} is filled with something that cannot be called" }
|
|
25
39
|
end
|
|
26
40
|
|
|
41
|
+
def unmodelled_ports
|
|
42
|
+
record_ports.select { |name| instance_variable_get(:"@#{name}") }
|
|
43
|
+
.reject { |name| active_record_model?(instance_variable_get(:"@#{name}")) }
|
|
44
|
+
.map { |name| "#{hub_name} #{name.to_s.tr("_", " ")} is filled with something that is not an Active Record model" }
|
|
45
|
+
end
|
|
46
|
+
|
|
27
47
|
private
|
|
28
48
|
|
|
29
49
|
def declared_ports = @declared_ports ||= []
|
|
30
50
|
|
|
51
|
+
def record_ports = @record_ports ||= []
|
|
52
|
+
|
|
53
|
+
def links_to(name) = (@record_links ||= {})[name] ||= {}
|
|
54
|
+
|
|
55
|
+
def fill_record_port(name, model)
|
|
56
|
+
instance_variable_set(:"@#{name}", model)
|
|
57
|
+
links_to(name).each_value { |link| link.call(model) } if active_record_model?(model)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def active_record_model?(filling) = filling.is_a?(Class) && filling < ActiveRecord::Base
|
|
61
|
+
|
|
31
62
|
def unwired(name)
|
|
32
63
|
->(*, **) { raise UnwiredPortError, unwired_message(name) }
|
|
33
64
|
end
|
data/lib/hub_kernel/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.25.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tylercschneider
|
|
@@ -66,6 +66,7 @@ files:
|
|
|
66
66
|
- lib/hub_kernel/hubs.rb
|
|
67
67
|
- lib/hub_kernel/kept_ports.rb
|
|
68
68
|
- lib/hub_kernel/markup.rb
|
|
69
|
+
- lib/hub_kernel/port_associations.rb
|
|
69
70
|
- lib/hub_kernel/ports.rb
|
|
70
71
|
- lib/hub_kernel/records.rb
|
|
71
72
|
- lib/hub_kernel/version.rb
|