dry-system 0.22.0 → 0.23.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 +400 -0
  3. data/LICENSE +1 -1
  4. data/README.md +2 -2
  5. data/dry-system.gemspec +2 -2
  6. data/lib/dry/system/component.rb +2 -3
  7. data/lib/dry/system/component_dir.rb +8 -34
  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 +264 -182
  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 +83 -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} +8 -5
  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 +324 -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 +18 -18
  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
@@ -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,324 @@
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
+ # FIXME: This shouldn't _need_ to be in an inherited hook but right now it's
57
+ # the only way to prevent individual Source instances from sharing settings
58
+ subclass.include Dry::Configurable
59
+ end
60
+
61
+ # @api private
62
+ def name
63
+ source_str = source_name
64
+ source_str = "#{source_group}->#{source_str}" if source_group
65
+
66
+ "Dry::System::Provider::Source[#{source_str}]"
67
+ end
68
+
69
+ # @api private
70
+ def to_s
71
+ "#<#{name}>"
72
+ end
73
+
74
+ # @api private
75
+ def inspect
76
+ to_s
77
+ end
78
+ end
79
+
80
+ CALLBACK_MAP = Hash.new { |h, k| h[k] = [] }.freeze
81
+
82
+ extend Dry::Core::ClassAttributes
83
+
84
+ defines :source_name, :source_group
85
+
86
+ # @api private
87
+ attr_reader :callbacks
88
+
89
+ # Returns the provider's own container for the provider.
90
+ #
91
+ # This container is namespaced based on the provider's `namespace:` configuration.
92
+ #
93
+ # Registered components in this container will be merged into the target container
94
+ # after the `prepare` and `start` lifecycle steps.
95
+ #
96
+ # @return [Dry::Container]
97
+ #
98
+ # @see #target_container
99
+ # @see Dry::System::Provider
100
+ #
101
+ # @api public
102
+ attr_reader :provider_container
103
+ alias_method :container, :provider_container
104
+
105
+ # Returns the target container for the provider.
106
+ #
107
+ # This is the container with which the provider is registered (via
108
+ # {Dry::System::Container.register_provider}).
109
+ #
110
+ # Registered components from the provider's container will be merged into this
111
+ # container after the `prepare` and `start` lifecycle steps.
112
+ #
113
+ # @return [Dry::System::Container]
114
+ #
115
+ # @see #provider_container
116
+ # @see Dry::System::Provider
117
+ #
118
+ # @api public
119
+ attr_reader :target_container
120
+ alias_method :target, :target_container
121
+
122
+ # @api private
123
+ def initialize(provider_container:, target_container:, &block)
124
+ super()
125
+ @callbacks = {before: CALLBACK_MAP.dup, after: CALLBACK_MAP.dup}
126
+ @provider_container = provider_container
127
+ @target_container = target_container
128
+ instance_exec(&block) if block
129
+ end
130
+
131
+ # Returns a string containing a human-readable representation of the provider.
132
+ #
133
+ # @return [String]
134
+ #
135
+ # @api private
136
+ def inspect
137
+ ivars = instance_variables.map { |ivar|
138
+ "#{ivar}=#{instance_variable_get(ivar).inspect}"
139
+ }.join(" ")
140
+
141
+ "#<#{self.class.name} #{ivars}>"
142
+ end
143
+
144
+ # Runs the behavior for the "prepare" lifecycle step.
145
+ #
146
+ # This should be implemented by your source subclass or specified by
147
+ # `SourceDSL#prepare` when registering a provider using a block.
148
+ #
149
+ # @return [void]
150
+ #
151
+ # @see SourceDSL#prepare
152
+ #
153
+ # @api public
154
+ def prepare; end
155
+
156
+ # Runs the behavior for the "start" lifecycle step.
157
+ #
158
+ # This should be implemented by your source subclass or specified by
159
+ # `SourceDSL#start` when registering a provider using a block.
160
+ #
161
+ # You can presume that {#prepare} has already run by the time this method is
162
+ # called.
163
+ #
164
+ # @return [void]
165
+ #
166
+ # @see SourceDSL#start
167
+ #
168
+ # @api public
169
+ def start; end
170
+
171
+ # Runs the behavior for the "stop" lifecycle step.
172
+ #
173
+ # This should be implemented by your source subclass or specified by
174
+ # `SourceDSL#stop` when registering a provider using a block.
175
+ #
176
+ # You can presume that {#prepare} and {#start} have already run by the time this
177
+ # method is called.
178
+ #
179
+ # @return [void]
180
+ #
181
+ # @see SourceDSL#stop
182
+ #
183
+ # @api public
184
+ def stop; end
185
+
186
+ def use(*provider_names)
187
+ Dry::Core::Deprecations.announce(
188
+ "Dry::System::Provider#use",
189
+ "Use `target_container.start` instead, e.g. `target_container.start(:another_provider)`", # rubocop:disable Layout/LineLength
190
+ tag: "dry-system",
191
+ uplevel: 1
192
+ )
193
+
194
+ provider_names.each do |name|
195
+ target_container.start(name)
196
+ end
197
+
198
+ self
199
+ end
200
+
201
+ # Registers a "before" callback for the given lifecycle step.
202
+ #
203
+ # The given block will be run before the lifecycle step method is run. The block
204
+ # will be evaluated in the context of the instance of this source.
205
+ #
206
+ # @param step_name [Symbol]
207
+ # @param block [Proc] the callback block
208
+ #
209
+ # @return [self]
210
+ #
211
+ # @see #after
212
+ #
213
+ # @api public
214
+ def before(step_name, &block)
215
+ if step_name.to_sym == :init
216
+ Dry::Core::Deprecations.announce(
217
+ "Dry::System::Provider before(:init) callback",
218
+ "Use `before(:prepare)` callback instead",
219
+ tag: "dry-system",
220
+ uplevel: 1
221
+ )
222
+
223
+ step_name = :prepare
224
+ end
225
+
226
+ callbacks[:before][step_name] << block
227
+ self
228
+ end
229
+
230
+ # Registers an "after" callback for the given lifecycle step.
231
+ #
232
+ # The given block will be run after the lifecycle step method is run. The block
233
+ # will be evaluated in the context of the instance of this source.
234
+ #
235
+ # @param step_name [Symbol]
236
+ # @param block [Proc] the callback block
237
+ #
238
+ # @return [self]
239
+ #
240
+ # @see #before
241
+ #
242
+ # @api public
243
+ def after(step_name, &block)
244
+ if step_name.to_sym == :init
245
+ Dry::Core::Deprecations.announce(
246
+ "Dry::System::Provider after(:init) callback",
247
+ "Use `after(:prepare)` callback instead",
248
+ tag: "dry-system",
249
+ uplevel: 1
250
+ )
251
+
252
+ step_name = :prepare
253
+ end
254
+
255
+ callbacks[:after][step_name] << block
256
+ self
257
+ end
258
+
259
+ # @api private
260
+ def run_callback(hook, step)
261
+ callbacks[hook][step].each do |callback|
262
+ if callback.parameters.any?
263
+ Dry::Core::Deprecations.announce(
264
+ "Dry::System::Provider::Source.before and .after callbacks with single block parameter", # rubocop:disable Layout/LineLength
265
+ "Use `provider_container` (or `container` for short) inside your block instead",
266
+ tag: "dry-system",
267
+ uplevel: 1
268
+ )
269
+
270
+ instance_exec(provider_container, &callback)
271
+ else
272
+ instance_eval(&callback)
273
+ end
274
+ end
275
+ end
276
+
277
+ private
278
+
279
+ # Registers a component in the provider container.
280
+ #
281
+ # When the provider's lifecycle steps are run (via {Dry::System::Provider}), these
282
+ # registered components will be merged into the target container.
283
+ #
284
+ # @return [Dry::Container] the provider container
285
+ #
286
+ # @api public
287
+ def register(...)
288
+ provider_container.register(...)
289
+ end
290
+
291
+ # Resolves a previously registered component from the provider container.
292
+ #
293
+ # @param key [String] the key for the component to resolve
294
+ #
295
+ # @return [Object] the previously registered component
296
+ #
297
+ # @api public
298
+ def resolve(key)
299
+ provider_container.resolve(key)
300
+ end
301
+
302
+ # @api private
303
+ def run_step_block(step_name)
304
+ step_block = self.class.step_blocks[step_name]
305
+ instance_eval(&step_block) if step_block
306
+ end
307
+
308
+ # @api private
309
+ def method_missing(name, *args, &block)
310
+ if container.key?(name)
311
+ container[name]
312
+ else
313
+ super
314
+ end
315
+ end
316
+
317
+ # @api private
318
+ def respond_to_missing?(name, include_all = false)
319
+ container.key?(name) || super
320
+ end
321
+ end
322
+ end
323
+ end
324
+ 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