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
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module System
5
+ class Provider
6
+ # A provider's source provides the specific behavior for a given provider to serve
7
+ # its purpose.
8
+ #
9
+ # Sources should be subclasses of `Dry::System::Source::Provider`, with instance
10
+ # methods for each lifecycle step providing their behavior: {#prepare}, {#start},
11
+ # and {#stop}.
12
+ #
13
+ # Inside each of these methods, you should create and configure your provider's
14
+ # objects as required, and then {#register} them with the {#provider_container}.
15
+ # When the provider's lifecycle steps are run (via {Dry::System::Provider}), these
16
+ # registered components will be merged into the target container.
17
+ #
18
+ # You can prepare a provider's source in two ways:
19
+ #
20
+ # 1. Passing a bock when registering the provider, which is then evaluated via
21
+ # {Dry::System::Provider::SourceDSL} to prepare the provider subclass. This
22
+ # approach is easiest for simple providers.
23
+ # 2. Manually creare your own subclass of {Dry::System::Provider} and implement your
24
+ # own instance methods for the lifecycle steps (you should not implement your own
25
+ # `#initialize`). This approach may be useful for more complex providers.
26
+ #
27
+ # @see Dry::System::Container.register_provider
28
+ # @see Dry::System.register_provider_source
29
+ # @see Dry::System::Source::ProviderDSL
30
+ #
31
+ # @api public
32
+ class Source
33
+ class << self
34
+ # Returns a new Dry::System::Provider::Source subclass with its behavior supplied by the
35
+ # given block, which is evaluated using Dry::System::Provider::SourceDSL.
36
+ #
37
+ # @see Dry::System::Provider::SourceDSL
38
+ #
39
+ # @api private
40
+ def for(name:, group: nil, &block)
41
+ Class.new(self) { |klass|
42
+ klass.source_name name
43
+ klass.source_group group
44
+ SourceDSL.evaluate(klass, &block) if block
45
+ }
46
+ end
47
+
48
+ def inherited(subclass)
49
+ super
50
+
51
+ # Include Dry::Configurable only when first subclassing to ensure that
52
+ # distinct Source subclasses do not share settings.
53
+ #
54
+ # The superclass check here allows deeper Source class hierarchies to be
55
+ # created without running into a Dry::Configurable::AlreadyIncluded error.
56
+ if subclass.superclass == Source
57
+ subclass.include Dry::Configurable
58
+ end
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
+ # Registers a "before" callback for the given lifecycle step.
187
+ #
188
+ # The given block will be run before the lifecycle step method is run. The block
189
+ # will be evaluated in the context of the instance of this source.
190
+ #
191
+ # @param step_name [Symbol]
192
+ # @param block [Proc] the callback block
193
+ #
194
+ # @return [self]
195
+ #
196
+ # @see #after
197
+ #
198
+ # @api public
199
+ def before(step_name, &block)
200
+ callbacks[:before][step_name] << block
201
+ self
202
+ end
203
+
204
+ # Registers an "after" callback for the given lifecycle step.
205
+ #
206
+ # The given block will be run after the lifecycle step method is run. The block
207
+ # will be evaluated in the context of the instance of this source.
208
+ #
209
+ # @param step_name [Symbol]
210
+ # @param block [Proc] the callback block
211
+ #
212
+ # @return [self]
213
+ #
214
+ # @see #before
215
+ #
216
+ # @api public
217
+ def after(step_name, &block)
218
+ callbacks[:after][step_name] << block
219
+ self
220
+ end
221
+
222
+ # @api private
223
+ def run_callback(hook, step)
224
+ callbacks[hook][step].each do |callback|
225
+ instance_eval(&callback)
226
+ end
227
+ end
228
+
229
+ private
230
+
231
+ # Registers a component in the provider container.
232
+ #
233
+ # When the provider's lifecycle steps are run (via {Dry::System::Provider}), these
234
+ # registered components will be merged into the target container.
235
+ #
236
+ # @return [Dry::Container] the provider container
237
+ #
238
+ # @api public
239
+ def register(...)
240
+ provider_container.register(...)
241
+ end
242
+
243
+ # Resolves a previously registered component from the provider container.
244
+ #
245
+ # @param key [String] the key for the component to resolve
246
+ #
247
+ # @return [Object] the previously registered component
248
+ #
249
+ # @api public
250
+ def resolve(key)
251
+ provider_container.resolve(key)
252
+ end
253
+
254
+ # @api private
255
+ def run_step_block(step_name)
256
+ step_block = self.class.step_blocks[step_name]
257
+ instance_eval(&step_block) if step_block
258
+ end
259
+
260
+ # @api private
261
+ def method_missing(name, *args, &block)
262
+ if container.key?(name)
263
+ container[name]
264
+ else
265
+ super
266
+ end
267
+ end
268
+
269
+ # @api private
270
+ def respond_to_missing?(name, include_all = false)
271
+ container.key?(name) || super
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module System
5
+ class Provider
6
+ # Configures a Dry::System::Provider::Source subclass using a DSL that makes it
7
+ # nicer to define source behaviour via a single block.
8
+ #
9
+ # @see Dry::System::Container.register_provider
10
+ #
11
+ # @api private
12
+ class SourceDSL
13
+ def self.evaluate(source_class, &block)
14
+ new(source_class).instance_eval(&block)
15
+ end
16
+
17
+ attr_reader :source_class
18
+
19
+ def initialize(source_class)
20
+ @source_class = source_class
21
+ end
22
+
23
+ def setting(...)
24
+ source_class.setting(...)
25
+ end
26
+
27
+ def prepare(&block)
28
+ source_class.define_method(:prepare, &block)
29
+ end
30
+
31
+ def start(&block)
32
+ source_class.define_method(:start, &block)
33
+ end
34
+
35
+ def stop(&block)
36
+ source_class.define_method(:stop, &block)
37
+ end
38
+
39
+ private
40
+
41
+ def method_missing(name, *args, &block)
42
+ if source_class.respond_to?(name)
43
+ source_class.public_send(name, *args, &block)
44
+ else
45
+ super
46
+ end
47
+ end
48
+
49
+ def respond_to_missing?(name, include_all = false)
50
+ source_class.respond_to?(name, include_all) || super
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,48 +1,286 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "concurrent/map"
4
3
  require "dry/system/constants"
5
- require "dry/system/components/bootable"
6
4
 
7
5
  module Dry
8
6
  module System
7
+ # Providers can prepare and register one or more objects and typically work with third
8
+ # party code. A typical provider might be for a database library, or an API client.
9
+ #
10
+ # The particular behavior for any provider is defined in a {Provider::Source}, which
11
+ # is a subclass created when you run {Container.register_provider} or
12
+ # {Dry::System.register_provider_source}. The Source provides this behavior through
13
+ # methods for each of the steps in the provider lifecycle: `prepare`, `start`, and
14
+ # `run`. These methods typically create and configure various objects, then register
15
+ # them with the {#provider_container}.
16
+ #
17
+ # The Provider manages this lifecycle by implementing common behavior around the
18
+ # lifecycle steps, such as running step callbacks, and only running steps when
19
+ # appropriate for the current status of the lifecycle.
20
+ #
21
+ # Providers can be registered via {Container.register_provider}.
22
+ #
23
+ # @example Simple provider
24
+ # class App < Dry::System::Container
25
+ # register_provider(:logger) do
26
+ # prepare do
27
+ # require "logger"
28
+ # end
29
+ #
30
+ # start do
31
+ # register(:logger, Logger.new($stdout))
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # App[:logger] # returns configured logger
37
+ #
38
+ # @example Using an external Provider Source
39
+ # class App < Dry::System::Container
40
+ # register_provider(:logger, from: :some_external_provider_source) do
41
+ # configure do |config|
42
+ # config.log_level = :debug
43
+ # end
44
+ #
45
+ # after :start do
46
+ # register(:my_extra_logger, resolve(:logger))
47
+ # end
48
+ # end
49
+ # end
50
+ #
51
+ # App[:my_extra_logger] # returns the extra logger registered in the callback
52
+ #
53
+ # @api public
9
54
  class Provider
10
- attr_reader :identifier
55
+ # Returns the provider's unique name.
56
+ #
57
+ # @return [Symbol]
58
+ #
59
+ # @api public
60
+ attr_reader :name
11
61
 
12
- attr_reader :options
62
+ # Returns the default namespace for the provider's container keys.
63
+ #
64
+ # @return [Symbol,String]
65
+ #
66
+ # @api public
67
+ attr_reader :namespace
13
68
 
14
- attr_reader :components
69
+ # Returns an array of lifecycle steps that have been run.
70
+ #
71
+ # @return [Array<Symbol>]
72
+ #
73
+ # @example
74
+ # provider.statuses # => [:prepare, :start]
75
+ #
76
+ # @api public
77
+ attr_reader :statuses
15
78
 
16
- def initialize(identifier, options)
17
- @identifier = identifier
18
- @options = options
19
- @components = Concurrent::Map.new
79
+ # Returns the name of the currently running step, if any.
80
+ #
81
+ # @return [Symbol, nil]
82
+ #
83
+ # @api private
84
+ attr_reader :step_running
85
+ private :step_running
86
+
87
+ # Returns the container for the provider.
88
+ #
89
+ # This is where the provider's source will register its components, which are then
90
+ # later marged into the target container after the `prepare` and `start` lifecycle
91
+ # steps.
92
+ #
93
+ # @return [Dry::Core::Container]
94
+ #
95
+ # @api public
96
+ attr_reader :provider_container
97
+ alias_method :container, :provider_container
98
+
99
+ # Returns the target container for the provider.
100
+ #
101
+ # This is the container with which the provider is registered (via
102
+ # {Dry::System::Container.register_provider}).
103
+ #
104
+ # Registered components from the provider's container will be merged into this
105
+ # container after the `prepare` and `start` lifecycle steps.
106
+ #
107
+ # @return [Dry::System::Container]
108
+ #
109
+ # @api public
110
+ attr_reader :target_container
111
+ alias_method :target, :target_container
112
+
113
+ # Returns the provider's source
114
+ #
115
+ # The source provides the specific behavior for the provider via methods
116
+ # implementing the lifecycle steps.
117
+ #
118
+ # The provider's source is defined when registering a provider with the container,
119
+ # or an external provider source.
120
+ #
121
+ # @see Dry::System::Container.register_provider
122
+ # @see Dry::System.register_provider_source
123
+ #
124
+ # @return [Dry::System::Provider::Source]
125
+ #
126
+ # @api private
127
+ attr_reader :source
128
+
129
+ # @api private
130
+ def initialize(name:, namespace: nil, target_container:, source_class:, &block) # rubocop:disable Style/KeywordParametersOrder
131
+ @name = name
132
+ @namespace = namespace
133
+ @target_container = target_container
134
+
135
+ @provider_container = build_provider_container
136
+ @statuses = []
137
+ @step_running = nil
138
+
139
+ @source = source_class.new(
140
+ provider_container: provider_container,
141
+ target_container: target_container,
142
+ &block
143
+ )
144
+ end
145
+
146
+ # Runs the `prepare` lifecycle step.
147
+ #
148
+ # Also runs any callbacks for the step, and then merges any registered components
149
+ # from the provider container into the target container.
150
+ #
151
+ # @return [self]
152
+ #
153
+ # @api public
154
+ def prepare
155
+ run_step(:prepare)
20
156
  end
21
157
 
22
- def boot_path
23
- options.fetch(:boot_path)
158
+ # Runs the `start` lifecycle step.
159
+ #
160
+ # Also runs any callbacks for the step, and then merges any registered components
161
+ # from the provider container into the target container.
162
+ #
163
+ # @return [self]
164
+ #
165
+ # @api public
166
+ def start
167
+ run_step(:prepare)
168
+ run_step(:start)
24
169
  end
25
170
 
26
- def boot_files
27
- ::Dir[boot_path.join("**/#{RB_GLOB}")].sort
171
+ # Runs the `stop` lifecycle step.
172
+ #
173
+ # Also runs any callbacks for the step.
174
+ #
175
+ # @return [self]
176
+ #
177
+ # @api public
178
+ def stop
179
+ return self unless started?
180
+
181
+ run_step(:stop)
28
182
  end
29
183
 
30
- def register_component(name, fn)
31
- components[name] = Components::Bootable.new(name, &fn)
184
+ # Returns true if the provider's `prepare` lifecycle step has run
185
+ #
186
+ # @api public
187
+ def prepared?
188
+ statuses.include?(:prepare)
32
189
  end
33
190
 
34
- def boot_file(name)
35
- boot_files.detect { |path| Pathname(path).basename(RB_EXT).to_s == name.to_s }
191
+ # Returns true if the provider's `start` lifecycle step has run
192
+ #
193
+ # @api public
194
+ def started?
195
+ statuses.include?(:start)
36
196
  end
37
197
 
38
- def component(name, options = {})
39
- identifier = options[:key] || name
40
- components.fetch(identifier).new(name, options)
198
+ # Returns true if the provider's `stop` lifecycle step has run
199
+ #
200
+ # @api public
201
+ def stopped?
202
+ statuses.include?(:stop)
41
203
  end
42
204
 
43
- def load_components
44
- boot_files.each { |f| Kernel.require f }
45
- freeze
205
+ private
206
+
207
+ # @api private
208
+ def build_provider_container
209
+ container = Core::Container.new
210
+
211
+ case namespace
212
+ when String, Symbol
213
+ container.namespace(namespace) { |c| return c }
214
+ when true
215
+ container.namespace(name) { |c| return c }
216
+ when nil
217
+ container
218
+ else
219
+ raise ArgumentError,
220
+ "+namespace:+ must be true, string or symbol: #{namespace.inspect} given."
221
+ end
222
+ end
223
+
224
+ # @api private
225
+ def run_step(step_name)
226
+ return self if step_running? || statuses.include?(step_name)
227
+
228
+ @step_running = step_name
229
+
230
+ source.run_callback(:before, step_name)
231
+ source.public_send(step_name)
232
+ source.run_callback(:after, step_name)
233
+
234
+ statuses << step_name
235
+
236
+ apply
237
+
238
+ @step_running = nil
239
+
240
+ self
241
+ end
242
+
243
+ # Returns true if a step is currenly running.
244
+ #
245
+ # This is important for short-circuiting the invocation of {#run_step} and avoiding
246
+ # infinite loops if a provider step happens to result in resolution of a component
247
+ # with the same root key as the provider's own name (which ordinarily results in
248
+ # that provider being started).
249
+ #
250
+ # @return [Boolean]
251
+ #
252
+ # @see {#run_step}
253
+ #
254
+ # @api private
255
+ def step_running?
256
+ !!step_running
257
+ end
258
+
259
+ # Registers any components from the provider's container in the main container.
260
+ #
261
+ # Called after each lifecycle step runs.
262
+ #
263
+ # @return [self]
264
+ #
265
+ # @api private
266
+ def apply
267
+ provider_container.each_key do |key|
268
+ next if target_container.registered?(key)
269
+
270
+ # Access the provider's container items directly so that we can preserve all
271
+ # their options when we merge them with the target container (e.g. if a
272
+ # component in the provider container was registered with a block, we want block
273
+ # registration behavior to be exhibited when later resolving that component from
274
+ # the target container). TODO: Make this part of dry-system's public API.
275
+ item = provider_container._container[key]
276
+
277
+ if item.callable?
278
+ target_container.register(key, **item.options, &item.item)
279
+ else
280
+ target_container.register(key, item.item, **item.options)
281
+ end
282
+ end
283
+
46
284
  self
47
285
  end
48
286
  end