dry-system 0.19.2 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +472 -1
  3. data/LICENSE +1 -1
  4. data/README.md +4 -3
  5. data/dry-system.gemspec +16 -15
  6. data/lib/dry/system/auto_registrar.rb +1 -13
  7. data/lib/dry/system/component.rb +104 -47
  8. data/lib/dry/system/component_dir.rb +88 -47
  9. data/lib/dry/system/components.rb +8 -4
  10. data/lib/dry/system/config/component_dir.rb +141 -53
  11. data/lib/dry/system/config/component_dirs.rb +176 -70
  12. data/lib/dry/system/config/namespace.rb +76 -0
  13. data/lib/dry/system/config/namespaces.rb +208 -0
  14. data/lib/dry/system/constants.rb +2 -2
  15. data/lib/dry/system/container.rb +279 -201
  16. data/lib/dry/system/errors.rb +72 -61
  17. data/lib/dry/system/identifier.rb +99 -79
  18. data/lib/dry/system/importer.rb +83 -12
  19. data/lib/dry/system/indirect_component.rb +65 -0
  20. data/lib/dry/system/loader.rb +8 -4
  21. data/lib/dry/system/{manual_registrar.rb → manifest_registrar.rb} +12 -13
  22. data/lib/dry/system/plugins/bootsnap.rb +3 -2
  23. data/lib/dry/system/plugins/dependency_graph/strategies.rb +37 -1
  24. data/lib/dry/system/plugins/dependency_graph.rb +26 -20
  25. data/lib/dry/system/plugins/env.rb +3 -2
  26. data/lib/dry/system/plugins/logging.rb +9 -5
  27. data/lib/dry/system/plugins/monitoring.rb +1 -1
  28. data/lib/dry/system/plugins/notifications.rb +1 -1
  29. data/lib/dry/system/plugins/zeitwerk/compat_inflector.rb +22 -0
  30. data/lib/dry/system/plugins/zeitwerk.rb +109 -0
  31. data/lib/dry/system/plugins.rb +8 -7
  32. data/lib/dry/system/provider/source.rb +324 -0
  33. data/lib/dry/system/provider/source_dsl.rb +94 -0
  34. data/lib/dry/system/provider.rb +264 -24
  35. data/lib/dry/system/provider_registrar.rb +276 -0
  36. data/lib/dry/system/provider_source_registry.rb +70 -0
  37. data/lib/dry/system/provider_sources/settings/config.rb +86 -0
  38. data/lib/dry/system/provider_sources/settings/loader.rb +53 -0
  39. data/lib/dry/system/provider_sources/settings.rb +40 -0
  40. data/lib/dry/system/provider_sources.rb +5 -0
  41. data/lib/dry/system/stubs.rb +1 -1
  42. data/lib/dry/system/version.rb +1 -1
  43. data/lib/dry/system.rb +45 -13
  44. metadata +25 -22
  45. data/lib/dry/system/booter/component_registry.rb +0 -35
  46. data/lib/dry/system/booter.rb +0 -200
  47. data/lib/dry/system/components/bootable.rb +0 -289
  48. data/lib/dry/system/components/config.rb +0 -35
  49. data/lib/dry/system/lifecycle.rb +0 -135
  50. data/lib/dry/system/provider_registry.rb +0 -27
  51. data/lib/dry/system/settings/file_loader.rb +0 -30
  52. data/lib/dry/system/settings/file_parser.rb +0 -51
  53. data/lib/dry/system/settings.rb +0 -67
  54. data/lib/dry/system/system_components/settings.rb +0 -11
@@ -1,289 +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] identifier
51
- # @return [Symbol] component's unique identifier
52
- attr_reader :identifier
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(identifier, options = {}, &block)
70
- @config = nil
71
- @config_block = nil
72
- @identifier = identifier
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(identifier, &@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(identifier, new_options = EMPTY_HASH)
215
- self.class.new(identifier, 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(identifier, options.merge(new_options))
225
- end
226
-
227
- # Return true
228
- #
229
- # @return [TrueClass]
230
- #
231
- # @api private
232
- def bootable?
233
- true
234
- end
235
-
236
- private
237
-
238
- # Return lifecycle object used for this component
239
- #
240
- # @return [Lifecycle]
241
- #
242
- # @api private
243
- def lifecycle
244
- @lifecycle ||= Lifecycle.new(lf_container, component: self, &block)
245
- end
246
-
247
- # Return configured container for the lifecycle object
248
- #
249
- # @return [Dry::Container]
250
- #
251
- # @api private
252
- def lf_container
253
- container = Dry::Container.new
254
-
255
- case namespace
256
- when String, Symbol
257
- container.namespace(namespace) { |c| return c }
258
- when true
259
- container.namespace(identifier) { |c| return c }
260
- when nil
261
- container
262
- else
263
- raise <<-STR
264
- +namespace+ boot option must be true, string or symbol #{namespace.inspect} given.
265
- STR
266
- end
267
- end
268
-
269
- # Set config object
270
- #
271
- # @return [Dry::Struct]
272
- #
273
- # @api private
274
- def configure!
275
- @config = settings.new(Config.new(&@config_block)) if settings
276
- end
277
-
278
- # Return block that will be evaluated in the lifecycle context
279
- #
280
- # @return [Proc]
281
- #
282
- # @api private
283
- def block
284
- options.fetch(:block)
285
- end
286
- end
287
- end
288
- end
289
- 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(identifier, options)
19
- items << Provider.new(identifier, options)
20
- end
21
-
22
- def [](identifier)
23
- detect { |provider| provider.identifier == identifier }
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,67 +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 :identifier
15
-
16
- attr_reader :schema
17
-
18
- def initialize(identifier, &block)
19
- @identifier = identifier
20
- @schema = {}
21
- instance_eval(&block)
22
- end
23
-
24
- def call
25
- Core::ClassBuilder.new(name: "Configuration", parent: Configuration).call do |klass|
26
- schema.each do |key, type|
27
- klass.setting(key, type)
28
- end
29
- end
30
- end
31
-
32
- def key(name, type)
33
- schema[name] = type
34
- end
35
- end
36
-
37
- class Configuration < Dry::Struct
38
- def self.setting(*args)
39
- attribute(*args)
40
- end
41
-
42
- def self.init(root, env)
43
- env_data = load_files(root, env)
44
- attributes = {}
45
- errors = {}
46
-
47
- schema.each do |key|
48
- value = ENV.fetch(key.name.to_s.upcase) { env_data[key.name.to_s.upcase] }
49
- type_check = key.try(value || Undefined)
50
-
51
- attributes[key.name] = value if value
52
- errors[key] = type_check if type_check.failure?
53
- end
54
-
55
- raise InvalidSettingsError, errors unless errors.empty?
56
-
57
- new(attributes)
58
- end
59
-
60
- def self.load_files(root, env)
61
- FileLoader.new.(root, env)
62
- end
63
- private_class_method :load_files
64
- end
65
- end
66
- end
67
- 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