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,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ require "dry/system/errors"
6
+ require "dry/system/constants"
7
+
8
+ module Dry
9
+ module System
10
+ # Default provider registrar implementation
11
+ #
12
+ # This is currently configured by default for every Dry::System::Container. The
13
+ # provider registrar is responsible for loading provider files and exposing an API for
14
+ # running the provider lifecycle steps.
15
+ #
16
+ # @api private
17
+ class ProviderRegistrar
18
+ # @api private
19
+ attr_reader :providers
20
+
21
+ # @api private
22
+ attr_reader :container
23
+
24
+ # @api private
25
+ def initialize(container)
26
+ @providers = {}
27
+ @container = container
28
+ end
29
+
30
+ # @api private
31
+ def freeze
32
+ providers.freeze
33
+ super
34
+ end
35
+
36
+ # rubocop:disable Metrics/PerceivedComplexity
37
+
38
+ # @see Container.register_provider
39
+ # @api private
40
+ def register_provider(name, namespace: nil, from: nil, source: nil, if: true, &block)
41
+ raise ProviderAlreadyRegisteredError, name if providers.key?(name)
42
+
43
+ if from && source.is_a?(Class)
44
+ raise ArgumentError, "You must supply a block when using a provider source"
45
+ end
46
+
47
+ if block && source.is_a?(Class)
48
+ raise ArgumentError, "You must supply only a `source:` option or a block, not both"
49
+ end
50
+
51
+ return self unless binding.local_variable_get(:if)
52
+
53
+ provider =
54
+ if from
55
+ build_provider_from_source(
56
+ name,
57
+ namespace: namespace,
58
+ source: source || name,
59
+ group: from,
60
+ &block
61
+ )
62
+ else
63
+ build_provider(name, namespace: namespace, source: source, &block)
64
+ end
65
+
66
+ providers[provider.name] = provider
67
+
68
+ self
69
+ end
70
+
71
+ # rubocop:enable Metrics/PerceivedComplexity
72
+
73
+ # Returns a provider for the given name, if it has already been loaded
74
+ #
75
+ # @api public
76
+ def [](provider_name)
77
+ providers[provider_name]
78
+ end
79
+ alias_method :provider, :[]
80
+
81
+ # @api private
82
+ def key?(provider_name)
83
+ providers.key?(provider_name)
84
+ end
85
+
86
+ # Returns a provider if it can be found or loaded, otherwise nil
87
+ #
88
+ # @return [Dry::System::Provider, nil]
89
+ #
90
+ # @api private
91
+ def find_and_load_provider(name)
92
+ name = name.to_sym
93
+
94
+ if (provider = providers[name])
95
+ return provider
96
+ end
97
+
98
+ return if finalized?
99
+
100
+ require_provider_file(name)
101
+
102
+ providers[name]
103
+ end
104
+
105
+ # @api private
106
+ def start_provider_dependency(component)
107
+ if (provider = find_and_load_provider(component.root_key))
108
+ provider.start
109
+ end
110
+ end
111
+
112
+ # Returns all provider files within the configured provider_paths.
113
+ #
114
+ # Searches for files in the order of the configured provider_paths. In the case of multiple
115
+ # identically-named boot files within different provider_paths, the file found first will be
116
+ # returned, and other matching files will be discarded.
117
+ #
118
+ # This method is public to allow other tools extending dry-system (like dry-rails)
119
+ # to access a canonical list of real, in-use provider files.
120
+ #
121
+ # @see Container.provider_paths
122
+ #
123
+ # @return [Array<Pathname>]
124
+ # @api public
125
+ def provider_files
126
+ @provider_files ||= provider_paths.each_with_object([[], []]) { |path, (provider_files, loaded)| # rubocop:disable Layout/LineLength
127
+ files = Dir["#{path}/#{RB_GLOB}"].sort
128
+
129
+ files.each do |file|
130
+ basename = File.basename(file)
131
+
132
+ unless loaded.include?(basename)
133
+ provider_files << Pathname(file)
134
+ loaded << basename
135
+ end
136
+ end
137
+ }.first
138
+ end
139
+
140
+ # @api private
141
+ def finalize!
142
+ provider_files.each do |path|
143
+ load_provider(path)
144
+ end
145
+
146
+ providers.each_value(&:start)
147
+
148
+ freeze
149
+ end
150
+
151
+ # @!method finalized?
152
+ # Returns true if the booter has been finalized
153
+ #
154
+ # @return [Boolean]
155
+ # @api private
156
+ alias_method :finalized?, :frozen?
157
+
158
+ # @api private
159
+ def shutdown
160
+ providers.each_value(&:stop)
161
+ self
162
+ end
163
+
164
+ # @api private
165
+ def prepare(provider_name)
166
+ with_provider(provider_name, &:prepare)
167
+ self
168
+ end
169
+
170
+ # @api private
171
+ def start(provider_name)
172
+ with_provider(provider_name, &:start)
173
+ self
174
+ end
175
+
176
+ # @api private
177
+ def stop(provider_name)
178
+ with_provider(provider_name, &:stop)
179
+ self
180
+ end
181
+
182
+ private
183
+
184
+ # @api private
185
+ def provider_paths
186
+ provider_dirs = container.config.provider_dirs
187
+
188
+ provider_dirs.map { |dir|
189
+ dir = Pathname(dir)
190
+
191
+ if dir.relative?
192
+ container.root.join(dir)
193
+ else
194
+ dir
195
+ end
196
+ }
197
+ end
198
+
199
+ def build_provider(name, namespace:, source: nil, &block)
200
+ source_class = source || Provider::Source.for(name: name, &block)
201
+
202
+ Provider.new(
203
+ name: name,
204
+ namespace: namespace,
205
+ target_container: container,
206
+ source_class: source_class
207
+ )
208
+ end
209
+
210
+ def build_provider_from_source(name, source:, group:, namespace:, &block)
211
+ source_class = System.provider_sources.resolve(name: source, group: group)
212
+
213
+ Provider.new(
214
+ name: name,
215
+ namespace: namespace,
216
+ target_container: container,
217
+ source_class: source_class,
218
+ &block
219
+ )
220
+ end
221
+
222
+ def with_provider(provider_name)
223
+ require_provider_file(provider_name) unless providers.key?(provider_name)
224
+
225
+ provider = providers[provider_name]
226
+
227
+ raise ProviderNotFoundError, provider_name unless provider
228
+
229
+ yield(provider)
230
+ end
231
+
232
+ def load_provider(path)
233
+ name = Pathname(path).basename(RB_EXT).to_s.to_sym
234
+
235
+ Kernel.require path unless providers.key?(name)
236
+
237
+ self
238
+ end
239
+
240
+ def require_provider_file(name)
241
+ provider_file = find_provider_file(name)
242
+
243
+ Kernel.require provider_file if provider_file
244
+ end
245
+
246
+ def find_provider_file(name)
247
+ provider_files.detect { |file| File.basename(file, RB_EXT) == name.to_s }
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/system/constants"
4
+
5
+ module Dry
6
+ module System
7
+ # @api private
8
+ class ProviderSourceRegistry
9
+ attr_reader :sources
10
+
11
+ def initialize
12
+ @sources = {}
13
+ end
14
+
15
+ def load_sources(path)
16
+ Dir[File.join(path, "**/#{RB_GLOB}")].sort.each do |file|
17
+ require file
18
+ end
19
+ end
20
+
21
+ def register(name:, group:, source:)
22
+ sources[key(name, group)] = source
23
+ end
24
+
25
+ def register_from_block(name:, group:, &block)
26
+ register(
27
+ name: name,
28
+ group: group,
29
+ source: Provider::Source.for(
30
+ name: name,
31
+ group: group,
32
+ &block
33
+ )
34
+ )
35
+ end
36
+
37
+ def resolve(name:, group:)
38
+ sources[key(name, group)].tap { |source|
39
+ unless source
40
+ raise ProviderSourceNotFoundError.new(
41
+ name: name,
42
+ group: group,
43
+ keys: sources.keys
44
+ )
45
+ end
46
+ }
47
+ end
48
+
49
+ private
50
+
51
+ def key(name, group)
52
+ {group: group, name: name}
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module System
5
+ module ProviderSources
6
+ # @api private
7
+ module Settings
8
+ InvalidSettingsError = Class.new(ArgumentError) do
9
+ # @api private
10
+ def initialize(errors)
11
+ message = <<~STR
12
+ Could not load settings. The following settings were invalid:
13
+
14
+ #{setting_errors(errors).join("\n")}
15
+ STR
16
+
17
+ super(message)
18
+ end
19
+
20
+ private
21
+
22
+ def setting_errors(errors)
23
+ errors.sort_by { |k, _| k }.map { |key, error| "#{key}: #{error}" }
24
+ end
25
+ end
26
+
27
+ # @api private
28
+ class Config
29
+ # @api private
30
+ def self.load(root:, env:, loader: Loader)
31
+ loader = loader.new(root: root, env: env)
32
+
33
+ new.tap do |settings_obj|
34
+ errors = {}
35
+
36
+ settings.to_a.each do |setting|
37
+ value = loader[setting.name.to_s.upcase]
38
+
39
+ begin
40
+ if value
41
+ settings_obj.config.public_send(:"#{setting.name}=", value)
42
+ else
43
+ settings_obj.config[setting.name]
44
+ end
45
+ rescue => e # rubocop:disable Style/RescueStandardError
46
+ errors[setting.name] = e
47
+ end
48
+ end
49
+
50
+ raise InvalidSettingsError, errors unless errors.empty?
51
+ end
52
+ end
53
+
54
+ include Dry::Configurable
55
+
56
+ private
57
+
58
+ def method_missing(name, *args, &block)
59
+ if config.respond_to?(name)
60
+ config.public_send(name, *args, &block)
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ def respond_to_missing?(name, include_all = false)
67
+ config.respond_to?(name, include_all) || super
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module System
5
+ module ProviderSources
6
+ module Settings
7
+ # @api private
8
+ class Loader
9
+ # @api private
10
+ attr_reader :store
11
+
12
+ # @api private
13
+ def initialize(root:, env:, store: ENV)
14
+ @store = store
15
+ load_dotenv(root, env.to_sym)
16
+ end
17
+
18
+ # @api private
19
+ def [](key)
20
+ store[key]
21
+ end
22
+
23
+ private
24
+
25
+ def load_dotenv(root, env)
26
+ require "dotenv"
27
+ Dotenv.load(*dotenv_files(root, env)) if defined?(Dotenv)
28
+ rescue LoadError
29
+ # Do nothing if dotenv is unavailable
30
+ end
31
+
32
+ def dotenv_files(root, env)
33
+ [
34
+ File.join(root, ".env.#{env}.local"),
35
+ (File.join(root, ".env.local") unless env == :test),
36
+ File.join(root, ".env.#{env}"),
37
+ File.join(root, ".env")
38
+ ].compact
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module System
5
+ module ProviderSources
6
+ module Settings
7
+ class Source < Dry::System::Provider::Source
8
+ setting :store
9
+
10
+ def prepare
11
+ require "dry/system/provider_sources/settings/config"
12
+ end
13
+
14
+ def start
15
+ register(:settings, settings.load(root: target.root, env: target.config.env))
16
+ end
17
+
18
+ def settings(&block)
19
+ # Save the block and evaluate it lazily to allow a provider with this source
20
+ # to `require` any necessary files for the block to evaluate correctly (e.g.
21
+ # requiring an app-specific types module for setting constructors)
22
+ if block
23
+ @settings_block = block
24
+ elsif defined? @settings_class
25
+ @settings_class
26
+ elsif @settings_block
27
+ @settings_class = Class.new(Settings::Config, &@settings_block)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ Dry::System.register_provider_source(
37
+ :settings,
38
+ group: :dry_system,
39
+ source: Dry::System::ProviderSources::Settings::Source
40
+ )
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/system"
4
+
5
+ Dry::System.register_provider_sources Pathname(__dir__).join("provider_sources").realpath
@@ -1,13 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/container/stub"
3
+ require "dry/core/container/stub"
4
4
 
5
5
  module Dry
6
6
  module System
7
7
  class Container
8
8
  # @api private
9
9
  module Stubs
10
- def finalize!(&block)
10
+ # This overrides default finalize! just to disable automatic freezing
11
+ # of the container
12
+ #
13
+ # @api private
14
+ def finalize!(**, &block)
11
15
  super(freeze: false, &block)
12
16
  end
13
17
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module System
5
- VERSION = "0.18.1"
5
+ VERSION = "1.0.1"
6
6
  end
7
7
  end
data/lib/dry/system.rb CHANGED
@@ -1,30 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/system/provider"
4
- require "dry/system/provider_registry"
3
+ require "zeitwerk"
4
+ require "dry/core"
5
5
 
6
6
  module Dry
7
7
  module System
8
- # Register external component provider
8
+ # @api private
9
+ def self.loader
10
+ @loader ||= Zeitwerk::Loader.new.tap do |loader|
11
+ root = File.expand_path("..", __dir__)
12
+ loader.tag = "dry-system"
13
+ loader.inflector = Zeitwerk::GemInflector.new("#{root}/dry-system.rb")
14
+ loader.push_dir(root)
15
+ loader.ignore(
16
+ "#{root}/dry-system.rb",
17
+ "#{root}/dry/system/{components,constants,errors,stubs,version}.rb"
18
+ )
19
+ loader.inflector.inflect("source_dsl" => "SourceDSL")
20
+ end
21
+ end
22
+
23
+ # Registers the provider sources in the files under the given path
9
24
  #
10
25
  # @api public
11
- def self.register_provider(identifier, options)
12
- providers.register(identifier, options)
13
- providers[identifier].load_components
14
- self
26
+ def self.register_provider_sources(path)
27
+ provider_sources.load_sources(path)
15
28
  end
16
29
 
17
- # Register an external component that can be booted within other systems
30
+ # Registers a provider source, which can be used as the basis for other providers
18
31
  #
19
32
  # @api public
20
- def self.register_component(identifier, provider:, &block)
21
- providers[provider].register_component(identifier, block)
22
- self
33
+ def self.register_provider_source(name, group:, source: nil, &block)
34
+ if source && block
35
+ raise ArgumentError, "You must supply only a `source:` option or a block, not both"
36
+ end
37
+
38
+ if source
39
+ provider_sources.register(name: name, group: group, source: source)
40
+ else
41
+ provider_sources.register_from_block(name: name, group: group, &block)
42
+ end
23
43
  end
24
44
 
25
45
  # @api private
26
- def self.providers
27
- @providers ||= ProviderRegistry.new
46
+ def self.provider_sources
47
+ @provider_sources ||= ProviderSourceRegistry.new
28
48
  end
49
+
50
+ loader.setup
29
51
  end
30
52
  end