dry-system 0.22.0 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
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