dry-system 0.18.1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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