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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +678 -0
- data/LICENSE +1 -1
- data/README.md +5 -4
- data/dry-system.gemspec +18 -21
- data/lib/dry/system/auto_registrar.rb +9 -64
- data/lib/dry/system/component.rb +124 -104
- data/lib/dry/system/component_dir.rb +171 -0
- data/lib/dry/system/config/component_dir.rb +228 -0
- data/lib/dry/system/config/component_dirs.rb +289 -0
- data/lib/dry/system/config/namespace.rb +75 -0
- data/lib/dry/system/config/namespaces.rb +196 -0
- data/lib/dry/system/constants.rb +2 -4
- data/lib/dry/system/container.rb +305 -345
- data/lib/dry/system/errors.rb +73 -56
- data/lib/dry/system/identifier.rb +176 -0
- data/lib/dry/system/importer.rb +89 -12
- data/lib/dry/system/indirect_component.rb +63 -0
- data/lib/dry/system/loader/autoloading.rb +24 -0
- data/lib/dry/system/loader.rb +49 -41
- data/lib/dry/system/{manual_registrar.rb → manifest_registrar.rb} +13 -14
- data/lib/dry/system/plugins/bootsnap.rb +3 -2
- data/lib/dry/system/plugins/dependency_graph/strategies.rb +38 -2
- data/lib/dry/system/plugins/dependency_graph.rb +25 -21
- data/lib/dry/system/plugins/env.rb +3 -2
- data/lib/dry/system/plugins/logging.rb +9 -8
- data/lib/dry/system/plugins/monitoring.rb +1 -2
- data/lib/dry/system/plugins/notifications.rb +1 -1
- data/lib/dry/system/plugins/plugin.rb +61 -0
- data/lib/dry/system/plugins/zeitwerk/compat_inflector.rb +22 -0
- data/lib/dry/system/plugins/zeitwerk.rb +109 -0
- data/lib/dry/system/plugins.rb +5 -73
- data/lib/dry/system/provider/source.rb +276 -0
- data/lib/dry/system/provider/source_dsl.rb +55 -0
- data/lib/dry/system/provider.rb +261 -23
- data/lib/dry/system/provider_registrar.rb +251 -0
- data/lib/dry/system/provider_source_registry.rb +56 -0
- data/lib/dry/system/provider_sources/settings/config.rb +73 -0
- data/lib/dry/system/provider_sources/settings/loader.rb +44 -0
- data/lib/dry/system/provider_sources/settings.rb +40 -0
- data/lib/dry/system/provider_sources.rb +5 -0
- data/lib/dry/system/stubs.rb +6 -2
- data/lib/dry/system/version.rb +1 -1
- data/lib/dry/system.rb +35 -13
- metadata +48 -97
- data/lib/dry/system/auto_registrar/configuration.rb +0 -43
- data/lib/dry/system/booter/component_registry.rb +0 -35
- data/lib/dry/system/booter.rb +0 -181
- data/lib/dry/system/components/bootable.rb +0 -289
- data/lib/dry/system/components/config.rb +0 -35
- data/lib/dry/system/components.rb +0 -8
- data/lib/dry/system/lifecycle.rb +0 -135
- data/lib/dry/system/provider_registry.rb +0 -27
- data/lib/dry/system/settings/file_loader.rb +0 -30
- data/lib/dry/system/settings/file_parser.rb +0 -51
- data/lib/dry/system/settings.rb +0 -67
- 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
|
+
)
|
data/lib/dry/system/stubs.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/dry/system/version.rb
CHANGED
data/lib/dry/system.rb
CHANGED
@@ -1,30 +1,52 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "dry/
|
3
|
+
require "zeitwerk"
|
4
|
+
require "dry/core"
|
5
5
|
|
6
6
|
module Dry
|
7
7
|
module System
|
8
|
-
#
|
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.
|
12
|
-
|
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
|
-
#
|
30
|
+
# Registers a provider source, which can be used as the basis for other providers
|
18
31
|
#
|
19
32
|
# @api public
|
20
|
-
def self.
|
21
|
-
|
22
|
-
|
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.
|
27
|
-
@
|
46
|
+
def self.provider_sources
|
47
|
+
@provider_sources ||= ProviderSourceRegistry.new
|
28
48
|
end
|
49
|
+
|
50
|
+
loader.setup
|
29
51
|
end
|
30
52
|
end
|