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,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/system/constants"
4
+
5
+ module Dry
6
+ module System
7
+ module Config
8
+ # @api public
9
+ class ComponentDir
10
+ include Dry::Configurable
11
+
12
+ # @!group Settings
13
+
14
+ # @!method auto_register=(policy)
15
+ #
16
+ # Sets the auto-registration policy for the component dir.
17
+ #
18
+ # This may be a simple boolean to enable or disable auto-registration for all
19
+ # components, or a proc accepting a {Dry::System::Component} and returning a
20
+ # boolean to configure auto-registration on a per-component basis
21
+ #
22
+ # Defaults to `true`.
23
+ #
24
+ # @param policy [Boolean, Proc]
25
+ # @return [Boolean, Proc]
26
+ #
27
+ # @example
28
+ # dir.auto_register = false
29
+ #
30
+ # @example
31
+ # dir.auto_register = proc do |component|
32
+ # !component.identifier.start_with?("entities")
33
+ # end
34
+ #
35
+ # @see auto_register
36
+ # @see Component
37
+ # @api public
38
+ #
39
+ # @!method auto_register
40
+ #
41
+ # Returns the configured auto-registration policy.
42
+ #
43
+ # @return [Boolean, Proc] the configured policy
44
+ #
45
+ # @see auto_register=
46
+ # @api public
47
+ setting :auto_register, default: true
48
+
49
+ # @!method instance=(instance_proc)
50
+ #
51
+ # Sets a proc used to return the instance of any component within the component
52
+ # dir.
53
+ #
54
+ # This proc should accept a {Dry::System::Component} and return the object to
55
+ # serve as the component's instance.
56
+ #
57
+ # When you provide an instance proc, it will be used in preference to the
58
+ # {loader} (either the default loader or an explicitly configured one). Provide
59
+ # an instance proc when you want a simple way to customize the instance for
60
+ # certain components. For complete control, provide a replacement loader via
61
+ # {loader=}.
62
+ #
63
+ # Defaults to `nil`.
64
+ #
65
+ # @param instance_proc [Proc, nil]
66
+ # @return [Proc]
67
+ #
68
+ # @example
69
+ # dir.instance = proc do |component|
70
+ # if component.key.match?(/workers\./)
71
+ # # Register classes for jobs
72
+ # component.loader.constant(component)
73
+ # else
74
+ # # Otherwise register regular instances per default loader
75
+ # component.loader.call(component)
76
+ # end
77
+ # end
78
+ #
79
+ # @see Component, Loader
80
+ # @api public
81
+ #
82
+ # @!method instance
83
+ #
84
+ # Returns the configured instance proc.
85
+ #
86
+ # @return [Proc, nil]
87
+ #
88
+ # @see instance=
89
+ # @api public
90
+ setting :instance
91
+
92
+ # @!method loader=(loader)
93
+ #
94
+ # Sets the loader to use when registering components from the dir in the
95
+ # container.
96
+ #
97
+ # Defaults to `Dry::System::Loader`.
98
+ #
99
+ # When using an autoloader like Zeitwerk, consider using
100
+ # `Dry::System::Loader::Autoloading`
101
+ #
102
+ # @param loader [#call] the loader
103
+ # @return [#call] the configured loader
104
+ #
105
+ # @see loader
106
+ # @see Loader
107
+ # @see Loader::Autoloading
108
+ # @api public
109
+ #
110
+ # @!method loader
111
+ #
112
+ # Returns the configured loader.
113
+ #
114
+ # @return [#call]
115
+ #
116
+ # @see loader=
117
+ # @api public
118
+ setting :loader, default: Dry::System::Loader
119
+
120
+ # @!method memoize=(policy)
121
+ #
122
+ # Sets whether to memoize components from the dir when registered in the
123
+ # container.
124
+ #
125
+ # This may be a simple boolean to enable or disable memoization for all
126
+ # components, or a proc accepting a `Dry::Sytem::Component` and returning a
127
+ # boolean to configure memoization on a per-component basis
128
+ #
129
+ # Defaults to `false`.
130
+ #
131
+ # @param policy [Boolean, Proc]
132
+ # @return [Boolean, Proc] the configured memoization policy
133
+ #
134
+ # @example
135
+ # dir.memoize = true
136
+ #
137
+ # @example
138
+ # dir.memoize = proc do |component|
139
+ # !component.identifier.start_with?("providers")
140
+ # end
141
+ #
142
+ # @see memoize
143
+ # @see Component
144
+ # @api public
145
+ #
146
+ # @!method memoize
147
+ #
148
+ # Returns the configured memoization policy.
149
+ #
150
+ # @return [Boolean, Proc] the configured memoization policy
151
+ #
152
+ # @see memoize=
153
+ # @api public
154
+ setting :memoize, default: false
155
+
156
+ # @!method namespaces
157
+ #
158
+ # Returns the configured namespaces for the component dir.
159
+ #
160
+ # Allows namespaces to added on the returned object via {Namespaces#add}.
161
+ #
162
+ # @return [Namespaces] the namespaces
163
+ #
164
+ # @see Namespaces#add
165
+ # @api public
166
+ setting :namespaces, default: Namespaces.new, cloneable: true
167
+
168
+ # @!method add_to_load_path=(policy)
169
+ #
170
+ # Sets whether the dir should be added to the `$LOAD_PATH` after the container
171
+ # is configured.
172
+ #
173
+ # Defaults to `true`. This may need to be set to `false` when using a class
174
+ # autoloading system.
175
+ #
176
+ # @param policy [Boolean]
177
+ # @return [Boolean]
178
+ #
179
+ # @see add_to_load_path
180
+ # @see Container.configure
181
+ # @api public
182
+ #
183
+ # @!method add_to_load_path
184
+ #
185
+ # Returns the configured value.
186
+ #
187
+ # @return [Boolean]
188
+ #
189
+ # @see add_to_load_path=
190
+ # @api public
191
+ setting :add_to_load_path, default: true
192
+
193
+ # @!endgroup
194
+
195
+ # Returns the component dir path, relative to the configured container root
196
+ #
197
+ # @return [String] the path
198
+ attr_reader :path
199
+
200
+ # @api public
201
+ def initialize(path)
202
+ super()
203
+ @path = path
204
+ yield self if block_given?
205
+ end
206
+
207
+ # @api private
208
+ def auto_register?
209
+ !!config.auto_register
210
+ end
211
+
212
+ private
213
+
214
+ def method_missing(name, *args, &block)
215
+ if config.respond_to?(name)
216
+ config.public_send(name, *args, &block)
217
+ else
218
+ super
219
+ end
220
+ end
221
+
222
+ def respond_to_missing?(name, include_all = false)
223
+ config.respond_to?(name) || super
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/system/constants"
4
+ require "dry/system/errors"
5
+
6
+ module Dry
7
+ module System
8
+ module Config
9
+ # The configured component dirs for a container
10
+ #
11
+ # @api public
12
+ class ComponentDirs
13
+ # @!group Settings
14
+
15
+ # @!method auto_register=(value)
16
+ #
17
+ # Sets a default `auto_register` for all added component dirs
18
+ #
19
+ # @see ComponentDir.auto_register=
20
+ # @see auto_register
21
+ #
22
+ # @!method auto_register
23
+ #
24
+ # Returns the configured default `auto_register`
25
+ #
26
+ # @see auto_register=
27
+
28
+ # @!method instance=(value)
29
+ #
30
+ # Sets a default `instance` for all added component dirs
31
+ #
32
+ # @see ComponentDir.instance=
33
+ # @see auto_register
34
+ #
35
+ # @!method auto_register
36
+ #
37
+ # Returns the configured default `instance`
38
+ #
39
+ # @see instance=
40
+
41
+ # @!method loader=(value)
42
+ #
43
+ # Sets a default `loader` value for all added component dirs
44
+ #
45
+ # @see ComponentDir.loader=
46
+ # @see loader
47
+ #
48
+ # @!method loader
49
+ #
50
+ # Returns the configured default `loader`
51
+ #
52
+ # @see loader=
53
+
54
+ # @!method memoize=(value)
55
+ #
56
+ # Sets a default `memoize` value for all added component dirs
57
+ #
58
+ # @see ComponentDir.memoize=
59
+ # @see memoize
60
+ #
61
+ # @!method memoize
62
+ #
63
+ # Returns the configured default `memoize`
64
+ #
65
+ # @see memoize=
66
+
67
+ # rubocop:disable Layout/LineLength
68
+
69
+ # @!method namespaces
70
+ #
71
+ # Returns the default configured namespaces for all added component dirs
72
+ #
73
+ # Allows namespaces to added on the returned object via {Dry::System::Config::Namespaces#add}.
74
+ #
75
+ # @see Dry::System::Config::Namespaces#add
76
+ #
77
+ # @return [Namespaces] the namespaces
78
+
79
+ # @!method add_to_load_path=(value)
80
+ #
81
+ # Sets a default `add_to_load_path` value for all added component dirs
82
+ #
83
+ # @see ComponentDir.add_to_load_path=
84
+ # @see add_to_load_path
85
+ #
86
+ # @!method add_to_load_path
87
+ #
88
+ # Returns the configured default `add_to_load_path`
89
+ #
90
+ # @see add_to_load_path=
91
+
92
+ # rubocop:enable Layout/LineLength
93
+
94
+ # @!endgroup
95
+
96
+ # A ComponentDir for configuring the default values to apply to all added
97
+ # component dirs
98
+ #
99
+ # @see #method_missing
100
+ # @api private
101
+ attr_reader :defaults
102
+
103
+ # Creates a new component dirs
104
+ #
105
+ # @api private
106
+ def initialize
107
+ @dirs = {}
108
+ @defaults = ComponentDir.new(nil)
109
+ end
110
+
111
+ # @api private
112
+ def initialize_copy(source)
113
+ @dirs = source.dirs.map { |path, dir| [path, dir.dup] }.to_h
114
+ @defaults = source.defaults.dup
115
+ end
116
+
117
+ # Returns and optionally yields a previously added component dir
118
+ #
119
+ # @param path [String] the path for the component dir
120
+ # @yieldparam dir [ComponentDir] the component dir
121
+ #
122
+ # @return [ComponentDir] the component dir
123
+ #
124
+ # @api public
125
+ def dir(path)
126
+ dirs[path].tap do |dir|
127
+ # Defaults can be (re-)applied first, since the dir has already been added
128
+ apply_defaults_to_dir(dir) if dir
129
+ yield dir if block_given?
130
+ end
131
+ end
132
+ alias_method :[], :dir
133
+
134
+ # @overload add(path)
135
+ # Adds and configures a component dir for the given path
136
+ #
137
+ # @param path [String] the path for the component dir, relative to the configured
138
+ # container root
139
+ # @yieldparam dir [ComponentDir] the component dir to configure
140
+ #
141
+ # @return [ComponentDir] the added component dir
142
+ #
143
+ # @example
144
+ # component_dirs.add "lib" do |dir|
145
+ # dir.default_namespace = "my_app"
146
+ # end
147
+ #
148
+ # @see ComponentDir
149
+ # @api public
150
+ #
151
+ # @overload add(dir)
152
+ # Adds a configured component dir
153
+ #
154
+ # @param dir [ComponentDir] the configured component dir
155
+ #
156
+ # @return [ComponentDir] the added component dir
157
+ #
158
+ # @example
159
+ # dir = Dry::System::ComponentDir.new("lib")
160
+ # component_dirs.add dir
161
+ #
162
+ # @see ComponentDir
163
+ # @api public
164
+ def add(path_or_dir)
165
+ path, dir_to_add = path_and_dir(path_or_dir)
166
+
167
+ raise ComponentDirAlreadyAddedError, path if dirs.key?(path)
168
+
169
+ dirs[path] = dir_to_add.tap do |dir|
170
+ # Defaults must be applied after yielding, since the dir is being newly added,
171
+ # and must have its configuration fully in place before we can know which
172
+ # defaults to apply
173
+ yield dir if path_or_dir == path && block_given?
174
+ apply_defaults_to_dir(dir)
175
+ end
176
+ end
177
+
178
+ # Deletes and returns a previously added component dir
179
+ #
180
+ # @param path [String] the path for the component dir
181
+ #
182
+ # @return [ComponentDir] the removed component dir
183
+ #
184
+ # @api public
185
+ def delete(path)
186
+ dirs.delete(path)
187
+ end
188
+
189
+ # Returns the paths of the component dirs
190
+ #
191
+ # @return [Array<String>] the component dir paths
192
+ #
193
+ # @api public
194
+ def paths
195
+ dirs.keys
196
+ end
197
+
198
+ # Returns the count of component dirs
199
+ #
200
+ # @return [Integer]
201
+ #
202
+ # @api public
203
+ def length
204
+ dirs.length
205
+ end
206
+ alias_method :size, :length
207
+
208
+ # Returns the added component dirs, with default settings applied
209
+ #
210
+ # @return [Array<ComponentDir>]
211
+ #
212
+ # @api public
213
+ def to_a
214
+ dirs.each { |_, dir| apply_defaults_to_dir(dir) }
215
+ dirs.values
216
+ end
217
+
218
+ # Calls the given block once for each added component dir, passing the dir as an
219
+ # argument.
220
+ #
221
+ # @yieldparam dir [ComponentDir] the yielded component dir
222
+ #
223
+ # @api public
224
+ def each(&block)
225
+ to_a.each(&block)
226
+ end
227
+
228
+ protected
229
+
230
+ # Returns the hash of component dirs, keyed by their paths
231
+ #
232
+ # Recently changed default configuration may not be applied to these dirs. Use
233
+ # #to_a or #each to access dirs with default configuration fully applied.
234
+ #
235
+ # This method exists to encapsulate the instance variable and to serve the needs
236
+ # of #initialize_copy
237
+ #
238
+ # @return [Hash{String => ComponentDir}]
239
+ #
240
+ # @api private
241
+ attr_reader :dirs
242
+
243
+ private
244
+
245
+ # Converts a path string or pre-built component dir into a path and dir tuple
246
+ #
247
+ # @param path_or_dir [String,ComponentDir]
248
+ #
249
+ # @return [Array<(String, ComponentDir)>]
250
+ #
251
+ # @see #add
252
+ def path_and_dir(path_or_dir)
253
+ if path_or_dir.is_a?(ComponentDir)
254
+ dir = path_or_dir
255
+ [dir.path, dir]
256
+ else
257
+ path = path_or_dir
258
+ [path, ComponentDir.new(path)]
259
+ end
260
+ end
261
+
262
+ # Applies default settings to a component dir. This is run every time the dirs are
263
+ # accessed to ensure defaults are applied regardless of when new component dirs
264
+ # are added. This method must be idempotent.
265
+ #
266
+ # @return [void]
267
+ def apply_defaults_to_dir(dir)
268
+ defaults.config.values.each do |key, _|
269
+ if defaults.configured?(key) && !dir.configured?(key)
270
+ dir.public_send(:"#{key}=", defaults.public_send(key).dup)
271
+ end
272
+ end
273
+ end
274
+
275
+ def method_missing(name, *args, &block)
276
+ if defaults.respond_to?(name)
277
+ defaults.public_send(name, *args, &block)
278
+ else
279
+ super
280
+ end
281
+ end
282
+
283
+ def respond_to_missing?(name, include_all = false)
284
+ defaults.respond_to?(name) || super
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/system/constants"
4
+
5
+ module Dry
6
+ module System
7
+ module Config
8
+ # A configured namespace for a component dir
9
+ #
10
+ # Namespaces consist of three elements:
11
+ #
12
+ # - The `path` within the component dir to which its namespace rules should apply.
13
+ # - A `key`, which determines the leading part of the key used to register
14
+ # each component in the container.
15
+ # - A `const`, which is the Ruby namespace expected to contain the class constants
16
+ # defined within each component's source file. This value is expected to be an
17
+ # "underscored" string, intended to be run through the configured inflector to be
18
+ # converted into a real constant (e.g. `"foo_bar/baz"` will become `FooBar::Baz`)
19
+ #
20
+ # Namespaces are added and configured for a component dir via {Namespaces#add}.
21
+ #
22
+ # @see Namespaces#add
23
+ #
24
+ # @api public
25
+ class Namespace
26
+ ROOT_PATH = nil
27
+
28
+ include Dry::Equalizer(:path, :key, :const)
29
+
30
+ # @api public
31
+ attr_reader :path
32
+
33
+ # @api public
34
+ attr_reader :key
35
+
36
+ # @api public
37
+ attr_reader :const
38
+
39
+ # Returns a namespace configured to serve as the default root namespace for a
40
+ # component dir, ensuring that all code within the dir can be loaded, regardless
41
+ # of any other explictly configured namespaces
42
+ #
43
+ # @return [Namespace] the root namespace
44
+ #
45
+ # @api private
46
+ def self.default_root
47
+ new(
48
+ path: ROOT_PATH,
49
+ key: nil,
50
+ const: nil
51
+ )
52
+ end
53
+
54
+ # @api private
55
+ def initialize(path:, key:, const:)
56
+ @path = path
57
+ # Default keys (i.e. when the user does not explicitly provide one) for non-root
58
+ # paths will include path separators, which we must convert into key separators
59
+ @key = key && key == path ? key.gsub(PATH_SEPARATOR, KEY_SEPARATOR) : key
60
+ @const = const
61
+ end
62
+
63
+ # @api public
64
+ def root?
65
+ path == ROOT_PATH
66
+ end
67
+
68
+ # @api public
69
+ def path?
70
+ !root?
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end