dry-system 0.18.1 → 1.0.1

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +678 -0
  3. data/LICENSE +1 -1
  4. data/README.md +5 -4
  5. data/dry-system.gemspec +18 -21
  6. data/lib/dry/system/auto_registrar.rb +9 -64
  7. data/lib/dry/system/component.rb +124 -104
  8. data/lib/dry/system/component_dir.rb +171 -0
  9. data/lib/dry/system/config/component_dir.rb +228 -0
  10. data/lib/dry/system/config/component_dirs.rb +289 -0
  11. data/lib/dry/system/config/namespace.rb +75 -0
  12. data/lib/dry/system/config/namespaces.rb +196 -0
  13. data/lib/dry/system/constants.rb +2 -4
  14. data/lib/dry/system/container.rb +305 -345
  15. data/lib/dry/system/errors.rb +73 -56
  16. data/lib/dry/system/identifier.rb +176 -0
  17. data/lib/dry/system/importer.rb +89 -12
  18. data/lib/dry/system/indirect_component.rb +63 -0
  19. data/lib/dry/system/loader/autoloading.rb +24 -0
  20. data/lib/dry/system/loader.rb +49 -41
  21. data/lib/dry/system/{manual_registrar.rb → manifest_registrar.rb} +13 -14
  22. data/lib/dry/system/plugins/bootsnap.rb +3 -2
  23. data/lib/dry/system/plugins/dependency_graph/strategies.rb +38 -2
  24. data/lib/dry/system/plugins/dependency_graph.rb +25 -21
  25. data/lib/dry/system/plugins/env.rb +3 -2
  26. data/lib/dry/system/plugins/logging.rb +9 -8
  27. data/lib/dry/system/plugins/monitoring.rb +1 -2
  28. data/lib/dry/system/plugins/notifications.rb +1 -1
  29. data/lib/dry/system/plugins/plugin.rb +61 -0
  30. data/lib/dry/system/plugins/zeitwerk/compat_inflector.rb +22 -0
  31. data/lib/dry/system/plugins/zeitwerk.rb +109 -0
  32. data/lib/dry/system/plugins.rb +5 -73
  33. data/lib/dry/system/provider/source.rb +276 -0
  34. data/lib/dry/system/provider/source_dsl.rb +55 -0
  35. data/lib/dry/system/provider.rb +261 -23
  36. data/lib/dry/system/provider_registrar.rb +251 -0
  37. data/lib/dry/system/provider_source_registry.rb +56 -0
  38. data/lib/dry/system/provider_sources/settings/config.rb +73 -0
  39. data/lib/dry/system/provider_sources/settings/loader.rb +44 -0
  40. data/lib/dry/system/provider_sources/settings.rb +40 -0
  41. data/lib/dry/system/provider_sources.rb +5 -0
  42. data/lib/dry/system/stubs.rb +6 -2
  43. data/lib/dry/system/version.rb +1 -1
  44. data/lib/dry/system.rb +35 -13
  45. metadata +48 -97
  46. data/lib/dry/system/auto_registrar/configuration.rb +0 -43
  47. data/lib/dry/system/booter/component_registry.rb +0 -35
  48. data/lib/dry/system/booter.rb +0 -181
  49. data/lib/dry/system/components/bootable.rb +0 -289
  50. data/lib/dry/system/components/config.rb +0 -35
  51. data/lib/dry/system/components.rb +0 -8
  52. data/lib/dry/system/lifecycle.rb +0 -135
  53. data/lib/dry/system/provider_registry.rb +0 -27
  54. data/lib/dry/system/settings/file_loader.rb +0 -30
  55. data/lib/dry/system/settings/file_parser.rb +0 -51
  56. data/lib/dry/system/settings.rb +0 -67
  57. data/lib/dry/system/system_components/settings.rb +0 -11
@@ -2,24 +2,10 @@
2
2
 
3
3
  require "pathname"
4
4
 
5
- require "dry-auto_inject"
6
- require "dry-configurable"
7
- require "dry-container"
5
+ require "dry/configurable"
6
+ require "dry/auto_inject"
8
7
  require "dry/inflector"
9
8
 
10
- require "dry/core/deprecations"
11
-
12
- require "dry/system"
13
- require "dry/system/errors"
14
- require "dry/system/loader"
15
- require "dry/system/booter"
16
- require "dry/system/auto_registrar"
17
- require "dry/system/manual_registrar"
18
- require "dry/system/importer"
19
- require "dry/system/component"
20
- require "dry/system/constants"
21
- require "dry/system/plugins"
22
-
23
9
  module Dry
24
10
  module System
25
11
  # Abstract container class to inherit from
@@ -46,7 +32,7 @@ module Dry
46
32
  #
47
33
  # Every container needs to be configured with following settings:
48
34
  #
49
- # * `:name` - a unique container identifier
35
+ # * `:name` - a unique container name
50
36
  # * `:root` - a system root directory (defaults to `pwd`)
51
37
  #
52
38
  # @example
@@ -61,219 +47,234 @@ module Dry
61
47
  # end
62
48
  #
63
49
  # # this will configure $LOAD_PATH to include your `lib` dir
64
- # load_paths!('lib')
50
+ # add_dirs_to_load_paths!('lib')
65
51
  # end
66
52
  #
67
53
  # @api public
68
54
  class Container
69
- extend Dry::Configurable
70
- extend Dry::Container::Mixin
55
+ extend Dry::Core::Container::Mixin
71
56
  extend Dry::System::Plugins
72
57
 
73
58
  setting :name
74
- setting :default_namespace
75
- setting(:root, Pathname.pwd.freeze) { |path| Pathname(path) }
76
- setting :system_dir, "system"
77
- setting :bootable_dirs, ["system/boot"]
78
- setting :registrations_dir, "container"
79
- setting :auto_register, []
80
- setting :inflector, Dry::Inflector.new
81
- setting :loader, Dry::System::Loader
82
- setting :booter, Dry::System::Booter
83
- setting :auto_registrar, Dry::System::AutoRegistrar
84
- setting :manual_registrar, Dry::System::ManualRegistrar
85
- setting :importer, Dry::System::Importer
86
- setting(:components, {}, reader: true, &:dup)
59
+ setting :root, default: Pathname.pwd.freeze, constructor: ->(path) { Pathname(path) }
60
+ setting :provider_dirs, default: ["system/providers"]
61
+ setting :registrations_dir, default: "system/registrations"
62
+ setting :component_dirs, default: Config::ComponentDirs.new, cloneable: true
63
+ setting :exports, reader: true
64
+ setting :inflector, default: Dry::Inflector.new
65
+ setting :auto_registrar, default: Dry::System::AutoRegistrar
66
+ setting :manifest_registrar, default: Dry::System::ManifestRegistrar
67
+ setting :provider_registrar, default: Dry::System::ProviderRegistrar
68
+ setting :importer, default: Dry::System::Importer
69
+
70
+ # Expect "." as key namespace separator. This is not intended to be user-configurable.
71
+ config.namespace_separator = KEY_SEPARATOR
87
72
 
88
73
  class << self
89
- def strategies(value = nil)
90
- if value
91
- @strategies = value
92
- else
93
- @strategies ||= Dry::AutoInject::Strategies
94
- end
95
- end
96
-
97
- extend Dry::Core::Deprecations["Dry::System::Container"]
98
-
99
- # Define a new configuration setting
74
+ # @!method config
75
+ # Returns the configuration for the container
100
76
  #
101
- # @see https://dry-rb.org/gems/dry-configurable
77
+ # @example
78
+ # container.config.root = "/path/to/app"
79
+ # container.config.root # => #<Pathname:/path/to/app>
102
80
  #
103
- # @api public
104
- def setting(name, *args, &block)
105
- super(name, *args, &block)
106
- # TODO: dry-configurable needs a public API for this
107
- config._settings << _settings[name]
108
- self
109
- end
81
+ # @return [Dry::Configurable::Config]
82
+ #
83
+ # @api public
110
84
 
111
- # Configures the container
85
+ # Yields a configuration object for the container, which you can use to modify the
86
+ # configuration, then runs the after-`configured` hooks and finalizes (freezes)
87
+ # the {config}.
88
+ #
89
+ # Does not finalize the config when given `finalize_config: false`
112
90
  #
113
91
  # @example
114
92
  # class MyApp < Dry::System::Container
115
93
  # configure do |config|
116
94
  # config.root = Pathname("/path/to/app")
117
95
  # config.name = :my_app
118
- # config.auto_register = %w(lib/apis lib/core)
119
96
  # end
120
97
  # end
121
98
  #
99
+ # @param finalize_config [Boolean]
100
+ #
122
101
  # @return [self]
123
102
  #
103
+ # @see after
104
+ #
124
105
  # @api public
125
- def configure(&block)
126
- hooks[:before_configure].each { |hook| instance_eval(&hook) }
106
+ def configure(finalize_config: true, &block)
127
107
  super(&block)
128
- load_paths!(config.system_dir)
108
+ configured!(finalize_config: finalize_config)
109
+ end
110
+
111
+ # Marks the container as configured, runs the after-`configured` hooks, then
112
+ # finalizes (freezes) the {config}.
113
+ #
114
+ # This method is useful to call if you're modifying the container's {config}
115
+ # directly, rather than via the config object yielded when calling {configure}.
116
+ #
117
+ # Does not finalize the config if given `finalize_config: false`.
118
+ #
119
+ # @param finalize_config [Boolean]
120
+ #
121
+ # @return [self]
122
+ #
123
+ # @see after
124
+ #
125
+ # @api public
126
+ def configured!(finalize_config: true)
127
+ return self if configured?
128
+
129
129
  hooks[:after_configure].each { |hook| instance_eval(&hook) }
130
+
131
+ _configurable_finalize! if finalize_config
132
+
133
+ @__configured__ = true
134
+
130
135
  self
131
136
  end
132
137
 
138
+ # Finalizes the config for this container
139
+ #
140
+ # @api private
141
+ alias_method :_configurable_finalize!, :finalize!
142
+
143
+ def configured?
144
+ @__configured__.equal?(true)
145
+ end
146
+
133
147
  # Registers another container for import
134
148
  #
135
149
  # @example
136
150
  # # system/container.rb
151
+ # require "dry/system/container"
152
+ # require "logger"
153
+ #
137
154
  # class Core < Dry::System::Container
138
- # configure do |config|
139
- # config.root = Pathname("/path/to/app")
140
- # config.auto_register = %w(lib/apis lib/core)
141
- # end
155
+ # register("logger", Logger.new($stdout))
142
156
  # end
143
157
  #
144
158
  # # apps/my_app/system/container.rb
145
159
  # require 'system/container'
146
160
  #
147
161
  # class MyApp < Dry::System::Container
148
- # configure do |config|
149
- # config.root = Pathname("/path/to/app")
150
- # config.auto_register = %w(lib/apis lib/core)
151
- # end
152
- #
153
- # import core: Core
162
+ # import(from: Core, as: :core)
154
163
  # end
155
164
  #
156
- # @param other [Hash, Dry::Container::Namespace]
165
+ # MyApp.import(keys: ["logger"], from: Core, as: :core2)
166
+ #
167
+ # MyApp["core.logger"].info("Test")
168
+ # MyApp["core2.logger"].info("Test2")
169
+ #
170
+ # @param keys [Array<String>] Keys for the components to import
171
+ # @param from [Class] The container to import from
172
+ # @param as [Symbol] Namespace to use for the components of the imported container
173
+ #
174
+ # @raise [Dry::System::ContainerAlreadyFinalizedError] if the container has already
175
+ # been finalized
157
176
  #
158
177
  # @api public
159
- def import(other)
160
- case other
161
- when Hash then importer.register(other)
162
- when Dry::Container::Namespace then super
163
- else
164
- raise ArgumentError, <<-STR
165
- +other+ must be a hash of names and systems, or a Dry::Container namespace
166
- STR
167
- end
178
+ def import(from:, as:, keys: nil)
179
+ raise Dry::System::ContainerAlreadyFinalizedError if finalized?
180
+
181
+ importer.register(container: from, namespace: as, keys: keys)
182
+
183
+ self
168
184
  end
169
185
 
170
- # Registers finalization function for a bootable component
186
+ # rubocop:disable Layout/LineLength
187
+
188
+ # @overload register_provider(name, namespace: nil, from: nil, source: nil, if: true, &block)
189
+ # Registers a provider and its lifecycle hooks
171
190
  #
172
- # By convention, boot files for components should be placed in a
173
- # `bootable_dirs` entry and they will be loaded on demand when
174
- # components are loaded in isolation, or during the finalization
175
- # process.
191
+ # By convention, you should place a file for each provider in one of the
192
+ # configured `provider_dirs`, and they will be loaded on demand when components
193
+ # are loaded in isolation, or during container finalization.
176
194
  #
177
- # @example
178
- # # system/container.rb
179
- # class MyApp < Dry::System::Container
180
- # configure do |config|
181
- # config.root = Pathname("/path/to/app")
182
- # config.name = :core
183
- # config.auto_register = %w(lib/apis lib/core)
195
+ # @example
196
+ # # system/container.rb
197
+ # class MyApp < Dry::System::Container
198
+ # configure do |config|
199
+ # config.root = Pathname("/path/to/app")
200
+ # end
184
201
  # end
185
202
  #
186
- # # system/boot/db.rb
187
- # #
188
- # # Simple component registration
189
- # MyApp.boot(:db) do |container|
190
- # require 'db'
203
+ # # system/providers/db.rb
204
+ # #
205
+ # # Simple provider registration
206
+ # MyApp.register_provider(:db) do
207
+ # start do
208
+ # require "db"
209
+ # register("db", DB.new)
210
+ # end
211
+ # end
191
212
  #
192
- # container.register(:db, DB.new)
193
- # end
213
+ # # system/providers/db.rb
214
+ # #
215
+ # # Provider registration with lifecycle triggers
216
+ # MyApp.register_provider(:db) do |container|
217
+ # init do
218
+ # require "db"
219
+ # DB.configure(ENV["DB_URL"])
220
+ # container.register("db", DB.new)
221
+ # end
194
222
  #
195
- # # system/boot/db.rb
196
- # #
197
- # # Component registration with lifecycle triggers
198
- # MyApp.boot(:db) do |container|
199
- # init do
200
- # require 'db'
201
- # DB.configure(ENV['DB_URL'])
202
- # container.register(:db, DB.new)
203
- # end
223
+ # start do
224
+ # container["db"].establish_connection
225
+ # end
204
226
  #
205
- # start do
206
- # db.establish_connection
227
+ # stop do
228
+ # container["db"].close_connection
229
+ # end
207
230
  # end
208
231
  #
209
- # stop do
210
- # db.close_connection
211
- # end
212
- # end
232
+ # # system/providers/db.rb
233
+ # #
234
+ # # Provider registration which uses another provider
235
+ # MyApp.register_provider(:db) do |container|
236
+ # start do
237
+ # use :logger
213
238
  #
214
- # # system/boot/db.rb
215
- # #
216
- # # Component registration which uses another bootable component
217
- # MyApp.boot(:db) do |container|
218
- # use :logger
219
- #
220
- # start do
221
- # require 'db'
222
- # DB.configure(ENV['DB_URL'], logger: logger)
223
- # container.register(:db, DB.new)
239
+ # require "db"
240
+ # DB.configure(ENV['DB_URL'], logger: logger)
241
+ # container.register("db", DB.new)
242
+ # end
224
243
  # end
225
- # end
226
244
  #
227
- # # system/boot/db.rb
228
- # #
229
- # # Component registration under a namespace. This will register the
230
- # # db object under `persistence.db` key
231
- # MyApp.namespace(:persistence) do |persistence|
232
- # require 'db'
233
- # DB.configure(ENV['DB_URL'], logger: logger)
234
- # persistence.register(:db, DB.new)
235
- # end
245
+ # # system/providers/db.rb
246
+ # #
247
+ # # Provider registration under a namespace. This will register the
248
+ # # db object with the "persistence.db" key
249
+ # MyApp.register_provider(:persistence, namespace: "db") do
250
+ # start do
251
+ # require "db"
252
+ # DB.configure(ENV["DB_URL"])
253
+ # register("db", DB.new)
254
+ # end
255
+ # end
236
256
  #
237
- # @param name [Symbol] a unique identifier for a bootable component
257
+ # @param name [Symbol] a unique name for the provider
258
+ # @param namespace [String, nil] the key namespace to use for any registrations
259
+ # made during the provider's lifecycle
260
+ # @param from [Symbol, nil] the group for the external provider source (with the
261
+ # provider source name inferred from `name` or passsed explicitly as
262
+ # `source:`)
263
+ # @param source [Symbol, nil] the name of the external provider source to use
264
+ # (if different from the value provided as `name`)
265
+ # @param if [Boolean] a boolean to determine whether to register the provider
238
266
  #
239
- # @see Lifecycle
267
+ # @see Provider
268
+ # @see Provider::Source
240
269
  #
241
- # @return [self]
270
+ # @return [self]
242
271
  #
243
- # @api public
244
- def boot(name, **opts, &block)
245
- if components.key?(name)
246
- raise DuplicatedComponentKeyError, <<-STR
247
- Bootable component #{name.inspect} was already registered
248
- STR
249
- end
250
-
251
- component =
252
- if opts[:from]
253
- boot_external(name, **opts, &block)
254
- else
255
- boot_local(name, **opts, &block)
256
- end
257
-
258
- booter.register_component component
259
-
260
- components[name] = component
261
- end
262
- deprecate :finalize, :boot
263
-
264
- # @api private
265
- def boot_external(identifier, from:, key: nil, namespace: nil, &block)
266
- System.providers[from].component(
267
- identifier, key: key, namespace: namespace, finalize: block, container: self
268
- )
272
+ # @api public
273
+ def register_provider(...)
274
+ providers.register_provider(...)
269
275
  end
270
276
 
271
- # @api private
272
- def boot_local(identifier, namespace: nil, &block)
273
- Components::Bootable.new(
274
- identifier, container: self, namespace: namespace, &block
275
- )
276
- end
277
+ # rubocop:enable Layout/LineLength
277
278
 
278
279
  # Return if a container was finalized
279
280
  #
@@ -316,51 +317,55 @@ module Dry
316
317
  def finalize!(freeze: true, &block)
317
318
  return self if finalized?
318
319
 
320
+ configured!
321
+
322
+ hooks[:before_finalize].each { |hook| instance_eval(&hook) }
319
323
  yield(self) if block
320
324
 
321
- importer.finalize!
322
- booter.finalize!
323
- manual_registrar.finalize!
325
+ providers.finalize!
324
326
  auto_registrar.finalize!
327
+ manifest_registrar.finalize!
328
+ importer.finalize!
325
329
 
326
330
  @__finalized__ = true
327
331
 
328
332
  self.freeze if freeze
333
+ hooks[:after_finalize].each { |hook| instance_eval(&hook) }
329
334
  self
330
335
  end
331
336
 
332
- # Boots a specific component
337
+ # Starts a provider
333
338
  #
334
- # As a result, `init` and `start` lifecycle triggers are called
339
+ # As a result, the provider's `prepare` and `start` lifecycle triggers are called
335
340
  #
336
341
  # @example
337
342
  # MyApp.start(:persistence)
338
343
  #
339
- # @param name [Symbol] the name of a registered bootable component
344
+ # @param name [Symbol] the name of a registered provider to start
340
345
  #
341
346
  # @return [self]
342
347
  #
343
348
  # @api public
344
349
  def start(name)
345
- booter.start(name)
350
+ providers.start(name)
346
351
  self
347
352
  end
348
353
 
349
- # Boots a specific component but calls only `init` lifecycle trigger
354
+ # Prepares a provider using its `prepare` lifecycle trigger
350
355
  #
351
- # This way of booting is useful in places where a heavy dependency is
352
- # needed but its started environment is not required
356
+ # Preparing (as opposed to starting) a provider is useful in places where some
357
+ # aspects of a heavier dependency are needed, but its fully started environment
353
358
  #
354
359
  # @example
355
- # MyApp.init(:persistence)
360
+ # MyApp.prepare(:persistence)
356
361
  #
357
- # @param [Symbol] name The name of a registered bootable component
362
+ # @param name [Symbol] The name of the registered provider to prepare
358
363
  #
359
364
  # @return [self]
360
365
  #
361
366
  # @api public
362
- def init(name)
363
- booter.init(name)
367
+ def prepare(name)
368
+ providers.prepare(name)
364
369
  self
365
370
  end
366
371
 
@@ -369,22 +374,23 @@ module Dry
369
374
  # @example
370
375
  # MyApp.stop(:persistence)
371
376
  #
372
- # @param [Symbol] name The name of a registered bootable component
377
+ # @param name [Symbol] The name of a registered bootable component
373
378
  #
374
379
  # @return [self]
375
380
  #
376
381
  # @api public
377
382
  def stop(name)
378
- booter.stop(name)
383
+ providers.stop(name)
379
384
  self
380
385
  end
381
386
 
387
+ # @api public
382
388
  def shutdown!
383
- booter.shutdown
389
+ providers.shutdown
384
390
  self
385
391
  end
386
392
 
387
- # Sets load paths relative to the container's root dir
393
+ # Adds the directories (relative to the container's root) to the Ruby load path
388
394
  #
389
395
  # @example
390
396
  # class MyApp < Dry::System::Container
@@ -392,68 +398,24 @@ module Dry
392
398
  # # ...
393
399
  # end
394
400
  #
395
- # load_paths!('lib')
401
+ # add_to_load_path!('lib')
396
402
  # end
397
403
  #
398
- # @param [Array<String>] dirs
404
+ # @param dirs [Array<String>]
399
405
  #
400
406
  # @return [self]
401
407
  #
402
408
  # @api public
403
- def load_paths!(*dirs)
404
- dirs.map(&root.method(:join)).each do |path|
405
- next if load_paths.include?(path)
406
-
407
- load_paths << path
408
- $LOAD_PATH.unshift(path.to_s)
409
+ def add_to_load_path!(*dirs)
410
+ dirs.reverse.map(&root.method(:join)).each do |path|
411
+ $LOAD_PATH.prepend(path.to_s) unless $LOAD_PATH.include?(path.to_s)
409
412
  end
410
413
  self
411
414
  end
412
415
 
413
416
  # @api public
414
417
  def load_registrations!(name)
415
- manual_registrar.(name)
416
- self
417
- end
418
-
419
- # Auto-registers components from the provided directory
420
- #
421
- # Typically you want to configure auto_register directories, and it will
422
- # work automatically. Use this method in cases where you want to have an
423
- # explicit way where some components are auto-registered, or if you want
424
- # to exclude some components from being auto-registered
425
- #
426
- # @example
427
- # class MyApp < Dry::System::Container
428
- # configure do |config|
429
- # # ...
430
- # end
431
- #
432
- # # with a dir
433
- # auto_register!('lib/core')
434
- #
435
- # # with a dir and a custom registration block
436
- # auto_register!('lib/core') do |config|
437
- # config.instance do |component|
438
- # # custom way of initializing a component
439
- # end
440
- #
441
- # config.exclude do |component|
442
- # # return true to exclude component from auto-registration
443
- # end
444
- # end
445
- # end
446
- #
447
- # @param [String] dir The dir name relative to the root dir
448
- #
449
- # @yield AutoRegistrar::Configuration
450
- # @see AutoRegistrar::Configuration
451
- #
452
- # @return [self]
453
- #
454
- # @api public
455
- def auto_register!(dir, &block)
456
- auto_registrar.(dir, &block)
418
+ manifest_registrar.(name)
457
419
  self
458
420
  end
459
421
 
@@ -482,8 +444,8 @@ module Dry
482
444
  # @param options [Hash] injector options
483
445
  #
484
446
  # @api public
485
- def injector(options = {strategies: strategies})
486
- Dry::AutoInject(self, options)
447
+ def injector(**options)
448
+ Dry::AutoInject(self, **options)
487
449
  end
488
450
 
489
451
  # Requires one or more files relative to the container's root
@@ -525,8 +487,8 @@ module Dry
525
487
  end
526
488
 
527
489
  # @api public
528
- def resolve(key, &block)
529
- load_component(key, &block) unless finalized?
490
+ def resolve(key)
491
+ load_component(key) unless finalized?
530
492
 
531
493
  super
532
494
  end
@@ -535,7 +497,7 @@ module Dry
535
497
  #
536
498
  # @!method registered?(key)
537
499
  # Whether a +key+ is registered (doesn't trigger loading)
538
- # @param [String,Symbol] key Identifier
500
+ # @param key [String,Symbol] The key
539
501
  # @return [Boolean]
540
502
  # @api public
541
503
  #
@@ -543,7 +505,7 @@ module Dry
543
505
  # Check if identifier is registered.
544
506
  # If not, try to load the component
545
507
  #
546
- # @param [String,Symbol] key Identifier
508
+ # @param key [String,Symbol] Identifier
547
509
  # @return [Boolean]
548
510
  #
549
511
  # @api public
@@ -557,26 +519,13 @@ module Dry
557
519
  end
558
520
 
559
521
  # @api private
560
- def load_paths
561
- @load_paths ||= []
522
+ def component_dirs
523
+ config.component_dirs.to_a.map { |dir| ComponentDir.new(config: dir, container: self) }
562
524
  end
563
525
 
564
526
  # @api private
565
- def booter
566
- @booter ||= config.booter.new(boot_paths)
567
- end
568
-
569
- # @api private
570
- def boot_paths
571
- config.bootable_dirs.map { |dir|
572
- dir = Pathname(dir)
573
-
574
- if dir.relative?
575
- root.join(dir)
576
- else
577
- dir
578
- end
579
- }
527
+ def providers
528
+ @providers ||= config.provider_registrar.new(self)
580
529
  end
581
530
 
582
531
  # @api private
@@ -585,8 +534,8 @@ module Dry
585
534
  end
586
535
 
587
536
  # @api private
588
- def manual_registrar
589
- @manual_registrar ||= config.manual_registrar.new(self)
537
+ def manifest_registrar
538
+ @manifest_registrar ||= config.manifest_registrar.new(self)
590
539
  end
591
540
 
592
541
  # @api private
@@ -594,74 +543,45 @@ module Dry
594
543
  @importer ||= config.importer.new(self)
595
544
  end
596
545
 
597
- # @api private
598
- def component(identifier, **options)
599
- if (component = booter.components.detect { |c| c.identifier == identifier })
600
- component
601
- else
602
- Component.new(
603
- identifier,
604
- loader: config.loader,
605
- namespace: config.default_namespace,
606
- separator: config.namespace_separator,
607
- inflector: config.inflector,
608
- **options
609
- )
610
- end
611
- end
612
-
613
- # @api private
614
- def require_component(component)
615
- return if registered?(component.identifier)
616
-
617
- raise FileNotFoundError, component unless component.file_exists?(load_paths)
618
-
619
- require_path(component.path)
620
-
621
- yield
622
- end
623
-
624
- # Allows subclasses to use a different strategy for required files.
546
+ # Registers a callback hook to run before container lifecycle events.
625
547
  #
626
- # E.g. apps that use `ActiveSupport::Dependencies::Loadable#require_dependency`
627
- # will override this method to allow container managed dependencies to be reloaded
628
- # for non-finalized containers.
548
+ # Currently, the only supported event is `:finalized`. This hook is called when
549
+ # you run `{finalize!}`.
629
550
  #
630
- # @api private
631
- def require_path(path)
632
- require path
633
- end
634
-
635
- # @api private
636
- def load_component(key, &block)
637
- return self if registered?(key)
638
-
639
- component(key).tap do |component|
640
- if component.bootable?
641
- booter.start(component)
642
- else
643
- root_key = component.root_key
644
-
645
- if (root_bootable = component(root_key)).bootable?
646
- booter.start(root_bootable)
647
- elsif importer.key?(root_key)
648
- load_imported_component(component.namespaced(root_key))
649
- end
650
-
651
- load_local_component(component, &block) unless registered?(key)
652
- end
653
- end
654
-
551
+ # When the given block is called, `self` is the container class, and no block
552
+ # arguments are given.
553
+ #
554
+ # @param event [Symbol] the event name
555
+ # @param block [Proc] the callback hook to run
556
+ #
557
+ # @return [self]
558
+ #
559
+ # @api public
560
+ def before(event, &block)
561
+ hooks[:"before_#{event}"] << block
655
562
  self
656
563
  end
657
564
 
658
- # @api private
565
+ # Registers a callback hook to run after container lifecycle events.
566
+ #
567
+ # The supported events are:
568
+ #
569
+ # - `:configured`, called when you run {configure} or {configured!}, or when
570
+ # running {finalize!} and neither of the prior two methods have been called.
571
+ # - `:finalized`, called when you run {finalize!}.
572
+ #
573
+ # When the given block is called, `self` is the container class, and no block
574
+ # arguments are given.
575
+ #
576
+ # @param event [Symbol] the event name
577
+ # @param block [Proc] the callback hook to run
578
+ #
579
+ # @return [self]
580
+ #
581
+ # @api public
659
582
  def after(event, &block)
660
583
  hooks[:"after_#{event}"] << block
661
- end
662
-
663
- def before(event, &block)
664
- hooks[:"before_#{event}"] << block
584
+ self
665
585
  end
666
586
 
667
587
  # @api private
@@ -671,45 +591,85 @@ module Dry
671
591
 
672
592
  # @api private
673
593
  def inherited(klass)
674
- new_hooks = Container.hooks.dup
675
-
676
594
  hooks.each do |event, blocks|
677
- new_hooks[event].concat(blocks)
678
- new_hooks[event].concat(klass.hooks[event])
595
+ klass.hooks[event].concat blocks.dup
679
596
  end
680
597
 
681
- klass.instance_variable_set(:@hooks, new_hooks)
598
+ klass.instance_variable_set(:@__configured__, false)
682
599
  klass.instance_variable_set(:@__finalized__, false)
600
+
683
601
  super
684
602
  end
685
603
 
686
- private
604
+ protected
687
605
 
606
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
688
607
  # @api private
689
- def load_local_component(component, default_namespace_fallback = false, &block)
690
- if booter.bootable?(component) || component.file_exists?(load_paths)
691
- booter.boot_dependency(component) unless finalized?
608
+ def load_component(key)
609
+ return self if registered?(key)
692
610
 
693
- require_component(component) do
694
- register(component.identifier) { component.instance }
695
- end
696
- elsif !default_namespace_fallback
697
- load_local_component(component.prepend(config.default_namespace), true, &block)
698
- elsif manual_registrar.file_exists?(component)
699
- manual_registrar.(component)
700
- elsif block_given?
701
- yield
702
- else
703
- raise ComponentLoadError, component
611
+ if (provider = providers.find_and_load_provider(key))
612
+ provider.start
613
+ return self
614
+ end
615
+
616
+ component = find_component(key)
617
+
618
+ providers.start_provider_dependency(component)
619
+ return self if registered?(key)
620
+
621
+ if component.loadable?
622
+ load_local_component(component)
623
+ elsif manifest_registrar.file_exists?(component)
624
+ manifest_registrar.(component)
625
+ elsif importer.namespace?(component.identifier.root_key)
626
+ load_imported_component(component.identifier, namespace: component.identifier.root_key)
627
+ elsif importer.namespace?(nil)
628
+ load_imported_component(component.identifier, namespace: nil)
704
629
  end
630
+
631
+ self
705
632
  end
633
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
706
634
 
707
- # @api private
708
- def load_imported_component(component)
709
- container = importer[component.namespace]
710
- container.load_component(component.identifier)
711
- importer.(component.namespace, container)
635
+ private
636
+
637
+ def load_local_component(component)
638
+ if component.auto_register?
639
+ register(component.identifier, memoize: component.memoize?) { component.instance }
640
+ end
641
+ end
642
+
643
+ def load_imported_component(identifier, namespace:)
644
+ return unless importer.namespace?(namespace)
645
+
646
+ import_key = identifier.namespaced(from: namespace, to: nil).key
647
+
648
+ importer.import(namespace, keys: [import_key])
712
649
  end
650
+
651
+ def find_component(key)
652
+ # Find the first matching component from within the configured component dirs.
653
+ # If no matching component is found, return a null component; this fallback is
654
+ # important because the component may still be loadable via the manifest
655
+ # registrar or an imported container.
656
+ component_dirs.detect { |dir|
657
+ if (component = dir.component_for_key(key))
658
+ break component
659
+ end
660
+ } || IndirectComponent.new(Identifier.new(key))
661
+ end
662
+ end
663
+
664
+ # Default hooks
665
+ after :configure do
666
+ # Add appropriately configured component dirs to the load path
667
+ #
668
+ # Do this in a single pass to preserve ordering (i.e. earliest dirs win)
669
+ paths = config.component_dirs.to_a.each_with_object([]) { |dir, arr|
670
+ arr << dir.path if dir.add_to_load_path
671
+ }
672
+ add_to_load_path!(*paths)
713
673
  end
714
674
  end
715
675
  end