dry-system 0.22.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +424 -0
  3. data/LICENSE +1 -1
  4. data/README.md +3 -3
  5. data/dry-system.gemspec +4 -5
  6. data/lib/dry/system/component.rb +10 -5
  7. data/lib/dry/system/component_dir.rb +14 -35
  8. data/lib/dry/system/components.rb +8 -4
  9. data/lib/dry/system/config/component_dir.rb +60 -16
  10. data/lib/dry/system/config/component_dirs.rb +23 -10
  11. data/lib/dry/system/config/namespace.rb +4 -6
  12. data/lib/dry/system/constants.rb +1 -1
  13. data/lib/dry/system/container.rb +275 -192
  14. data/lib/dry/system/errors.rb +73 -53
  15. data/lib/dry/system/identifier.rb +62 -20
  16. data/lib/dry/system/importer.rb +90 -12
  17. data/lib/dry/system/indirect_component.rb +1 -1
  18. data/lib/dry/system/loader.rb +6 -1
  19. data/lib/dry/system/{manual_registrar.rb → manifest_registrar.rb} +9 -6
  20. data/lib/dry/system/plugins/bootsnap.rb +2 -1
  21. data/lib/dry/system/plugins/dependency_graph/strategies.rb +37 -1
  22. data/lib/dry/system/plugins/dependency_graph.rb +26 -20
  23. data/lib/dry/system/plugins/env.rb +2 -1
  24. data/lib/dry/system/plugins/logging.rb +2 -2
  25. data/lib/dry/system/plugins/monitoring.rb +1 -1
  26. data/lib/dry/system/plugins/notifications.rb +1 -1
  27. data/lib/dry/system/plugins/zeitwerk/compat_inflector.rb +22 -0
  28. data/lib/dry/system/plugins/zeitwerk.rb +109 -0
  29. data/lib/dry/system/plugins.rb +7 -4
  30. data/lib/dry/system/provider/source.rb +329 -0
  31. data/lib/dry/system/provider/source_dsl.rb +94 -0
  32. data/lib/dry/system/provider.rb +262 -22
  33. data/lib/dry/system/provider_registrar.rb +276 -0
  34. data/lib/dry/system/provider_source_registry.rb +70 -0
  35. data/lib/dry/system/provider_sources/settings/config.rb +86 -0
  36. data/lib/dry/system/provider_sources/settings/loader.rb +53 -0
  37. data/lib/dry/system/provider_sources/settings.rb +40 -0
  38. data/lib/dry/system/provider_sources.rb +5 -0
  39. data/lib/dry/system/version.rb +1 -1
  40. data/lib/dry/system.rb +44 -12
  41. metadata +23 -37
  42. data/lib/dry/system/booter/component_registry.rb +0 -35
  43. data/lib/dry/system/booter.rb +0 -200
  44. data/lib/dry/system/components/bootable.rb +0 -280
  45. data/lib/dry/system/components/config.rb +0 -35
  46. data/lib/dry/system/lifecycle.rb +0 -135
  47. data/lib/dry/system/provider_registry.rb +0 -27
  48. data/lib/dry/system/settings/file_loader.rb +0 -30
  49. data/lib/dry/system/settings/file_parser.rb +0 -51
  50. data/lib/dry/system/settings.rb +0 -64
  51. data/lib/dry/system/system_components/settings.rb +0 -11
@@ -8,7 +8,7 @@ module Dry
8
8
  module Logging
9
9
  # @api private
10
10
  def self.extended(system)
11
- system.before(:configure) do
11
+ system.instance_eval do
12
12
  setting :logger, reader: true
13
13
 
14
14
  setting :log_dir, default: "log"
@@ -40,7 +40,7 @@ module Dry
40
40
  elsif config.logger
41
41
  register(:logger, config.logger)
42
42
  else
43
- config.logger = logger = config.logger_class.new(log_file_path)
43
+ config.logger = config.logger_class.new(log_file_path)
44
44
  config.logger.level = log_level
45
45
 
46
46
  register(:logger, config.logger)
@@ -21,7 +21,7 @@ module Dry
21
21
 
22
22
  # @api private
23
23
  def self.dependencies
24
- {'dry-events': "dry/events/publisher"}
24
+ {"dry-events": "dry/events/publisher"}
25
25
  end
26
26
 
27
27
  # @api private
@@ -12,7 +12,7 @@ module Dry
12
12
 
13
13
  # @api private
14
14
  def self.dependencies
15
- {'dry-monitor': "dry/monitor/notifications"}
15
+ {"dry-monitor": "dry/monitor/notifications"}
16
16
  end
17
17
 
18
18
  # @api private
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module System
5
+ module Plugins
6
+ class Zeitwerk < Module
7
+ # @api private
8
+ class CompatInflector
9
+ attr_reader :config
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ end
14
+
15
+ def camelize(string, _)
16
+ config.inflector.camelize(string)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/system/constants"
4
+
5
+ module Dry
6
+ module System
7
+ module Plugins
8
+ # @api private
9
+ class Zeitwerk < Module
10
+ # @api private
11
+ def self.dependencies
12
+ [
13
+ "dry/system/loader/autoloading",
14
+ "dry/system/plugins/zeitwerk/compat_inflector",
15
+ {"zeitwerk" => "zeitwerk"}
16
+ ]
17
+ end
18
+
19
+ # @api private
20
+ attr_reader :loader, :run_setup, :eager_load, :debug
21
+
22
+ # @api private
23
+ def initialize(loader: nil, run_setup: true, eager_load: nil, debug: false)
24
+ @loader = loader || ::Zeitwerk::Loader.new
25
+ @run_setup = run_setup
26
+ @eager_load = eager_load
27
+ @debug = debug
28
+ super()
29
+ end
30
+
31
+ # @api private
32
+ def extended(system)
33
+ system.setting :autoloader, reader: true
34
+
35
+ system.config.autoloader = loader
36
+ system.config.component_dirs.loader = Dry::System::Loader::Autoloading
37
+ system.config.component_dirs.add_to_load_path = false
38
+
39
+ system.after(:configure, &method(:setup_autoloader))
40
+
41
+ super
42
+ end
43
+
44
+ private
45
+
46
+ def setup_autoloader(system)
47
+ configure_loader(system.autoloader, system)
48
+
49
+ push_component_dirs_to_loader(system, system.autoloader)
50
+
51
+ system.autoloader.setup if run_setup
52
+
53
+ system.after(:finalize) { system.autoloader.eager_load } if eager_load?(system)
54
+
55
+ system
56
+ end
57
+
58
+ # Build a zeitwerk loader with the configured component directories
59
+ #
60
+ # @return [Zeitwerk::Loader]
61
+ def configure_loader(loader, system)
62
+ loader.tag = system.config.name || system.name unless loader.tag
63
+ loader.inflector = CompatInflector.new(system.config)
64
+ loader.logger = method(:puts) if debug
65
+ end
66
+
67
+ # Add component dirs to the zeitwerk loader
68
+ #
69
+ # @return [Zeitwerk::Loader]
70
+ def push_component_dirs_to_loader(system, loader)
71
+ system.config.component_dirs.each do |dir|
72
+ dir.namespaces.each do |ns|
73
+ loader.push_dir(
74
+ system.root.join(dir.path, ns.path.to_s),
75
+ namespace: module_for_namespace(ns, system.config.inflector)
76
+ )
77
+ end
78
+ end
79
+
80
+ loader
81
+ end
82
+
83
+ def module_for_namespace(namespace, inflector)
84
+ return Object unless namespace.const
85
+
86
+ begin
87
+ inflector.constantize(inflector.camelize(namespace.const))
88
+ rescue NameError
89
+ namespace.const.split(PATH_SEPARATOR).reduce(Object) { |parent_mod, mod_path|
90
+ get_or_define_module(parent_mod, inflector.camelize(mod_path))
91
+ }
92
+ end
93
+ end
94
+
95
+ def get_or_define_module(parent_mod, name)
96
+ parent_mod.const_get(name)
97
+ rescue NameError
98
+ parent_mod.const_set(name, Module.new)
99
+ end
100
+
101
+ def eager_load?(system)
102
+ return eager_load unless eager_load.nil?
103
+
104
+ system.config.respond_to?(:env) && system.config.env == :production
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -21,8 +21,8 @@ module Dry
21
21
  end
22
22
 
23
23
  # @api private
24
- def apply_to(system, options)
25
- system.extend(stateful? ? mod.new(options) : mod)
24
+ def apply_to(system, **options)
25
+ system.extend(stateful? ? mod.new(**options) : mod)
26
26
  system.instance_eval(&block) if block
27
27
  system
28
28
  end
@@ -90,13 +90,13 @@ module Dry
90
90
  # @return [self]
91
91
  #
92
92
  # @api public
93
- def use(name, options = {})
93
+ def use(name, **options)
94
94
  return self if enabled_plugins.include?(name)
95
95
 
96
96
  raise PluginNotFoundError, name unless (plugin = Plugins.registry[name])
97
97
 
98
98
  plugin.load_dependencies
99
- plugin.apply_to(self, options)
99
+ plugin.apply_to(self, **options)
100
100
 
101
101
  enabled_plugins << name
102
102
 
@@ -131,6 +131,9 @@ module Dry
131
131
 
132
132
  require "dry/system/plugins/dependency_graph"
133
133
  register(:dependency_graph, Plugins::DependencyGraph)
134
+
135
+ require "dry/system/plugins/zeitwerk"
136
+ register(:zeitwerk, Plugins::Zeitwerk)
134
137
  end
135
138
  end
136
139
  end
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require "dry/core/class_attributes"
5
+ require "dry/core/deprecations"
6
+ require_relative "source_dsl"
7
+
8
+ module Dry
9
+ module System
10
+ class Provider
11
+ # A provider's source provides the specific behavior for a given provider to serve
12
+ # its purpose.
13
+ #
14
+ # Sources should be subclasses of `Dry::System::Source::Provider`, with instance
15
+ # methods for each lifecycle step providing their behavior: {#prepare}, {#start},
16
+ # and {#stop}.
17
+ #
18
+ # Inside each of these methods, you should create and configure your provider's
19
+ # objects as required, and then {#register} them with the {#provider_container}.
20
+ # When the provider's lifecycle steps are run (via {Dry::System::Provider}), these
21
+ # registered components will be merged into the target container.
22
+ #
23
+ # You can prepare a provider's source in two ways:
24
+ #
25
+ # 1. Passing a bock when registering the provider, which is then evaluated via
26
+ # {Dry::System::Provider::SourceDSL} to prepare the provider subclass. This
27
+ # approach is easiest for simple providers.
28
+ # 2. Manually creare your own subclass of {Dry::System::Provider} and implement your
29
+ # own instance methods for the lifecycle steps (you should not implement your own
30
+ # `#initialize`). This approach may be useful for more complex providers.
31
+ #
32
+ # @see Dry::System::Container.register_provider
33
+ # @see Dry::System.register_provider_source
34
+ # @see Dry::System::Source::ProviderDSL
35
+ #
36
+ # @api public
37
+ class Source
38
+ class << self
39
+ # Returns a new Dry::System::Provider::Source subclass with its behavior supplied by the
40
+ # given block, which is evaluated using Dry::System::Provider::SourceDSL.
41
+ #
42
+ # @see Dry::System::Provider::SourceDSL
43
+ #
44
+ # @api private
45
+ def for(name:, group: nil, target_container:, &block) # rubocop:disable Style/KeywordParametersOrder
46
+ Class.new(self) { |klass|
47
+ klass.source_name name
48
+ klass.source_group group
49
+ SourceDSL.evaluate(klass, target_container, &block) if block
50
+ }
51
+ end
52
+
53
+ def inherited(subclass)
54
+ super
55
+
56
+ # Include Dry::Configurable only when first subclassing to ensure that
57
+ # distinct Source subclasses do not share settings.
58
+ #
59
+ # The superclass check here allows deeper Source class hierarchies to be
60
+ # created without running into a Dry::Configurable::AlreadyIncluded error.
61
+ if subclass.superclass == Source
62
+ subclass.include Dry::Configurable
63
+ end
64
+ end
65
+
66
+ # @api private
67
+ def name
68
+ source_str = source_name
69
+ source_str = "#{source_group}->#{source_str}" if source_group
70
+
71
+ "Dry::System::Provider::Source[#{source_str}]"
72
+ end
73
+
74
+ # @api private
75
+ def to_s
76
+ "#<#{name}>"
77
+ end
78
+
79
+ # @api private
80
+ def inspect
81
+ to_s
82
+ end
83
+ end
84
+
85
+ CALLBACK_MAP = Hash.new { |h, k| h[k] = [] }.freeze
86
+
87
+ extend Dry::Core::ClassAttributes
88
+
89
+ defines :source_name, :source_group
90
+
91
+ # @api private
92
+ attr_reader :callbacks
93
+
94
+ # Returns the provider's own container for the provider.
95
+ #
96
+ # This container is namespaced based on the provider's `namespace:` configuration.
97
+ #
98
+ # Registered components in this container will be merged into the target container
99
+ # after the `prepare` and `start` lifecycle steps.
100
+ #
101
+ # @return [Dry::Container]
102
+ #
103
+ # @see #target_container
104
+ # @see Dry::System::Provider
105
+ #
106
+ # @api public
107
+ attr_reader :provider_container
108
+ alias_method :container, :provider_container
109
+
110
+ # Returns the target container for the provider.
111
+ #
112
+ # This is the container with which the provider is registered (via
113
+ # {Dry::System::Container.register_provider}).
114
+ #
115
+ # Registered components from the provider's container will be merged into this
116
+ # container after the `prepare` and `start` lifecycle steps.
117
+ #
118
+ # @return [Dry::System::Container]
119
+ #
120
+ # @see #provider_container
121
+ # @see Dry::System::Provider
122
+ #
123
+ # @api public
124
+ attr_reader :target_container
125
+ alias_method :target, :target_container
126
+
127
+ # @api private
128
+ def initialize(provider_container:, target_container:, &block)
129
+ super()
130
+ @callbacks = {before: CALLBACK_MAP.dup, after: CALLBACK_MAP.dup}
131
+ @provider_container = provider_container
132
+ @target_container = target_container
133
+ instance_exec(&block) if block
134
+ end
135
+
136
+ # Returns a string containing a human-readable representation of the provider.
137
+ #
138
+ # @return [String]
139
+ #
140
+ # @api private
141
+ def inspect
142
+ ivars = instance_variables.map { |ivar|
143
+ "#{ivar}=#{instance_variable_get(ivar).inspect}"
144
+ }.join(" ")
145
+
146
+ "#<#{self.class.name} #{ivars}>"
147
+ end
148
+
149
+ # Runs the behavior for the "prepare" lifecycle step.
150
+ #
151
+ # This should be implemented by your source subclass or specified by
152
+ # `SourceDSL#prepare` when registering a provider using a block.
153
+ #
154
+ # @return [void]
155
+ #
156
+ # @see SourceDSL#prepare
157
+ #
158
+ # @api public
159
+ def prepare; end
160
+
161
+ # Runs the behavior for the "start" lifecycle step.
162
+ #
163
+ # This should be implemented by your source subclass or specified by
164
+ # `SourceDSL#start` when registering a provider using a block.
165
+ #
166
+ # You can presume that {#prepare} has already run by the time this method is
167
+ # called.
168
+ #
169
+ # @return [void]
170
+ #
171
+ # @see SourceDSL#start
172
+ #
173
+ # @api public
174
+ def start; end
175
+
176
+ # Runs the behavior for the "stop" lifecycle step.
177
+ #
178
+ # This should be implemented by your source subclass or specified by
179
+ # `SourceDSL#stop` when registering a provider using a block.
180
+ #
181
+ # You can presume that {#prepare} and {#start} have already run by the time this
182
+ # method is called.
183
+ #
184
+ # @return [void]
185
+ #
186
+ # @see SourceDSL#stop
187
+ #
188
+ # @api public
189
+ def stop; end
190
+
191
+ def use(*provider_names)
192
+ Dry::Core::Deprecations.announce(
193
+ "Dry::System::Provider#use",
194
+ "Use `target_container.start` instead, e.g. `target_container.start(:another_provider)`", # rubocop:disable Layout/LineLength
195
+ tag: "dry-system",
196
+ uplevel: 1
197
+ )
198
+
199
+ provider_names.each do |name|
200
+ target_container.start(name)
201
+ end
202
+
203
+ self
204
+ end
205
+
206
+ # Registers a "before" callback for the given lifecycle step.
207
+ #
208
+ # The given block will be run before the lifecycle step method is run. The block
209
+ # will be evaluated in the context of the instance of this source.
210
+ #
211
+ # @param step_name [Symbol]
212
+ # @param block [Proc] the callback block
213
+ #
214
+ # @return [self]
215
+ #
216
+ # @see #after
217
+ #
218
+ # @api public
219
+ def before(step_name, &block)
220
+ if step_name.to_sym == :init
221
+ Dry::Core::Deprecations.announce(
222
+ "Dry::System::Provider before(:init) callback",
223
+ "Use `before(:prepare)` callback instead",
224
+ tag: "dry-system",
225
+ uplevel: 1
226
+ )
227
+
228
+ step_name = :prepare
229
+ end
230
+
231
+ callbacks[:before][step_name] << block
232
+ self
233
+ end
234
+
235
+ # Registers an "after" callback for the given lifecycle step.
236
+ #
237
+ # The given block will be run after the lifecycle step method is run. The block
238
+ # will be evaluated in the context of the instance of this source.
239
+ #
240
+ # @param step_name [Symbol]
241
+ # @param block [Proc] the callback block
242
+ #
243
+ # @return [self]
244
+ #
245
+ # @see #before
246
+ #
247
+ # @api public
248
+ def after(step_name, &block)
249
+ if step_name.to_sym == :init
250
+ Dry::Core::Deprecations.announce(
251
+ "Dry::System::Provider after(:init) callback",
252
+ "Use `after(:prepare)` callback instead",
253
+ tag: "dry-system",
254
+ uplevel: 1
255
+ )
256
+
257
+ step_name = :prepare
258
+ end
259
+
260
+ callbacks[:after][step_name] << block
261
+ self
262
+ end
263
+
264
+ # @api private
265
+ def run_callback(hook, step)
266
+ callbacks[hook][step].each do |callback|
267
+ if callback.parameters.any?
268
+ Dry::Core::Deprecations.announce(
269
+ "Dry::System::Provider::Source.before and .after callbacks with single block parameter", # rubocop:disable Layout/LineLength
270
+ "Use `provider_container` (or `container` for short) inside your block instead",
271
+ tag: "dry-system",
272
+ uplevel: 1
273
+ )
274
+
275
+ instance_exec(provider_container, &callback)
276
+ else
277
+ instance_eval(&callback)
278
+ end
279
+ end
280
+ end
281
+
282
+ private
283
+
284
+ # Registers a component in the provider container.
285
+ #
286
+ # When the provider's lifecycle steps are run (via {Dry::System::Provider}), these
287
+ # registered components will be merged into the target container.
288
+ #
289
+ # @return [Dry::Container] the provider container
290
+ #
291
+ # @api public
292
+ def register(...)
293
+ provider_container.register(...)
294
+ end
295
+
296
+ # Resolves a previously registered component from the provider container.
297
+ #
298
+ # @param key [String] the key for the component to resolve
299
+ #
300
+ # @return [Object] the previously registered component
301
+ #
302
+ # @api public
303
+ def resolve(key)
304
+ provider_container.resolve(key)
305
+ end
306
+
307
+ # @api private
308
+ def run_step_block(step_name)
309
+ step_block = self.class.step_blocks[step_name]
310
+ instance_eval(&step_block) if step_block
311
+ end
312
+
313
+ # @api private
314
+ def method_missing(name, *args, &block)
315
+ if container.key?(name)
316
+ container[name]
317
+ else
318
+ super
319
+ end
320
+ end
321
+
322
+ # @api private
323
+ def respond_to_missing?(name, include_all = false)
324
+ container.key?(name) || super
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/deprecations"
4
+
5
+ module Dry
6
+ module System
7
+ class Provider
8
+ # Configures a Dry::System::Provider::Source subclass using a DSL that makes it
9
+ # nicer to define source behaviour via a single block.
10
+ #
11
+ # @see Dry::System::Container.register_provider
12
+ #
13
+ # @api private
14
+ class SourceDSL
15
+ extend Dry::Core::Deprecations["Dry::System::Provider::SourceDSL"]
16
+
17
+ def self.evaluate(source_class, target_container, &block)
18
+ if block.parameters.any?
19
+ Dry::Core::Deprecations.announce(
20
+ "Dry::System.register_provider with single block parameter",
21
+ "Use `target_container` (or `target` for short) inside your block instead",
22
+ tag: "dry-system"
23
+ )
24
+ new(source_class).instance_exec(target_container, &block)
25
+ else
26
+ new(source_class).instance_eval(&block)
27
+ end
28
+ end
29
+
30
+ attr_reader :source_class
31
+
32
+ def initialize(source_class)
33
+ @source_class = source_class
34
+ end
35
+
36
+ def setting(...)
37
+ source_class.setting(...)
38
+ end
39
+
40
+ # rubocop:disable Layout/LineLength
41
+
42
+ def settings(&block)
43
+ Dry::Core::Deprecations.announce(
44
+ "Dry::System.register_provider with nested settings block",
45
+ "Use individual top-level `setting` declarations instead (see dry-configurable docs for details)",
46
+ tag: "dry-system",
47
+ uplevel: 1
48
+ )
49
+
50
+ DeprecatedSettingsDSL.new(self).instance_eval(&block)
51
+ end
52
+
53
+ # rubocop:enable Layout/LineLength
54
+
55
+ class DeprecatedSettingsDSL
56
+ def initialize(base_dsl)
57
+ @base_dsl = base_dsl
58
+ end
59
+
60
+ def key(name, type)
61
+ @base_dsl.setting(name, constructor: type)
62
+ end
63
+ end
64
+
65
+ def prepare(&block)
66
+ source_class.define_method(:prepare, &block)
67
+ end
68
+ deprecate :init, :prepare
69
+
70
+ def start(&block)
71
+ source_class.define_method(:start, &block)
72
+ end
73
+
74
+ def stop(&block)
75
+ source_class.define_method(:stop, &block)
76
+ end
77
+
78
+ private
79
+
80
+ def method_missing(name, *args, &block)
81
+ if source_class.respond_to?(name)
82
+ source_class.public_send(name, *args, &block)
83
+ else
84
+ super
85
+ end
86
+ end
87
+
88
+ def respond_to_missing?(name, include_all = false)
89
+ source_class.respond_to?(name, include_all) || super
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end