dry-system 0.18.1 → 1.0.1

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 (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
@@ -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,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "dry/system"
4
-
5
- Dry::System.register_provider(
6
- :system,
7
- boot_path: Pathname(__dir__).join("system_components").realpath
8
- )
@@ -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