hub_kernel 0.25.0 → 0.26.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: 3e8e24b52cfa8070ea79fc420ec714555b3e81377725ac9ea8e8a3a20ac280bb
4
- data.tar.gz: 130ba409bb6db19a6cd0d1c34ffc535407ba03255f8e282bd2f333c53bef6000
3
+ metadata.gz: fbd4a1ca059e2203955aab7a07510cd07620427086b6153455f0e84d1f4ea639
4
+ data.tar.gz: 26a9be066b129fa83177261e5f9e15cc72c206c3210dc4fc1f26c6a65ac29676
5
5
  SHA512:
6
- metadata.gz: d159cfc7e9b03980d03de84433ca09d3f1b5a11d758b10a3412a54eff96b8636b1187d9b2e4d54d2582ef7610d43c8df0e63c8a99de1553f17cb698175b34e9a
7
- data.tar.gz: 7e075af8fa7fbe2cc6b7952a45c5a710c7702a39686cf38b0b121166c39f89f06f53e3e9114e3543e606cb22e9628c42ca09f96ba70ceafe31922b74bf491dce
6
+ metadata.gz: 80bcd028c34e3be272d7c81b81852a0814c592df1cdbfb58954095dc4f240db87d02e4cd78afebd7cec0a0a9d1212d5f0585203724eefab74e81d42901b43050
7
+ data.tar.gz: 782767a253efd77447d8be7b2d16dc7086f8715515d19189435cb4a4fc5c64663cb25e721e2056fd19356b82b84b742bfbedab434652b6030a5a9c21fb41323c
data/README.md CHANGED
@@ -167,6 +167,52 @@ the app fills the port with.
167
167
  The model names only its own hub, so the crossing check passes it. The app's
168
168
  `to_prepare` block that fills the port is the one place both models are named.
169
169
 
170
+ ### Has-many links through a record port
171
+ A hub's model lists the records another hub owns that point back at it with
172
+ `has_many_port`, naming its own hub and one of that hub's record ports:
173
+
174
+ ```ruby
175
+ module Sales
176
+ class Job < ApplicationRecord
177
+ has_many_port :quotes, port: Sales, record: :quote_record
178
+ end
179
+ end
180
+ ```
181
+
182
+ hub_kernel defines an ordinary `has_many :quotes` whose class is the model the app filled
183
+ the port with. `job.quotes` lists the quotes, and `includes`, `preload`, `joins`, `where`
184
+ and `order` work on the link as on any `has_many`. Every other `has_many` option, such as
185
+ `foreign_key:` or `inverse_of:`, and a scope passed before the options, reach the
186
+ association unchanged, apart from the options below that write to the listed records.
187
+ The class name comes from the port, so one passed is not used.
188
+
189
+ A has-many link is always read-only, and no option makes it writable:
190
+
191
+ - A record listed through the link, loaded, preloaded or queried, raises
192
+ `ActiveRecord::ReadOnlyRecord` when it is saved or destroyed.
193
+ - Adding a record to the list with `<<`, `push`, `append`, `concat` or `replace`, removing
194
+ one with `delete`, `destroy`, `delete_all`, `destroy_all` or `clear`, and building or
195
+ creating one with `build`, `new`, `create` or `create!` raise
196
+ `ActiveRecord::ReadOnlyRecord`, on the list and on a query made from it.
197
+ - Assigning the list with `quotes=` or `quote_ids=` raises `ActiveRecord::ReadOnlyRecord`.
198
+ - Writing the listed records all at once with `update_all`, `update_counters`, `insert`,
199
+ `insert!`, `insert_all`, `insert_all!`, `upsert` or `upsert_all` raises
200
+ `ActiveRecord::ReadOnlyRecord`.
201
+ - `autosave`, `counter_cache` and a `dependent` value that destroys, deletes or nullifies
202
+ the listed records each write to them, so passing any of them raises an `ArgumentError`
203
+ naming it.
204
+
205
+ `dependent: :restrict_with_exception` and `dependent: :restrict_with_error` write nothing
206
+ to the listed records, so the link takes them. Deleting the model while the link lists
207
+ records is refused, the first way by raising `ActiveRecord::DeleteRestrictionError` and
208
+ the second by adding an error and returning false, and deleting it when the link lists
209
+ none goes ahead.
210
+
211
+ The link is defined as soon as both the model has loaded and the port is filled, in
212
+ whichever order those happen, the same way as a `belongs_to_port` link. A boot with eager
213
+ loading on defines it, and after a code reload in development the reloaded model lists
214
+ the reloaded model the app fills the port with.
215
+
170
216
  ### The start check
171
217
  Every hub that declares a port or exposes a method is on `HubKernel::Hubs.list`. Call
172
218
  `HubKernel::Hubs.check!` after the app fills its ports, and the app refuses to start
@@ -1,6 +1,9 @@
1
1
  module HubKernel
2
2
  module PortAssociations
3
3
  WRITING_OPTIONS = %i[autosave dependent touch counter_cache].freeze
4
+ LIST_WRITING_OPTIONS = %i[autosave dependent counter_cache].freeze
5
+ RESTRICTING = %i[restrict_with_error restrict_with_exception].freeze
6
+ LIST_WRITING_METHODS = %i[<< push append concat build new create create! delete destroy delete_all destroy_all clear replace update_all insert insert! insert_all insert_all! upsert upsert_all].freeze
4
7
 
5
8
  def belongs_to_port(name, scope = nil, port:, record:, **options)
6
9
  refuse_writing_options(name, options)
@@ -10,18 +13,35 @@ module HubKernel
10
13
  end
11
14
  end
12
15
 
16
+ def has_many_port(name, scope = nil, port:, record:, **options)
17
+ refuse_writing_options(name, options.reject { |option, value| option == :dependent && RESTRICTING.include?(value) }, LIST_WRITING_OPTIONS)
18
+ refusal = "#{self.name}'s #{name} link is read-only, so no record can be written through it"
19
+ port.link_record(record, [ self.name, name ]) do |model|
20
+ has_many name, read_only(scope, refusing_list(refusal)), **options, class_name: model.name
21
+ refuse_replacing_list(name, refusal)
22
+ end
23
+ end
24
+
13
25
  private
14
26
 
15
- def refuse_writing_options(name, options)
16
- writing = options.keys & WRITING_OPTIONS
27
+ def refuse_writing_options(name, options, writers = WRITING_OPTIONS)
28
+ writing = options.keys & writers
17
29
  raise ArgumentError, "#{name} is a read-only record port link, so it does not take #{writing.to_sentence}" if writing.any?
18
30
  end
19
31
 
20
- def read_only(scope)
21
- return -> { readonly } unless scope
22
- return -> { instance_exec(&scope).readonly } if scope.arity.zero?
32
+ def read_only(scope, *extensions)
33
+ return -> { readonly.extending(*extensions) } unless scope
34
+ return -> { instance_exec(&scope).readonly.extending(*extensions) } if scope.arity.zero?
35
+
36
+ ->(owner) { instance_exec(owner, &scope).readonly.extending(*extensions) }
37
+ end
38
+
39
+ def refusing_list(refusal)
40
+ Module.new { LIST_WRITING_METHODS.each { |method| define_method(method) { |*| raise ActiveRecord::ReadOnlyRecord, refusal } } }
41
+ end
23
42
 
24
- ->(owner) { instance_exec(owner, &scope).readonly }
43
+ def refuse_replacing_list(name, refusal)
44
+ [ :"#{name}=", :"#{name.to_s.singularize}_ids=" ].each { |method| define_method(method) { |*| raise ActiveRecord::ReadOnlyRecord, refusal } }
25
45
  end
26
46
 
27
47
  def refuse_creating_through(name)
@@ -1,3 +1,3 @@
1
1
  module HubKernel
2
- VERSION = "0.25.0"
2
+ VERSION = "0.26.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.25.0
4
+ version: 0.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider