dry-system 0.22.0 → 0.23.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +400 -0
  3. data/LICENSE +1 -1
  4. data/README.md +2 -2
  5. data/dry-system.gemspec +2 -2
  6. data/lib/dry/system/component.rb +2 -3
  7. data/lib/dry/system/component_dir.rb +8 -34
  8. data/lib/dry/system/components.rb +8 -4
  9. data/lib/dry/system/config/component_dir.rb +60 -16
  10. data/lib/dry/system/config/component_dirs.rb +23 -10
  11. data/lib/dry/system/config/namespace.rb +4 -6
  12. data/lib/dry/system/constants.rb +1 -1
  13. data/lib/dry/system/container.rb +264 -182
  14. data/lib/dry/system/errors.rb +73 -53
  15. data/lib/dry/system/identifier.rb +62 -20
  16. data/lib/dry/system/importer.rb +83 -12
  17. data/lib/dry/system/indirect_component.rb +1 -1
  18. data/lib/dry/system/loader.rb +6 -1
  19. data/lib/dry/system/{manual_registrar.rb → manifest_registrar.rb} +8 -5
  20. data/lib/dry/system/plugins/bootsnap.rb +2 -1
  21. data/lib/dry/system/plugins/dependency_graph/strategies.rb +37 -1
  22. data/lib/dry/system/plugins/dependency_graph.rb +26 -20
  23. data/lib/dry/system/plugins/env.rb +2 -1
  24. data/lib/dry/system/plugins/logging.rb +2 -2
  25. data/lib/dry/system/plugins/monitoring.rb +1 -1
  26. data/lib/dry/system/plugins/notifications.rb +1 -1
  27. data/lib/dry/system/plugins/zeitwerk/compat_inflector.rb +22 -0
  28. data/lib/dry/system/plugins/zeitwerk.rb +109 -0
  29. data/lib/dry/system/plugins.rb +7 -4
  30. data/lib/dry/system/provider/source.rb +324 -0
  31. data/lib/dry/system/provider/source_dsl.rb +94 -0
  32. data/lib/dry/system/provider.rb +262 -22
  33. data/lib/dry/system/provider_registrar.rb +276 -0
  34. data/lib/dry/system/provider_source_registry.rb +70 -0
  35. data/lib/dry/system/provider_sources/settings/config.rb +86 -0
  36. data/lib/dry/system/provider_sources/settings/loader.rb +53 -0
  37. data/lib/dry/system/provider_sources/settings.rb +40 -0
  38. data/lib/dry/system/provider_sources.rb +5 -0
  39. data/lib/dry/system/version.rb +1 -1
  40. data/lib/dry/system.rb +44 -12
  41. metadata +18 -18
  42. data/lib/dry/system/booter/component_registry.rb +0 -35
  43. data/lib/dry/system/booter.rb +0 -200
  44. data/lib/dry/system/components/bootable.rb +0 -280
  45. data/lib/dry/system/components/config.rb +0 -35
  46. data/lib/dry/system/lifecycle.rb +0 -135
  47. data/lib/dry/system/provider_registry.rb +0 -27
  48. data/lib/dry/system/settings/file_loader.rb +0 -30
  49. data/lib/dry/system/settings/file_parser.rb +0 -51
  50. data/lib/dry/system/settings.rb +0 -64
  51. data/lib/dry/system/system_components/settings.rb +0 -11
@@ -1,280 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "dry/system/lifecycle"
4
- require "dry/system/settings"
5
- require "dry/system/components/config"
6
- require "dry/system/constants"
7
-
8
- module Dry
9
- module System
10
- module Components
11
- # Bootable components can provide one or more objects and typically depend
12
- # on 3rd-party code. A typical bootable component can be a database library,
13
- # or an API client.
14
- #
15
- # These components can be registered via `Container.boot` and external component
16
- # providers can register their components too, which then can be used and configured
17
- # by your system.
18
- #
19
- # @example simple logger
20
- # class App < Dry::System::Container
21
- # boot(:logger) do
22
- # init do
23
- # require "logger"
24
- # end
25
- #
26
- # start do
27
- # register(:logger, Logger.new($stdout))
28
- # end
29
- # end
30
- # end
31
- #
32
- # App[:logger] # returns configured logger
33
- #
34
- # @example using built-in system components
35
- # class App < Dry::System::Container
36
- # boot(:settings, from: :system) do
37
- # settings do
38
- # key :database_url, Types::String.constrained(filled: true)
39
- # key :session_secret, Types::String.constrained(filled: true)
40
- # end
41
- # end
42
- # end
43
- #
44
- # App[:settings] # returns loaded settings
45
- #
46
- # @api public
47
- class Bootable
48
- DEFAULT_FINALIZE = proc {}
49
-
50
- # @!attribute [r] key
51
- # @return [Symbol] component's unique name
52
- attr_reader :name
53
-
54
- # @!attribute [r] options
55
- # @return [Hash] component's options
56
- attr_reader :options
57
-
58
- # @!attribute [r] triggers
59
- # @return [Hash] lifecycle step after/before callbacks
60
- attr_reader :triggers
61
-
62
- # @!attribute [r] namespace
63
- # @return [Symbol,String] default namespace for the container keys
64
- attr_reader :namespace
65
-
66
- TRIGGER_MAP = Hash.new { |h, k| h[k] = [] }.freeze
67
-
68
- # @api private
69
- def initialize(name, options = {}, &block)
70
- @config = nil
71
- @config_block = nil
72
- @name = name
73
- @triggers = {before: TRIGGER_MAP.dup, after: TRIGGER_MAP.dup}
74
- @options = block ? options.merge(block: block) : options
75
- @namespace = options[:namespace]
76
- finalize = options[:finalize] || DEFAULT_FINALIZE
77
- instance_exec(&finalize)
78
- end
79
-
80
- # Execute `init` step
81
- #
82
- # @return [Bootable]
83
- #
84
- # @api public
85
- def init
86
- trigger(:before, :init)
87
- lifecycle.(:init)
88
- trigger(:after, :init)
89
- self
90
- end
91
-
92
- # Execute `start` step
93
- #
94
- # @return [Bootable]
95
- #
96
- # @api public
97
- def start
98
- trigger(:before, :start)
99
- lifecycle.(:start)
100
- trigger(:after, :start)
101
- self
102
- end
103
-
104
- # Execute `stop` step
105
- #
106
- # @return [Bootable]
107
- #
108
- # @api public
109
- def stop
110
- lifecycle.(:stop)
111
- self
112
- end
113
-
114
- # Specify a before callback
115
- #
116
- # @return [Bootable]
117
- #
118
- # @api public
119
- def before(event, &block)
120
- triggers[:before][event] << block
121
- self
122
- end
123
-
124
- # Specify an after callback
125
- #
126
- # @return [Bootable]
127
- #
128
- # @api public
129
- def after(event, &block)
130
- triggers[:after][event] << block
131
- self
132
- end
133
-
134
- # Configure a component
135
- #
136
- # @return [Bootable]
137
- #
138
- # @api public
139
- def configure(&block)
140
- @config_block = block
141
- end
142
-
143
- # Define configuration settings with keys and types
144
- #
145
- # @return [Bootable]
146
- #
147
- # @api public
148
- def settings(&block)
149
- if block
150
- @settings_block = block
151
- elsif @settings_block
152
- @settings = Settings::DSL.new(&@settings_block).call
153
- else
154
- @settings
155
- end
156
- end
157
-
158
- # Return component's configuration
159
- #
160
- # @return [Dry::Struct]
161
- #
162
- # @api public
163
- def config
164
- @config || configure!
165
- end
166
-
167
- # Return a list of lifecycle steps that were executed
168
- #
169
- # @return [Array<Symbol>]
170
- #
171
- # @api public
172
- def statuses
173
- lifecycle.statuses
174
- end
175
-
176
- # Return system's container used by this component
177
- #
178
- # @return [Dry::Struct]
179
- #
180
- # @api public
181
- def container
182
- options.fetch(:container)
183
- end
184
-
185
- # Automatically called by the booter object after starting a component
186
- #
187
- # @return [Bootable]
188
- #
189
- # @api private
190
- def finalize
191
- lifecycle.container.each do |key, item|
192
- container.register(key, item) unless container.registered?(key)
193
- end
194
- self
195
- end
196
-
197
- # Trigger a callback
198
- #
199
- # @return [Bootable]
200
- #
201
- # @api private
202
- def trigger(key, event)
203
- triggers[key][event].each do |fn|
204
- container.instance_exec(lifecycle.container, &fn)
205
- end
206
- self
207
- end
208
-
209
- # Return a new instance with updated name and options
210
- #
211
- # @return [Dry::Struct]
212
- #
213
- # @api private
214
- def new(name, new_options = EMPTY_HASH)
215
- self.class.new(name, options.merge(new_options))
216
- end
217
-
218
- # Return a new instance with updated options
219
- #
220
- # @return [Dry::Struct]
221
- #
222
- # @api private
223
- def with(new_options)
224
- self.class.new(name, options.merge(new_options))
225
- end
226
-
227
- private
228
-
229
- # Return lifecycle object used for this component
230
- #
231
- # @return [Lifecycle]
232
- #
233
- # @api private
234
- def lifecycle
235
- @lifecycle ||= Lifecycle.new(lf_container, component: self, &block)
236
- end
237
-
238
- # Return configured container for the lifecycle object
239
- #
240
- # @return [Dry::Container]
241
- #
242
- # @api private
243
- def lf_container
244
- container = Dry::Container.new
245
-
246
- case namespace
247
- when String, Symbol
248
- container.namespace(namespace) { |c| return c }
249
- when true
250
- container.namespace(name) { |c| return c }
251
- when nil
252
- container
253
- else
254
- raise <<-STR
255
- +namespace+ boot option must be true, string or symbol #{namespace.inspect} given.
256
- STR
257
- end
258
- end
259
-
260
- # Set config object
261
- #
262
- # @return [Dry::Struct]
263
- #
264
- # @api private
265
- def configure!
266
- @config = settings.new(Config.new(&@config_block)) if settings
267
- end
268
-
269
- # Return block that will be evaluated in the lifecycle context
270
- #
271
- # @return [Proc]
272
- #
273
- # @api private
274
- def block
275
- options.fetch(:block)
276
- end
277
- end
278
- end
279
- end
280
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dry
4
- module System
5
- module Components
6
- class Config
7
- def self.new(&block)
8
- config = super
9
- yield(config) if block_given?
10
- config
11
- end
12
-
13
- def initialize
14
- @settings = {}
15
- end
16
-
17
- def to_hash
18
- @settings
19
- end
20
-
21
- private
22
-
23
- def method_missing(meth, value = nil)
24
- if meth.to_s.end_with?("=")
25
- @settings[meth.to_s.gsub("=", "").to_sym] = value
26
- elsif @settings.key?(meth)
27
- @settings[meth]
28
- else
29
- super
30
- end
31
- end
32
- end
33
- end
34
- end
35
- end
@@ -1,135 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "concurrent/map"
4
-
5
- require "dry/system/settings"
6
-
7
- module Dry
8
- module System
9
- # Lifecycle booting DSL
10
- #
11
- # Lifecycle objects are used in the boot files where you can register custom
12
- # init/start/stop triggers
13
- #
14
- # @see [Container.finalize]
15
- #
16
- # @api private
17
- class Lifecycle < BasicObject
18
- attr_reader :container
19
-
20
- attr_reader :statuses
21
-
22
- attr_reader :triggers
23
-
24
- attr_reader :opts
25
-
26
- # @api private
27
- def self.new(container, opts = {}, &block)
28
- cache.fetch_or_store([container, opts, block].hash) do
29
- super
30
- end
31
- end
32
-
33
- # @api private
34
- def self.cache
35
- @cache ||= ::Concurrent::Map.new
36
- end
37
-
38
- # @api private
39
- def initialize(container, opts, &block)
40
- @container = container
41
- @settings = nil
42
- @statuses = []
43
- @triggers = {}
44
- @opts = opts
45
- instance_exec(target, &block)
46
- end
47
-
48
- # @api private
49
- def call(*triggers)
50
- triggers.each do |trigger|
51
- unless statuses.include?(trigger)
52
- __send__(trigger)
53
- statuses << trigger
54
- end
55
- end
56
- end
57
-
58
- # @api private
59
- def settings(&block)
60
- component.settings(&block)
61
- end
62
-
63
- # @api private
64
- def configure(&block)
65
- component.configure(&block)
66
- end
67
-
68
- # @api private
69
- def config
70
- component.config
71
- end
72
-
73
- # @api private
74
- def init(&block)
75
- trigger!(:init, &block)
76
- end
77
-
78
- # @api private
79
- def start(&block)
80
- trigger!(:start, &block)
81
- end
82
-
83
- # @api private
84
- def stop(&block)
85
- trigger!(:stop, &block)
86
- end
87
-
88
- # @api private
89
- def use(*names)
90
- names.each do |name|
91
- target.start(name)
92
- end
93
- end
94
-
95
- # @api private
96
- def register(*args, &block)
97
- container.register(*args, &block)
98
- end
99
-
100
- # @api private
101
- def component
102
- opts[:component]
103
- end
104
-
105
- # @api private
106
- def target
107
- component.container
108
- end
109
-
110
- private
111
-
112
- # @api private
113
- def trigger!(name, &block)
114
- if triggers.key?(name)
115
- triggers[name].(target)
116
- elsif block
117
- triggers[name] = block
118
- end
119
- end
120
-
121
- # @api private
122
- def method_missing(meth, *args, &block)
123
- if target.registered?(meth)
124
- target[meth]
125
- elsif container.key?(meth)
126
- container[meth]
127
- elsif ::Kernel.respond_to?(meth)
128
- ::Kernel.public_send(meth, *args, &block)
129
- else
130
- super
131
- end
132
- end
133
- end
134
- end
135
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dry
4
- module System
5
- class ProviderRegistry
6
- include Enumerable
7
-
8
- attr_reader :items
9
-
10
- def initialize
11
- @items = []
12
- end
13
-
14
- def each(&block)
15
- items.each(&block)
16
- end
17
-
18
- def register(name, options)
19
- items << Provider.new(name, options)
20
- end
21
-
22
- def [](name)
23
- detect { |provider| provider.name == name }
24
- end
25
- end
26
- end
27
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "dry/system/settings/file_parser"
4
-
5
- module Dry
6
- module System
7
- module Settings
8
- class FileLoader
9
- def call(root, env)
10
- files(root, env).reduce({}) do |hash, file|
11
- hash.merge(parser.(file))
12
- end
13
- end
14
-
15
- private
16
-
17
- def parser
18
- @parser ||= FileParser.new
19
- end
20
-
21
- def files(root, env)
22
- [
23
- root.join(".env"),
24
- root.join(".env.#{env}")
25
- ].compact
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dry
4
- module System
5
- module Settings
6
- class FileParser
7
- # Regex extracted from dotenv gem
8
- # https://github.com/bkeepers/dotenv/blob/master/lib/dotenv/parser.rb#L14
9
- LINE = /
10
- \A
11
- \s*
12
- (?:export\s+)? # optional export
13
- ([\w\.]+) # key
14
- (?:\s*=\s*|:\s+?) # separator
15
- ( # optional value begin
16
- '(?:\'|[^'])*' # single quoted value
17
- | # or
18
- "(?:\"|[^"])*" # double quoted value
19
- | # or
20
- [^#\n]+ # unquoted value
21
- )? # value end
22
- \s*
23
- (?:\#.*)? # optional comment
24
- \z
25
- /x.freeze
26
-
27
- def call(file)
28
- File.readlines(file).each_with_object({}) do |line, hash|
29
- parse_line(line, hash)
30
- end
31
- rescue Errno::ENOENT
32
- {}
33
- end
34
-
35
- private
36
-
37
- def parse_line(line, hash)
38
- if (match = line.match(LINE))
39
- key, value = match.captures
40
- hash[key] = parse_value(value || "")
41
- end
42
- hash
43
- end
44
-
45
- def parse_value(value)
46
- value.strip.sub(/\A(['"])(.*)\1\z/, '\2')
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "dry/core/class_builder"
4
- require "dry/types"
5
- require "dry/struct"
6
-
7
- require "dry/system/settings/file_loader"
8
- require "dry/system/constants"
9
-
10
- module Dry
11
- module System
12
- module Settings
13
- class DSL < BasicObject
14
- attr_reader :schema
15
-
16
- def initialize(&block)
17
- @schema = {}
18
- instance_eval(&block)
19
- end
20
-
21
- def call
22
- Core::ClassBuilder.new(name: "Configuration", parent: Configuration).call do |klass|
23
- schema.each do |key, type|
24
- klass.setting(key, type)
25
- end
26
- end
27
- end
28
-
29
- def key(name, type)
30
- schema[name] = type
31
- end
32
- end
33
-
34
- class Configuration < Dry::Struct
35
- def self.setting(*args)
36
- attribute(*args)
37
- end
38
-
39
- def self.init(root, env)
40
- env_data = load_files(root, env)
41
- attributes = {}
42
- errors = {}
43
-
44
- schema.each do |key|
45
- value = ENV.fetch(key.name.to_s.upcase) { env_data[key.name.to_s.upcase] }
46
- type_check = key.try(value || Undefined)
47
-
48
- attributes[key.name] = value if value
49
- errors[key] = type_check if type_check.failure?
50
- end
51
-
52
- raise InvalidSettingsError, errors unless errors.empty?
53
-
54
- new(attributes)
55
- end
56
-
57
- def self.load_files(root, env)
58
- FileLoader.new.(root, env)
59
- end
60
- private_class_method :load_files
61
- end
62
- end
63
- end
64
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- Dry::System.register_component(:settings, provider: :system) do
4
- init do
5
- require "dry/system/settings"
6
- end
7
-
8
- start do
9
- register(:settings, settings.init(target.root, target.config.env))
10
- end
11
- end