dry-system 0.21.0 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +424 -0
  3. data/LICENSE +1 -1
  4. data/README.md +4 -4
  5. data/dry-system.gemspec +3 -4
  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 +75 -19
  10. data/lib/dry/system/config/component_dirs.rb +151 -32
  11. data/lib/dry/system/config/namespace.rb +11 -6
  12. data/lib/dry/system/config/namespaces.rb +96 -9
  13. data/lib/dry/system/constants.rb +1 -1
  14. data/lib/dry/system/container.rb +264 -182
  15. data/lib/dry/system/errors.rb +73 -53
  16. data/lib/dry/system/identifier.rb +62 -20
  17. data/lib/dry/system/importer.rb +83 -12
  18. data/lib/dry/system/indirect_component.rb +1 -1
  19. data/lib/dry/system/loader.rb +6 -1
  20. data/lib/dry/system/{manual_registrar.rb → manifest_registrar.rb} +8 -5
  21. data/lib/dry/system/plugins/bootsnap.rb +3 -2
  22. data/lib/dry/system/plugins/dependency_graph/strategies.rb +37 -1
  23. data/lib/dry/system/plugins/dependency_graph.rb +26 -20
  24. data/lib/dry/system/plugins/env.rb +2 -1
  25. data/lib/dry/system/plugins/logging.rb +2 -2
  26. data/lib/dry/system/plugins/monitoring.rb +1 -1
  27. data/lib/dry/system/plugins/notifications.rb +1 -1
  28. data/lib/dry/system/plugins/zeitwerk/compat_inflector.rb +22 -0
  29. data/lib/dry/system/plugins/zeitwerk.rb +109 -0
  30. data/lib/dry/system/plugins.rb +7 -4
  31. data/lib/dry/system/provider/source.rb +324 -0
  32. data/lib/dry/system/provider/source_dsl.rb +94 -0
  33. data/lib/dry/system/provider.rb +262 -22
  34. data/lib/dry/system/provider_registrar.rb +276 -0
  35. data/lib/dry/system/provider_source_registry.rb +70 -0
  36. data/lib/dry/system/provider_sources/settings/config.rb +86 -0
  37. data/lib/dry/system/provider_sources/settings/loader.rb +53 -0
  38. data/lib/dry/system/provider_sources/settings.rb +40 -0
  39. data/lib/dry/system/provider_sources.rb +5 -0
  40. data/lib/dry/system/version.rb +1 -1
  41. data/lib/dry/system.rb +44 -12
  42. metadata +23 -37
  43. data/lib/dry/system/booter/component_registry.rb +0 -35
  44. data/lib/dry/system/booter.rb +0 -200
  45. data/lib/dry/system/components/bootable.rb +0 -280
  46. data/lib/dry/system/components/config.rb +0 -35
  47. data/lib/dry/system/lifecycle.rb +0 -135
  48. data/lib/dry/system/provider_registry.rb +0 -27
  49. data/lib/dry/system/settings/file_loader.rb +0 -30
  50. data/lib/dry/system/settings/file_parser.rb +0 -51
  51. data/lib/dry/system/settings.rb +0 -64
  52. 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