hub_kernel 0.26.0 → 0.27.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: fbd4a1ca059e2203955aab7a07510cd07620427086b6153455f0e84d1f4ea639
4
- data.tar.gz: 26a9be066b129fa83177261e5f9e15cc72c206c3210dc4fc1f26c6a65ac29676
3
+ metadata.gz: d5519634e5f01317012e931057dc2caa508996a18ee0f952ee6e4de867f285ff
4
+ data.tar.gz: 5477988282902fb115811f3f08f6a7eebe8547eee1f7a5ec9c56a3b8f758272c
5
5
  SHA512:
6
- metadata.gz: 80bcd028c34e3be272d7c81b81852a0814c592df1cdbfb58954095dc4f240db87d02e4cd78afebd7cec0a0a9d1212d5f0585203724eefab74e81d42901b43050
7
- data.tar.gz: 782767a253efd77447d8be7b2d16dc7086f8715515d19189435cb4a4fc5c64663cb25e721e2056fd19356b82b84b742bfbedab434652b6030a5a9c21fb41323c
6
+ metadata.gz: bf9219c9a2027f881df22d5aca5149f34408fb6c91156f550ab9fbd992c572d4bc3bfb31f944722fbb73154c37646ef3a3386e9049f3d5aefaf82a5962727f3c
7
+ data.tar.gz: 70344238486a35ed0bff5707898f24f3ee807647cb03f95e0b9426fd640a1506a968fb13dc561d18ab13e1262d203c887c0ae83e2aad7db070e8be095412943e
data/README.md CHANGED
@@ -164,6 +164,10 @@ link. A boot with eager loading on defines it the same way. After a code reload
164
164
  development, the reloaded model has the link again, and it points at the reloaded model
165
165
  the app fills the port with.
166
166
 
167
+ A link that names a port its hub does not declare, or one of the hub's call ports, raises
168
+ an `ArgumentError` when the model loads, naming the model, the hub and the port. A link
169
+ whose `port:` is not a hub module raises an `ArgumentError` naming the model.
170
+
167
171
  The model names only its own hub, so the crossing check passes it. The app's
168
172
  `to_prepare` block that fills the port is the one place both models are named.
169
173
 
@@ -213,6 +217,9 @@ whichever order those happen, the same way as a `belongs_to_port` link. A boot w
213
217
  loading on defines it, and after a code reload in development the reloaded model lists
214
218
  the reloaded model the app fills the port with.
215
219
 
220
+ A has-many link refuses a port that is not one of its hub's record ports, and a `port:`
221
+ that is not a hub module, in the same way as a `belongs_to_port` link.
222
+
216
223
  ### The start check
217
224
  Every hub that declares a port or exposes a method is on `HubKernel::Hubs.list`. Call
218
225
  `HubKernel::Hubs.check!` after the app fills its ports, and the app refuses to start
@@ -393,6 +400,30 @@ end
393
400
  `HubKernel::Ports` or `HubKernel::Exposes`, such as "hubs/suplies holds no hub module",
394
401
  so a misspelled or half-made hub folder does not pass unnoticed.
395
402
 
403
+ ### The unlinked id check
404
+ A host's test can check that every link a hub's model makes is an association, so a new
405
+ link to another hub's record cannot be added as a bare id column:
406
+
407
+ ```ruby
408
+ require "hub_kernel/conformance/unlinked_ids"
409
+
410
+ class UnlinkedIdsTest < ActiveSupport::TestCase
411
+ include HubKernel::Conformance::UnlinkedIds
412
+
413
+ unlinked_ids except: [ "Billing::Invoice#legacy_id" ]
414
+ end
415
+ ```
416
+
417
+ - The test reads every Active Record model under each hub folder's `app/models`, and
418
+ fails naming each column ending in `_id` that no `belongs_to` on that model uses as its
419
+ foreign key, such as "Kitchen::Dish#oven_id has no association using it".
420
+ - A `belongs_to_port` link counts, and so does an ordinary `belongs_to` inside a hub.
421
+ - `except` lists columns the test does not report, each written as the model and the
422
+ column joined by `#`. The `unlinked_ids` line may be left out when there are none.
423
+ - Abstract classes and models whose table does not exist are skipped.
424
+ - The test fails with "The unlinked id check found no model with a table in any hub
425
+ folder" when it finds none, so a wrong setup does not pass unnoticed.
426
+
396
427
  ## Installation
397
428
  Add this line to your application's Gemfile:
398
429
 
@@ -0,0 +1,41 @@
1
+ require "active_support/concern"
2
+ require "hub_kernel/hub_folders"
3
+
4
+ module HubKernel
5
+ module Conformance
6
+ module UnlinkedIds
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def unlinked_ids(except: [])
11
+ define_method(:unlinked_id_exceptions) { except }
12
+ end
13
+ end
14
+
15
+ included do
16
+ test "every id column of a hub model has an association" do
17
+ flunk "The unlinked id check found no model with a table in any hub folder" if hub_models.none?
18
+
19
+ found = hub_models.flat_map { |model| unlinked_columns(model).map { |column| "#{model.name}##{column}" } } - unlinked_id_exceptions
20
+ found = found.map { |entry| "#{entry} has no association using it" }
21
+ assert found.empty?, found.join("\n")
22
+ end
23
+ end
24
+
25
+ def unlinked_id_exceptions = []
26
+
27
+ def hub_models = @hub_models ||= tabled_hub_models
28
+
29
+ def tabled_hub_models
30
+ Dir.glob("#{HubKernel::HubFolders.code_glob}/models/**/*.rb", base: Rails.root).sort
31
+ .map { |path| path.sub(%r{\A.*?/models/}, "").delete_suffix(".rb").camelize.safe_constantize }
32
+ .select { |model| model.is_a?(Class) && model < ActiveRecord::Base && !model.abstract_class? && model.table_exists? }
33
+ end
34
+
35
+ def unlinked_columns(model)
36
+ linked = model.reflect_on_all_associations(:belongs_to).map { |association| association.foreign_key.to_s }
37
+ model.column_names.select { |column| column.end_with?("_id") }.sort - linked
38
+ end
39
+ end
40
+ end
41
+ end
@@ -7,6 +7,7 @@ module HubKernel
7
7
 
8
8
  def belongs_to_port(name, scope = nil, port:, record:, **options)
9
9
  refuse_writing_options(name, options)
10
+ refuse_non_hub(name, port)
10
11
  port.link_record(record, [ self.name, name ]) do |model|
11
12
  belongs_to name, read_only(scope), **options, class_name: model.name
12
13
  refuse_creating_through(name)
@@ -15,6 +16,7 @@ module HubKernel
15
16
 
16
17
  def has_many_port(name, scope = nil, port:, record:, **options)
17
18
  refuse_writing_options(name, options.reject { |option, value| option == :dependent && RESTRICTING.include?(value) }, LIST_WRITING_OPTIONS)
19
+ refuse_non_hub(name, port)
18
20
  refusal = "#{self.name}'s #{name} link is read-only, so no record can be written through it"
19
21
  port.link_record(record, [ self.name, name ]) do |model|
20
22
  has_many name, read_only(scope, refusing_list(refusal)), **options, class_name: model.name
@@ -29,6 +31,10 @@ module HubKernel
29
31
  raise ArgumentError, "#{name} is a read-only record port link, so it does not take #{writing.to_sentence}" if writing.any?
30
32
  end
31
33
 
34
+ def refuse_non_hub(name, port)
35
+ raise ArgumentError, "#{self.name} links #{name} through #{port}, which is not a hub module" unless port.is_a?(HubKernel::Ports)
36
+ end
37
+
32
38
  def read_only(scope, *extensions)
33
39
  return -> { readonly.extending(*extensions) } unless scope
34
40
  return -> { instance_exec(&scope).readonly.extending(*extensions) } if scope.arity.zero?
@@ -22,6 +22,8 @@ module HubKernel
22
22
  end
23
23
 
24
24
  def link_record(name, model_and_link, &link)
25
+ refuse_link(name, model_and_link, "#{self.name} does not declare") unless declared_ports.include?(name)
26
+ refuse_link(name, model_and_link, "is a call port, not a record port") unless record_ports.include?(name)
25
27
  links_to(name)[model_and_link] = link
26
28
  filling = public_send(name)
27
29
  link.call(filling) if active_record_model?(filling)
@@ -50,7 +52,11 @@ module HubKernel
50
52
 
51
53
  def record_ports = @record_ports ||= []
52
54
 
53
- def links_to(name) = (@record_links ||= {})[name] ||= {}
55
+ def refuse_link(name, (model, link), reason)
56
+ raise ArgumentError, "#{model} links #{link} through #{self.name} #{name}, which #{reason}"
57
+ end
58
+
59
+ def links_to(name) =(@record_links ||= {})[name] ||= {}
54
60
 
55
61
  def fill_record_port(name, model)
56
62
  instance_variable_set(:"@#{name}", model)
@@ -1,3 +1,3 @@
1
1
  module HubKernel
2
- VERSION = "0.26.0"
2
+ VERSION = "0.27.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.26.0
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider
@@ -59,6 +59,7 @@ files:
59
59
  - lib/hub_kernel/conformance/exposed.rb
60
60
  - lib/hub_kernel/conformance/hub.rb
61
61
  - lib/hub_kernel/conformance/partial.rb
62
+ - lib/hub_kernel/conformance/unlinked_ids.rb
62
63
  - lib/hub_kernel/crossings.rb
63
64
  - lib/hub_kernel/engine.rb
64
65
  - lib/hub_kernel/follow_up.rb