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
@@ -1,48 +1,288 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "concurrent/map"
4
- require "dry/system/constants"
5
- require "dry/system/components/bootable"
3
+ require "dry/core/deprecations"
4
+ require_relative "constants"
5
+ require_relative "provider/source"
6
6
 
7
7
  module Dry
8
8
  module System
9
+ # Providers can prepare and register one or more objects and typically work with third
10
+ # party code. A typical provider might be for a database library, or an API client.
11
+ #
12
+ # The particular behavior for any provider is defined in a {Provider::Source}, which
13
+ # is a subclass created when you run {Container.register_provider} or
14
+ # {Dry::System.register_provider_source}. The Source provides this behavior through
15
+ # methods for each of the steps in the provider lifecycle: `prepare`, `start`, and
16
+ # `run`. These methods typically create and configure various objects, then register
17
+ # them with the {#provider_container}.
18
+ #
19
+ # The Provider manages this lifecycle by implementing common behavior around the
20
+ # lifecycle steps, such as running step callbacks, and only running steps when
21
+ # appropriate for the current status of the lifecycle.
22
+ #
23
+ # Providers can be registered via {Container.register_provider}.
24
+ #
25
+ # @example Simple provider
26
+ # class App < Dry::System::Container
27
+ # register_provider(:logger) do
28
+ # prepare do
29
+ # require "logger"
30
+ # end
31
+ #
32
+ # start do
33
+ # register(:logger, Logger.new($stdout))
34
+ # end
35
+ # end
36
+ # end
37
+ #
38
+ # App[:logger] # returns configured logger
39
+ #
40
+ # @example Using an external Provider Source
41
+ # class App < Dry::System::Container
42
+ # register_provider(:logger, from: :some_external_provider_source) do
43
+ # configure do |config|
44
+ # config.log_level = :debug
45
+ # end
46
+ #
47
+ # after :start do
48
+ # register(:my_extra_logger, resolve(:logger))
49
+ # end
50
+ # end
51
+ # end
52
+ #
53
+ # App[:my_extra_logger] # returns the extra logger registered in the callback
54
+ #
55
+ # @api public
9
56
  class Provider
57
+ # Returns the provider's unique name.
58
+ #
59
+ # @return [Symbol]
60
+ #
61
+ # @api public
10
62
  attr_reader :name
11
63
 
12
- attr_reader :options
64
+ # Returns the default namespace for the provider's container keys.
65
+ #
66
+ # @return [Symbol,String]
67
+ #
68
+ # @api public
69
+ attr_reader :namespace
13
70
 
14
- attr_reader :components
71
+ # Returns an array of lifecycle steps that have been run.
72
+ #
73
+ # @return [Array<Symbol>]
74
+ #
75
+ # @example
76
+ # provider.statuses # => [:prepare, :start]
77
+ #
78
+ # @api public
79
+ attr_reader :statuses
15
80
 
16
- def initialize(name, options)
81
+ # Returns the name of the currently running step, if any.
82
+ #
83
+ # @return [Symbol, nil]
84
+ #
85
+ # @api private
86
+ attr_reader :step_running
87
+ private :step_running
88
+
89
+ # Returns the container for the provider.
90
+ #
91
+ # This is where the provider's source will register its components, which are then
92
+ # later marged into the target container after the `prepare` and `start` lifecycle
93
+ # steps.
94
+ #
95
+ # @return [Dry::Container]
96
+ #
97
+ # @api public
98
+ attr_reader :provider_container
99
+ alias_method :container, :provider_container
100
+
101
+ # Returns the target container for the provider.
102
+ #
103
+ # This is the container with which the provider is registered (via
104
+ # {Dry::System::Container.register_provider}).
105
+ #
106
+ # Registered components from the provider's container will be merged into this
107
+ # container after the `prepare` and `start` lifecycle steps.
108
+ #
109
+ # @return [Dry::System::Container]
110
+ #
111
+ # @api public
112
+ attr_reader :target_container
113
+ alias_method :target, :target_container
114
+
115
+ # Returns the provider's source
116
+ #
117
+ # The source provides the specific behavior for the provider via methods
118
+ # implementing the lifecycle steps.
119
+ #
120
+ # The provider's source is defined when registering a provider with the container,
121
+ # or an external provider source.
122
+ #
123
+ # @see Dry::System::Container.register_provider
124
+ # @see Dry::System.register_provider_source
125
+ #
126
+ # @return [Dry::System::Provider::Source]
127
+ #
128
+ # @api private
129
+ attr_reader :source
130
+
131
+ # @api private
132
+ def initialize(name:, namespace: nil, target_container:, source_class:, &block) # rubocop:disable Style/KeywordParametersOrder
17
133
  @name = name
18
- @options = options
19
- @components = Concurrent::Map.new
134
+ @namespace = namespace
135
+ @target_container = target_container
136
+
137
+ @provider_container = build_provider_container
138
+ @statuses = []
139
+ @step_running = nil
140
+
141
+ @source = source_class.new(
142
+ provider_container: provider_container,
143
+ target_container: target_container,
144
+ &block
145
+ )
146
+ end
147
+
148
+ # Runs the `prepare` lifecycle step.
149
+ #
150
+ # Also runs any callbacks for the step, and then merges any registered components
151
+ # from the provider container into the target container.
152
+ #
153
+ # @return [self]
154
+ #
155
+ # @api public
156
+ def prepare
157
+ run_step(:prepare)
20
158
  end
21
159
 
22
- def boot_path
23
- options.fetch(:boot_path)
160
+ # Runs the `start` lifecycle step.
161
+ #
162
+ # Also runs any callbacks for the step, and then merges any registered components
163
+ # from the provider container into the target container.
164
+ #
165
+ # @return [self]
166
+ #
167
+ # @api public
168
+ def start
169
+ run_step(:prepare)
170
+ run_step(:start)
24
171
  end
25
172
 
26
- def boot_files
27
- ::Dir[boot_path.join("**/#{RB_GLOB}")].sort
173
+ # Runs the `stop` lifecycle step.
174
+ #
175
+ # Also runs any callbacks for the step.
176
+ #
177
+ # @return [self]
178
+ #
179
+ # @api public
180
+ def stop
181
+ return self unless started?
182
+
183
+ run_step(:stop)
28
184
  end
29
185
 
30
- def register_component(name, fn)
31
- components[name] = Components::Bootable.new(name, &fn)
186
+ # Returns true if the provider's `prepare` lifecycle step has run
187
+ #
188
+ # @api public
189
+ def prepared?
190
+ statuses.include?(:prepare)
32
191
  end
33
192
 
34
- def boot_file(name)
35
- boot_files.detect { |path| Pathname(path).basename(RB_EXT).to_s == name.to_s }
193
+ # Returns true if the provider's `start` lifecycle step has run
194
+ #
195
+ # @api public
196
+ def started?
197
+ statuses.include?(:start)
36
198
  end
37
199
 
38
- def component(component_name, options = {})
39
- component_key = options[:key] || component_name
40
- components.fetch(component_key).new(component_name, options)
200
+ # Returns true if the provider's `stop` lifecycle step has run
201
+ #
202
+ # @api public
203
+ def stopped?
204
+ statuses.include?(:stop)
41
205
  end
42
206
 
43
- def load_components
44
- boot_files.each { |f| Kernel.require f }
45
- freeze
207
+ private
208
+
209
+ # @api private
210
+ def build_provider_container
211
+ container = Dry::Container.new
212
+
213
+ case namespace
214
+ when String, Symbol
215
+ container.namespace(namespace) { |c| return c }
216
+ when true
217
+ container.namespace(name) { |c| return c }
218
+ when nil
219
+ container
220
+ else
221
+ raise ArgumentError,
222
+ "+namespace:+ must be true, string or symbol: #{namespace.inspect} given."
223
+ end
224
+ end
225
+
226
+ # @api private
227
+ def run_step(step_name)
228
+ return self if step_running? || statuses.include?(step_name)
229
+
230
+ @step_running = step_name
231
+
232
+ source.run_callback(:before, step_name)
233
+ source.public_send(step_name)
234
+ source.run_callback(:after, step_name)
235
+
236
+ statuses << step_name
237
+
238
+ apply
239
+
240
+ @step_running = nil
241
+
242
+ self
243
+ end
244
+
245
+ # Returns true if a step is currenly running.
246
+ #
247
+ # This is important for short-circuiting the invocation of {#run_step} and avoiding
248
+ # infinite loops if a provider step happens to result in resolution of a component
249
+ # with the same root key as the provider's own name (which ordinarily results in
250
+ # that provider being started).
251
+ #
252
+ # @return [Boolean]
253
+ #
254
+ # @see {#run_step}
255
+ #
256
+ # @api private
257
+ def step_running?
258
+ !!step_running
259
+ end
260
+
261
+ # Registers any components from the provider's container in the main container.
262
+ #
263
+ # Called after each lifecycle step runs.
264
+ #
265
+ # @return [self]
266
+ #
267
+ # @api private
268
+ def apply
269
+ provider_container.each_key do |key|
270
+ next if target_container.registered?(key)
271
+
272
+ # Access the provider's container items directly so that we can preserve all
273
+ # their options when we merge them with the target container (e.g. if a
274
+ # component in the provider container was registered with a block, we want block
275
+ # registration behavior to be exhibited when later resolving that component from
276
+ # the target container). TODO: Make this part of dry-system's public API.
277
+ item = provider_container._container[key]
278
+
279
+ if item.callable?
280
+ target_container.register(key, **item.options, &item.item)
281
+ else
282
+ target_container.register(key, item.item, **item.options)
283
+ end
284
+ end
285
+
46
286
  self
47
287
  end
48
288
  end
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/deprecations"
4
+ require "dry/system"
5
+ require "pathname"
6
+ require_relative "errors"
7
+ require_relative "constants"
8
+ require_relative "provider"
9
+
10
+ module Dry
11
+ module System
12
+ # Default provider registrar implementation
13
+ #
14
+ # This is currently configured by default for every Dry::System::Container. The
15
+ # provider registrar is responsible for loading provider files and exposing an API for
16
+ # running the provider lifecycle steps.
17
+ #
18
+ # @api private
19
+ class ProviderRegistrar
20
+ extend Dry::Core::Deprecations["Dry::System::Container"]
21
+
22
+ # @api private
23
+ attr_reader :providers
24
+
25
+ # @api private
26
+ attr_reader :container
27
+
28
+ # @api private
29
+ def initialize(container)
30
+ @providers = {}
31
+ @container = container
32
+ end
33
+
34
+ # @api private
35
+ def freeze
36
+ providers.freeze
37
+ super
38
+ end
39
+
40
+ # rubocop:disable Metrics/PerceivedComplexity
41
+
42
+ # @see Container.register_provider
43
+ # @api private
44
+ def register_provider(name, namespace: nil, from: nil, source: nil, if: true, &block)
45
+ raise ProviderAlreadyRegisteredError, name if providers.key?(name)
46
+
47
+ if from && source.is_a?(Class)
48
+ raise ArgumentError, "You must supply a block when using a provider source"
49
+ end
50
+
51
+ if block && source.is_a?(Class)
52
+ raise ArgumentError, "You must supply only a `source:` option or a block, not both"
53
+ end
54
+
55
+ return self unless binding.local_variable_get(:if)
56
+
57
+ provider =
58
+ if from
59
+ build_provider_from_source(
60
+ name,
61
+ namespace: namespace,
62
+ source: source || name,
63
+ group: from,
64
+ &block
65
+ )
66
+ else
67
+ build_provider(name, namespace: namespace, source: source, &block)
68
+ end
69
+
70
+ providers[provider.name] = provider
71
+
72
+ self
73
+ end
74
+
75
+ # rubocop:enable Metrics/PerceivedComplexity
76
+
77
+ # Returns a provider for the given name, if it has already been loaded
78
+ #
79
+ # @api public
80
+ def [](provider_name)
81
+ providers[provider_name]
82
+ end
83
+ alias_method :provider, :[]
84
+
85
+ # @api private
86
+ def key?(provider_name)
87
+ providers.key?(provider_name)
88
+ end
89
+
90
+ # Returns a provider if it can be found or loaded, otherwise nil
91
+ #
92
+ # @return [Dry::System::Provider, nil]
93
+ #
94
+ # @api private
95
+ def find_and_load_provider(name)
96
+ name = name.to_sym
97
+
98
+ if (provider = providers[name])
99
+ return provider
100
+ end
101
+
102
+ return if finalized?
103
+
104
+ require_provider_file(name)
105
+
106
+ providers[name]
107
+ end
108
+
109
+ # @api private
110
+ def start_provider_dependency(component)
111
+ if (provider = find_and_load_provider(component.root_key))
112
+ provider.start
113
+ end
114
+ end
115
+
116
+ # Returns all provider files within the configured provider_paths.
117
+ #
118
+ # Searches for files in the order of the configured provider_paths. In the case of multiple
119
+ # identically-named boot files within different provider_paths, the file found first will be
120
+ # returned, and other matching files will be discarded.
121
+ #
122
+ # This method is public to allow other tools extending dry-system (like dry-rails)
123
+ # to access a canonical list of real, in-use provider files.
124
+ #
125
+ # @see Container.provider_paths
126
+ #
127
+ # @return [Array<Pathname>]
128
+ # @api public
129
+ def provider_files
130
+ @provider_files ||= provider_paths.each_with_object([[], []]) { |path, (provider_files, loaded)| # rubocop:disable Layout/LineLength
131
+ files = Dir["#{path}/#{RB_GLOB}"].sort
132
+
133
+ files.each do |file|
134
+ basename = File.basename(file)
135
+
136
+ unless loaded.include?(basename)
137
+ provider_files << Pathname(file)
138
+ loaded << basename
139
+ end
140
+ end
141
+ }.first
142
+ end
143
+ deprecate :boot_files, :provider_files
144
+
145
+ # @api private
146
+ def finalize!
147
+ provider_files.each do |path|
148
+ load_provider(path)
149
+ end
150
+
151
+ providers.each_value(&:start)
152
+
153
+ freeze
154
+ end
155
+
156
+ # @!method finalized?
157
+ # Returns true if the booter has been finalized
158
+ #
159
+ # @return [Boolean]
160
+ # @api private
161
+ alias_method :finalized?, :frozen?
162
+
163
+ # @api private
164
+ def shutdown
165
+ providers.each_value(&:stop)
166
+ self
167
+ end
168
+
169
+ # @api private
170
+ def prepare(provider_name)
171
+ with_provider(provider_name, &:prepare)
172
+ self
173
+ end
174
+
175
+ # @api private
176
+ def start(provider_name)
177
+ with_provider(provider_name, &:start)
178
+ self
179
+ end
180
+
181
+ # @api private
182
+ def stop(provider_name)
183
+ with_provider(provider_name, &:stop)
184
+ self
185
+ end
186
+
187
+ private
188
+
189
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Layout/LineLength
190
+ # @api private
191
+ def provider_paths
192
+ provider_dirs = container.config.provider_dirs
193
+ bootable_dirs = container.config.bootable_dirs || ["system/boot"]
194
+
195
+ if container.config.provider_dirs == ["system/providers"] && \
196
+ provider_dirs.none? { |d| container.root.join(d).exist? } && \
197
+ bootable_dirs.any? { |d| container.root.join(d).exist? }
198
+ Dry::Core::Deprecations.announce(
199
+ "Dry::System::Container.config.bootable_dirs (defaulting to 'system/boot')",
200
+ "Use `Dry::System::Container.config.provider_dirs` (defaulting to 'system/providers') instead",
201
+ tag: "dry-system",
202
+ uplevel: 2
203
+ )
204
+
205
+ provider_dirs = bootable_dirs
206
+ end
207
+
208
+ provider_dirs.map { |dir|
209
+ dir = Pathname(dir)
210
+
211
+ if dir.relative?
212
+ container.root.join(dir)
213
+ else
214
+ dir
215
+ end
216
+ }
217
+ end
218
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Layout/LineLength
219
+
220
+ def build_provider(name, namespace:, source: nil, &block)
221
+ source_class = source || Provider::Source.for(
222
+ name: name,
223
+ target_container: container,
224
+ &block
225
+ )
226
+
227
+ Provider.new(
228
+ name: name,
229
+ namespace: namespace,
230
+ target_container: container,
231
+ source_class: source_class
232
+ )
233
+ end
234
+
235
+ def build_provider_from_source(name, source:, group:, namespace:, &block)
236
+ source_class = System.provider_sources.resolve(name: source, group: group)
237
+
238
+ Provider.new(
239
+ name: name,
240
+ namespace: namespace,
241
+ target_container: container,
242
+ source_class: source_class,
243
+ &block
244
+ )
245
+ end
246
+
247
+ def with_provider(provider_name)
248
+ require_provider_file(provider_name) unless providers.key?(provider_name)
249
+
250
+ provider = providers[provider_name]
251
+
252
+ raise ProviderNotFoundError, provider_name unless provider
253
+
254
+ yield(provider)
255
+ end
256
+
257
+ def load_provider(path)
258
+ name = Pathname(path).basename(RB_EXT).to_s.to_sym
259
+
260
+ Kernel.require path unless providers.key?(name)
261
+
262
+ self
263
+ end
264
+
265
+ def require_provider_file(name)
266
+ provider_file = find_provider_file(name)
267
+
268
+ Kernel.require provider_file if provider_file
269
+ end
270
+
271
+ def find_provider_file(name)
272
+ provider_files.detect { |file| File.basename(file, RB_EXT) == name.to_s }
273
+ end
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/deprecations"
4
+ require_relative "constants"
5
+ require_relative "provider/source"
6
+
7
+ module Dry
8
+ module System
9
+ # @api private
10
+ class ProviderSourceRegistry
11
+ attr_reader :sources
12
+
13
+ def initialize
14
+ @sources = {}
15
+ end
16
+
17
+ def load_sources(path)
18
+ Dir[File.join(path, "**/#{RB_GLOB}")].sort.each do |file|
19
+ require file
20
+ end
21
+ end
22
+
23
+ def register(name:, group:, source:)
24
+ sources[key(name, group)] = source
25
+ end
26
+
27
+ def register_from_block(name:, group:, target_container:, &block)
28
+ register(
29
+ name: name,
30
+ group: group,
31
+ source: Provider::Source.for(
32
+ name: name,
33
+ group: group,
34
+ target_container: target_container,
35
+ &block
36
+ )
37
+ )
38
+ end
39
+
40
+ def resolve(name:, group:)
41
+ if group == :system
42
+ Dry::Core::Deprecations.announce(
43
+ "Providers using `from: :system`",
44
+ "Use `from: :dry_system` instead",
45
+ tag: "dry-system",
46
+ uplevel: 1
47
+ )
48
+
49
+ group = :dry_system
50
+ end
51
+
52
+ sources[key(name, group)].tap { |source|
53
+ unless source
54
+ raise ProviderSourceNotFoundError.new(
55
+ name: name,
56
+ group: group,
57
+ keys: sources.keys
58
+ )
59
+ end
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def key(name, group)
66
+ {group: group, name: name}
67
+ end
68
+ end
69
+ end
70
+ end