thecore_backend_commons 3.4.1 → 3.5.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: 47f357e5a5b355623f7eaa5443e8ba8cfb5566311f6c5f866cbda9740a48a044
4
- data.tar.gz: 52725ea116ba712bc9c68b4c3b757c2a64a7b1270b2efd1f56cd16c503d21bbe
3
+ metadata.gz: 5e57106019f11e4ac721eaee6a0d974f3c640e32bd6d031a462d3356a20f1091
4
+ data.tar.gz: 2ad214f8fe011dafc87a3323d22b8d5f0e69bf0cc8ba61766385f219e14e41b6
5
5
  SHA512:
6
- metadata.gz: fbb02e11840fdd2608ce289285424c5792e50ce09ef207c08a4aafd943761e01f2ed72e50bbe855c0fb8d35e61bb6e24c0ee170d335d26f19f8645b00b918d1e
7
- data.tar.gz: cfbcd1ecdfb625c4eaa3119a64394afb73806f5c764b3c7982a44432aee24143de465ec268a60ed927714abf0702190e751401377d703dbe4226cc4bd49ac2b8
6
+ metadata.gz: fec2dcd9fe2769f2853a34ec1f8b887a8c9e6f9766e911821ef741df81cfa58399da343745dc589b384b05746ed599f6e61fd3d8ba6d48f928fe2156fcd3b527
7
+ data.tar.gz: f46bf3f1ee9ff55bbabd17b7e5f3614d1a9d04ad7a58113d014f5030c2ed1f1f39d39d11f2a0556876d3540356f9a2565a9d75d5cb0c5fd882343cd8f5c9f6ed
data/README.md CHANGED
@@ -126,6 +126,39 @@ PushNotificationChannel.broadcast_to(subscriber, message)
126
126
 
127
127
  ---
128
128
 
129
+ ## `DefaultModuleRegistry` — shared `ApplicationRecord.inherited` hook
130
+
131
+ Other gems in the ecosystem (`model_driven_api`, `thecore_ui_rails_admin`, ...) need to give
132
+ *every* model some default behavior — a default API serialization shape, a default RailsAdmin
133
+ navigation entry — without requiring a generated per-model concern file for the
134
+ no-customization case. Rather than each gem independently overriding
135
+ `ApplicationRecord.inherited` (and risking one override clobbering another), they all register
136
+ into this one shared registry:
137
+
138
+ ```ruby
139
+ ThecoreBackendCommons::DefaultModuleRegistry.register(
140
+ MyDefaultModule, # normally an ActiveSupport::Concern with an `included do ... end` block
141
+ applies_to: ->(klass) { true } # optional; defaults to every ApplicationRecord subclass
142
+ )
143
+ ```
144
+
145
+ Every registered module is `include`d, in registration order, into every `ApplicationRecord`
146
+ subclass for which `applies_to` returns true — at the moment the subclass is *defined*, not via
147
+ a post-boot scan (which would miss classes not yet autoloaded in development). This is wired up
148
+ by installing an `ApplicationRecord.inherited` hook from `config.to_prepare` (see
149
+ `config/initializers/default_module_registry.rb`), so it's already in place before Rails eager
150
+ loads every model in production.
151
+
152
+ Consumers today: `model_driven_api`'s `ModelDrivenApiDefaultJsonAttrs` (default `json_attrs`)
153
+ and `thecore_ui_rails_admin`'s `ThecoreUiRailsAdminDefaultNavigationConcern` (default
154
+ `navigation_label`/`navigation_icon`) — see their own READMEs for what each one does. A model
155
+ that still needs custom behavior keeps (or adds) an explicit `Api::ModelName`/
156
+ `RailsAdmin::ModelName` concern exactly as before; it simply runs *after* the default and
157
+ overrides it. Full mechanics (idempotency, abstract/STI exclusion, `to_prepare` vs
158
+ `after_initialize` timing) are documented in this gem's `CLAUDE.md`.
159
+
160
+ ---
161
+
129
162
  ## Invio email e configurazione SMTP
130
163
 
131
164
  ### Configurazione
@@ -0,0 +1,10 @@
1
+ Rails.application.configure do
2
+ # Installed from `to_prepare` (not `after_initialize`) so the hook is in
3
+ # place *before* `eager_load!` runs -- see
4
+ # `ThecoreBackendCommons::DefaultModuleRegistry.install!` for why that
5
+ # ordering matters. `to_prepare` also re-runs on every class reload in
6
+ # development, so `install!` is idempotent.
7
+ config.to_prepare do
8
+ ThecoreBackendCommons::DefaultModuleRegistry.install!(ApplicationRecord)
9
+ end
10
+ end
@@ -0,0 +1,133 @@
1
+ module ThecoreBackendCommons
2
+ # Shared registry other gems use to register "default" modules that get
3
+ # `include`d automatically into every `ApplicationRecord` subclass, as it
4
+ # is defined -- not via a post-boot scan of `ApplicationRecord.subclasses`.
5
+ #
6
+ # This exists so multiple gems (`model_driven_api`, `thecore_ui_rails_admin`,
7
+ # ...) can each contribute default model behavior (API serialization shape,
8
+ # RailsAdmin config, ...) through ONE shared `ApplicationRecord.inherited`
9
+ # hook instead of each gem independently overriding `inherited` itself.
10
+ # See ADR 0001/0002 (`vendor/external/thecore/docs/adr/` in the host app).
11
+ #
12
+ # == Usage
13
+ #
14
+ # ThecoreBackendCommons::DefaultModuleRegistry.register(
15
+ # MyDefaultModule,
16
+ # applies_to: ->(klass) { klass.table_exists? rescue false }
17
+ # )
18
+ #
19
+ # `applies_to` defaults to "every subclass" (`->(_klass) { true }`) when
20
+ # omitted. Registered modules are `include`d, in registration order, into
21
+ # every `ApplicationRecord` subclass for which `applies_to` returns true,
22
+ # at the moment the subclass is defined -- see `InheritedHook` below.
23
+ #
24
+ # `MyDefaultModule` should normally be an `ActiveSupport::Concern` with an
25
+ # `included do ... end` block, so its default methods/callbacks/DSL calls
26
+ # land as *own* behavior on each model class (see ADR 0001's consequences
27
+ # section -- `model_driven_api`'s introspection depends on this).
28
+ #
29
+ # == Idempotency
30
+ #
31
+ # - Calling `.register` twice with the *same* module object is a no-op the
32
+ # second time -- it will not be applied twice to any subclass.
33
+ # - `.apply_to` never re-includes a module a class already has in its
34
+ # ancestor chain (e.g. an STI subclass that already inherited it from its
35
+ # base class), so STI subclasses are never double-included.
36
+ # - `.apply_to` never applies anything to an abstract class
37
+ # (`klass.abstract_class?`) -- most importantly `ApplicationRecord`
38
+ # itself (`primary_abstract_class`), which never receives default
39
+ # modules through this mechanism since `ApplicationRecord.inherited` only
40
+ # fires for classes that inherit *from* `ApplicationRecord`, never for
41
+ # `ApplicationRecord`'s own definition.
42
+ module DefaultModuleRegistry
43
+ Entry = Struct.new(:mod, :applies_to)
44
+ private_constant :Entry
45
+
46
+ # Prepended onto `ApplicationRecord.singleton_class` (see `.install!`)
47
+ # so that every subsequent subclass definition applies the registry.
48
+ #
49
+ # Calls `super` first so it composes correctly with ActiveRecord's own
50
+ # `inherited` (which sets up `base_class`, STI bookkeeping, ...) and with
51
+ # any other gem's pre-existing `inherited` override further up the
52
+ # singleton-class ancestor chain.
53
+ module InheritedHook
54
+ def inherited(subclass)
55
+ super
56
+ ThecoreBackendCommons::DefaultModuleRegistry.apply_to(subclass)
57
+ end
58
+ end
59
+
60
+ class << self
61
+ # Registers +mod+ so it gets `include`d into every matching
62
+ # `ApplicationRecord` subclass defined from now on. Returns +mod+.
63
+ #
64
+ # +applies_to+ is a 1-arity callable invoked with the candidate class;
65
+ # only classes for which it returns truthy receive +mod+.
66
+ #
67
+ # Registering the same module object again is a no-op -- the original
68
+ # registration (and its original `applies_to`) is kept.
69
+ def register(mod, applies_to: ->(_klass) { true })
70
+ entries << Entry.new(mod, applies_to) unless registered?(mod)
71
+ mod
72
+ end
73
+
74
+ # True if +mod+ has already been registered.
75
+ def registered?(mod)
76
+ entries.any? { |entry| entry.mod.equal?(mod) }
77
+ end
78
+
79
+ # Applies every registered module whose `applies_to` predicate matches
80
+ # +klass+, in registration order. No-op for abstract classes. Never
81
+ # re-includes a module +klass+ already has in its ancestor chain.
82
+ #
83
+ # Called automatically by `InheritedHook` -- only call directly from
84
+ # application/test code that needs to (re-)apply defaults to a class
85
+ # defined before the registry/hook was set up.
86
+ def apply_to(klass)
87
+ return klass if klass.abstract_class?
88
+
89
+ entries.each do |entry|
90
+ next unless entry.applies_to.call(klass)
91
+ next if klass.include?(entry.mod)
92
+
93
+ klass.include(entry.mod)
94
+ end
95
+
96
+ klass
97
+ end
98
+
99
+ # Installs `InheritedHook` onto +base_class+'s singleton class so every
100
+ # subsequently-defined subclass triggers `.apply_to`. Idempotent --
101
+ # safe to call on every `to_prepare` cycle in development.
102
+ #
103
+ # Must run before any subclass of +base_class+ is defined (i.e. before
104
+ # eager loading), which is why the host initializer installs it from
105
+ # `config.to_prepare` rather than `config.after_initialize` -- Rails
106
+ # runs `to_prepare` callbacks *before* `eager_load!`, while
107
+ # `after_initialize` runs *after* it (see
108
+ # `Rails::Application::Finisher`). Installing from `after_initialize`
109
+ # would miss every subclass already loaded by eager loading in
110
+ # production.
111
+ def install!(base_class)
112
+ return base_class if base_class.singleton_class.ancestors.include?(InheritedHook)
113
+
114
+ base_class.singleton_class.prepend(InheritedHook)
115
+ base_class
116
+ end
117
+
118
+ # Test helper: clears all registrations. Not intended for production
119
+ # use -- does not uninstall `InheritedHook` from any class it was
120
+ # already installed on, and does not un-include modules already
121
+ # included into existing classes.
122
+ def reset!
123
+ @entries = []
124
+ end
125
+
126
+ private
127
+
128
+ def entries
129
+ @entries ||= []
130
+ end
131
+ end
132
+ end
133
+ end
@@ -1,3 +1,3 @@
1
1
  module ThecoreBackendCommons
2
- VERSION = "3.4.1".freeze
2
+ VERSION = "3.5.0".freeze
3
3
  end
@@ -17,6 +17,7 @@ require "thecore_backend_commons/engine"
17
17
  require "thecore_backend_commons/smtp_config"
18
18
  require "thecore_backend_commons/smtp_tester"
19
19
  require "thecore_backend_commons/push_notification_service"
20
+ require "thecore_backend_commons/default_module_registry"
20
21
 
21
22
  module ThecoreBackendCommons
22
23
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thecore_backend_commons
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.1
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriele Tassoni
@@ -216,6 +216,7 @@ files:
216
216
  - config/initializers/concern_integer.rb
217
217
  - config/initializers/concern_string.rb
218
218
  - config/initializers/concern_user.rb
219
+ - config/initializers/default_module_registry.rb
219
220
  - config/initializers/extension_nil.rb
220
221
  - config/initializers/extension_string.rb
221
222
  - config/locales/en.devise.custom.yml
@@ -229,6 +230,7 @@ files:
229
230
  - db/seeds.rb
230
231
  - lib/tasks/thecore_backend_commons_tasks.rake
231
232
  - lib/thecore_backend_commons.rb
233
+ - lib/thecore_backend_commons/default_module_registry.rb
232
234
  - lib/thecore_backend_commons/engine.rb
233
235
  - lib/thecore_backend_commons/push_notification_service.rb
234
236
  - lib/thecore_backend_commons/smtp_config.rb