dry-system 0.19.2 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +472 -1
  3. data/LICENSE +1 -1
  4. data/README.md +4 -3
  5. data/dry-system.gemspec +16 -15
  6. data/lib/dry/system/auto_registrar.rb +1 -13
  7. data/lib/dry/system/component.rb +104 -47
  8. data/lib/dry/system/component_dir.rb +88 -47
  9. data/lib/dry/system/components.rb +8 -4
  10. data/lib/dry/system/config/component_dir.rb +141 -53
  11. data/lib/dry/system/config/component_dirs.rb +176 -70
  12. data/lib/dry/system/config/namespace.rb +76 -0
  13. data/lib/dry/system/config/namespaces.rb +208 -0
  14. data/lib/dry/system/constants.rb +2 -2
  15. data/lib/dry/system/container.rb +279 -201
  16. data/lib/dry/system/errors.rb +72 -61
  17. data/lib/dry/system/identifier.rb +99 -79
  18. data/lib/dry/system/importer.rb +83 -12
  19. data/lib/dry/system/indirect_component.rb +65 -0
  20. data/lib/dry/system/loader.rb +8 -4
  21. data/lib/dry/system/{manual_registrar.rb → manifest_registrar.rb} +12 -13
  22. data/lib/dry/system/plugins/bootsnap.rb +3 -2
  23. data/lib/dry/system/plugins/dependency_graph/strategies.rb +37 -1
  24. data/lib/dry/system/plugins/dependency_graph.rb +26 -20
  25. data/lib/dry/system/plugins/env.rb +3 -2
  26. data/lib/dry/system/plugins/logging.rb +9 -5
  27. data/lib/dry/system/plugins/monitoring.rb +1 -1
  28. data/lib/dry/system/plugins/notifications.rb +1 -1
  29. data/lib/dry/system/plugins/zeitwerk/compat_inflector.rb +22 -0
  30. data/lib/dry/system/plugins/zeitwerk.rb +109 -0
  31. data/lib/dry/system/plugins.rb +8 -7
  32. data/lib/dry/system/provider/source.rb +324 -0
  33. data/lib/dry/system/provider/source_dsl.rb +94 -0
  34. data/lib/dry/system/provider.rb +264 -24
  35. data/lib/dry/system/provider_registrar.rb +276 -0
  36. data/lib/dry/system/provider_source_registry.rb +70 -0
  37. data/lib/dry/system/provider_sources/settings/config.rb +86 -0
  38. data/lib/dry/system/provider_sources/settings/loader.rb +53 -0
  39. data/lib/dry/system/provider_sources/settings.rb +40 -0
  40. data/lib/dry/system/provider_sources.rb +5 -0
  41. data/lib/dry/system/stubs.rb +1 -1
  42. data/lib/dry/system/version.rb +1 -1
  43. data/lib/dry/system.rb +45 -13
  44. metadata +25 -22
  45. data/lib/dry/system/booter/component_registry.rb +0 -35
  46. data/lib/dry/system/booter.rb +0 -200
  47. data/lib/dry/system/components/bootable.rb +0 -289
  48. data/lib/dry/system/components/config.rb +0 -35
  49. data/lib/dry/system/lifecycle.rb +0 -135
  50. data/lib/dry/system/provider_registry.rb +0 -27
  51. data/lib/dry/system/settings/file_loader.rb +0 -30
  52. data/lib/dry/system/settings/file_parser.rb +0 -51
  53. data/lib/dry/system/settings.rb +0 -67
  54. 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
10
- attr_reader :identifier
57
+ # Returns the provider's unique name.
58
+ #
59
+ # @return [Symbol]
60
+ #
61
+ # @api public
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(identifier, options)
17
- @identifier = identifier
18
- @options = options
19
- @components = Concurrent::Map.new
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
133
+ @name = name
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(name, options = {})
39
- identifier = options[:key] || name
40
- components.fetch(identifier).new(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