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,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