dry-system 0.15.0 → 0.19.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +142 -2
  3. data/LICENSE +1 -1
  4. data/README.md +1 -1
  5. data/dry-system.gemspec +5 -4
  6. data/lib/dry-system.rb +1 -1
  7. data/lib/dry/system.rb +2 -2
  8. data/lib/dry/system/auto_registrar.rb +17 -59
  9. data/lib/dry/system/booter.rb +68 -41
  10. data/lib/dry/system/component.rb +62 -100
  11. data/lib/dry/system/component_dir.rb +128 -0
  12. data/lib/dry/system/components.rb +2 -2
  13. data/lib/dry/system/components/bootable.rb +6 -34
  14. data/lib/dry/system/components/config.rb +2 -2
  15. data/lib/dry/system/config/component_dir.rb +202 -0
  16. data/lib/dry/system/config/component_dirs.rb +184 -0
  17. data/lib/dry/system/constants.rb +5 -5
  18. data/lib/dry/system/container.rb +133 -184
  19. data/lib/dry/system/errors.rb +21 -16
  20. data/lib/dry/system/identifier.rb +157 -0
  21. data/lib/dry/system/lifecycle.rb +2 -2
  22. data/lib/dry/system/loader.rb +40 -41
  23. data/lib/dry/system/loader/autoloading.rb +26 -0
  24. data/lib/dry/system/magic_comments_parser.rb +2 -2
  25. data/lib/dry/system/manual_registrar.rb +1 -1
  26. data/lib/dry/system/plugins.rb +7 -7
  27. data/lib/dry/system/plugins/bootsnap.rb +3 -3
  28. data/lib/dry/system/plugins/dependency_graph.rb +3 -3
  29. data/lib/dry/system/plugins/dependency_graph/strategies.rb +1 -1
  30. data/lib/dry/system/plugins/logging.rb +5 -5
  31. data/lib/dry/system/plugins/monitoring.rb +3 -3
  32. data/lib/dry/system/plugins/monitoring/proxy.rb +3 -3
  33. data/lib/dry/system/plugins/notifications.rb +1 -1
  34. data/lib/dry/system/provider.rb +3 -3
  35. data/lib/dry/system/settings.rb +6 -6
  36. data/lib/dry/system/settings/file_loader.rb +2 -2
  37. data/lib/dry/system/settings/file_parser.rb +1 -1
  38. data/lib/dry/system/stubs.rb +1 -1
  39. data/lib/dry/system/system_components/settings.rb +1 -1
  40. data/lib/dry/system/version.rb +1 -1
  41. metadata +21 -25
  42. data/lib/dry/system/auto_registrar/configuration.rb +0 -43
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/map'
3
+ require "concurrent/map"
4
4
 
5
- require 'dry-equalizer'
6
- require 'dry/inflector'
7
- require 'dry/system/loader'
8
- require 'dry/system/errors'
9
- require 'dry/system/constants'
5
+ require "dry/core/equalizer"
6
+ require "dry/inflector"
7
+ require "dry/system/loader"
8
+ require "dry/system/errors"
9
+ require "dry/system/constants"
10
+ require_relative "identifier"
10
11
 
11
12
  module Dry
12
13
  module System
@@ -14,157 +15,118 @@ module Dry
14
15
  # They expose an API to query this information and use a configurable
15
16
  # loader object to initialize class instances.
16
17
  #
17
- # Components are created automatically through auto-registration and can be
18
- # accessed through `Container.auto_register!` which yields them.
19
- #
20
18
  # @api public
21
19
  class Component
22
- include Dry::Equalizer(:identifier, :path)
20
+ include Dry::Equalizer(:identifier, :file_path, :options)
23
21
 
24
22
  DEFAULT_OPTIONS = {
25
23
  separator: DEFAULT_SEPARATOR,
26
- namespace: nil,
27
- inflector: Dry::Inflector.new
24
+ inflector: Dry::Inflector.new,
25
+ loader: Loader
28
26
  }.freeze
29
27
 
30
28
  # @!attribute [r] identifier
31
29
  # @return [String] component's unique identifier
32
30
  attr_reader :identifier
33
31
 
34
- # @!attribute [r] path
35
- # @return [String] component's relative path
36
- attr_reader :path
37
-
38
- # @!attribute [r] file
39
- # @return [String] component's file name
40
- attr_reader :file
32
+ # @!attribute [r] file_path
33
+ # @return [String, nil] full path to the component's file, if found
34
+ attr_reader :file_path
41
35
 
42
36
  # @!attribute [r] options
43
37
  # @return [Hash] component's options
44
38
  attr_reader :options
45
39
 
46
- # @!attribute [r] loader
47
- # @return [Object#call] component's loader object
48
- attr_reader :loader
49
-
50
40
  # @api private
51
- def self.new(*args, &block)
52
- cache.fetch_or_store([*args, block].hash) do
53
- name, options = args
54
- options = DEFAULT_OPTIONS.merge(options || EMPTY_HASH)
55
-
56
- ns, sep, inflector = options.values_at(:namespace, :separator, :inflector)
57
- identifier = extract_identifier(name, ns, sep)
58
-
59
- path = name.to_s.gsub(sep, PATH_SEPARATOR)
60
- loader = options.fetch(:loader, Loader).new(path, inflector)
61
-
62
- super(identifier, path, options.merge(loader: loader))
63
- end
64
- end
65
-
66
- # @api private
67
- def self.extract_identifier(name, ns, sep)
68
- name_s = name.to_s
69
- identifier = ns ? remove_namespace_from_name(name_s, ns) : name_s
70
-
71
- identifier.scan(WORD_REGEX).join(sep)
72
- end
73
-
74
- # @api private
75
- def self.remove_namespace_from_name(name, ns)
76
- match_value = name.match(/^(?<remove_namespace>#{ns})(?<separator>\W)(?<identifier>.*)/)
77
-
78
- match_value ? match_value[:identifier] : name
41
+ def self.new(identifier, options = EMPTY_HASH)
42
+ options = DEFAULT_OPTIONS.merge(options)
43
+
44
+ namespace = options.delete(:namespace)
45
+ separator = options.delete(:separator)
46
+
47
+ identifier =
48
+ if identifier.is_a?(Identifier)
49
+ identifier
50
+ else
51
+ Identifier.new(
52
+ identifier,
53
+ namespace: namespace,
54
+ separator: separator
55
+ )
56
+ end
57
+
58
+ super(identifier, **options)
79
59
  end
80
60
 
81
61
  # @api private
82
- def self.cache
83
- @cache ||= Concurrent::Map.new
84
- end
85
-
86
- # @api private
87
- def initialize(identifier, path, options)
62
+ def initialize(identifier, file_path: nil, **options)
88
63
  @identifier = identifier
89
- @path = path
64
+ @file_path = file_path
90
65
  @options = options
91
- @file = "#{path}#{RB_EXT}"
92
- @loader = options.fetch(:loader)
93
- freeze
94
66
  end
95
67
 
96
- # Returns components instance
97
- #
98
- # @example
99
- # class MyApp < Dry::System::Container
100
- # configure do |config|
101
- # config.name = :my_app
102
- # config.root = Pathname('/my/app')
103
- # end
104
- #
105
- # auto_register!('lib/clients') do |component|
106
- # # some custom initialization logic, ie:
107
- # constant = component.loader.constant
108
- # constant.create
109
- # end
110
- # end
68
+ # Returns the component's instance
111
69
  #
112
70
  # @return [Object] component's class instance
113
- #
114
71
  # @api public
115
72
  def instance(*args)
116
- loader.call(*args)
73
+ loader.call(self, *args)
117
74
  end
118
75
  ruby2_keywords(:instance) if respond_to?(:ruby2_keywords, true)
119
76
 
120
77
  # @api private
121
- def boot?
78
+ def bootable?
122
79
  false
123
80
  end
124
81
 
125
- # @api private
126
- def file_exists?(paths)
127
- paths.any? { |path| path.join(file).exist? }
82
+ def key
83
+ identifier.to_s
128
84
  end
129
85
 
130
- # @api private
131
- def prepend(name)
132
- self.class.new(
133
- [name, identifier].join(separator), options.merge(loader: loader.class)
134
- )
86
+ def path
87
+ identifier.path
88
+ end
89
+
90
+ def root_key
91
+ identifier.root_key
135
92
  end
136
93
 
94
+ # Returns true if the component has a corresponding file
95
+ #
96
+ # @return [Boolean]
137
97
  # @api private
138
- def namespaced(namespace)
139
- self.class.new(
140
- path, options.merge(loader: loader.class, namespace: namespace)
141
- )
98
+ def file_exists?
99
+ !!file_path
142
100
  end
143
101
 
144
102
  # @api private
145
- def separator
146
- options[:separator]
103
+ def loader
104
+ options[:loader]
147
105
  end
148
106
 
149
107
  # @api private
150
- def namespace
151
- options[:namespace]
108
+ def inflector
109
+ options[:inflector]
152
110
  end
153
111
 
154
112
  # @api private
155
113
  def auto_register?
156
- !!options.fetch(:auto_register) { true }
114
+ callable_option?(options[:auto_register])
157
115
  end
158
116
 
159
117
  # @api private
160
- def root_key
161
- namespaces.first
118
+ def memoize?
119
+ callable_option?(options[:memoize])
162
120
  end
163
121
 
164
122
  private
165
123
 
166
- def namespaces
167
- identifier.split(separator).map(&:to_sym)
124
+ def callable_option?(value)
125
+ if value.respond_to?(:call)
126
+ !!value.call(self)
127
+ else
128
+ !!value
129
+ end
168
130
  end
169
131
  end
170
132
  end
@@ -0,0 +1,128 @@
1
+ require "pathname"
2
+ require_relative "constants"
3
+ require_relative "identifier"
4
+ require_relative "magic_comments_parser"
5
+
6
+ module Dry
7
+ module System
8
+ # A configured component directory within the container's root. Provides access to the
9
+ # component directory's configuration, as well as methods for locating component files
10
+ # within the directory
11
+ #
12
+ # @see Dry::System::Config::ComponentDir
13
+ # @api private
14
+ class ComponentDir
15
+ # @!attribute [r] config
16
+ # @return [Dry::System::Config::ComponentDir] the component directory configuration
17
+ # @api private
18
+ attr_reader :config
19
+
20
+ # @!attribute [r] container
21
+ # @return [Dry::System::Container] the container managing the component directory
22
+ # @api private
23
+ attr_reader :container
24
+
25
+ # @api private
26
+ def initialize(config:, container:)
27
+ @config = config
28
+ @container = container
29
+ end
30
+
31
+ # Returns a component for a given identifier if a matching component file could be
32
+ # found within the component dir
33
+ #
34
+ # This will search within the component dir's configured default_namespace first,
35
+ # then fall back to searching for a non-namespaced file
36
+ #
37
+ # @param identifier [String] the identifier string
38
+ # @return [Dry::System::Component, nil] the component, if found
39
+ #
40
+ # @api private
41
+ def component_for_identifier(identifier)
42
+ identifier = Identifier.new(
43
+ identifier,
44
+ namespace: default_namespace,
45
+ separator: container.config.namespace_separator
46
+ )
47
+
48
+ if (file_path = find_component_file(identifier.path))
49
+ return build_component(identifier, file_path)
50
+ end
51
+
52
+ identifier = identifier.with(namespace: nil)
53
+ if (file_path = find_component_file(identifier.path))
54
+ build_component(identifier, file_path)
55
+ end
56
+ end
57
+
58
+ # Returns a component for a full path to a Ruby source file within the component dir
59
+ #
60
+ # @param path [String] the full path to the file
61
+ # @return [Dry::System::Component] the component
62
+ #
63
+ # @api private
64
+ def component_for_path(path)
65
+ separator = container.config.namespace_separator
66
+
67
+ key = Pathname(path).relative_path_from(full_path).to_s
68
+ .sub(RB_EXT, EMPTY_STRING)
69
+ .scan(WORD_REGEX)
70
+ .join(separator)
71
+
72
+ identifier = Identifier.new(key, separator: separator)
73
+
74
+ if identifier.start_with?(default_namespace)
75
+ identifier = identifier.dequalified(default_namespace, namespace: default_namespace)
76
+ end
77
+
78
+ build_component(identifier, path)
79
+ end
80
+
81
+ # Returns the full path of the component directory
82
+ #
83
+ # @return [Pathname]
84
+ # @api private
85
+ def full_path
86
+ container.root.join(path)
87
+ end
88
+
89
+ # @api private
90
+ def component_options
91
+ {
92
+ auto_register: auto_register,
93
+ loader: loader,
94
+ memoize: memoize
95
+ }
96
+ end
97
+
98
+ private
99
+
100
+ def build_component(identifier, file_path)
101
+ options = {
102
+ inflector: container.config.inflector,
103
+ **component_options,
104
+ **MagicCommentsParser.(file_path)
105
+ }
106
+
107
+ Component.new(identifier, file_path: file_path, **options)
108
+ end
109
+
110
+ def find_component_file(component_path)
111
+ component_file = full_path.join("#{component_path}#{RB_EXT}")
112
+ component_file if component_file.exist?
113
+ end
114
+
115
+ def method_missing(name, *args, &block)
116
+ if config.respond_to?(name)
117
+ config.public_send(name, *args, &block)
118
+ else
119
+ super
120
+ end
121
+ end
122
+
123
+ def respond_to_missing?(name, include_all = false)
124
+ config.respond_to?(name) || super
125
+ end
126
+ end
127
+ end
128
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/system'
3
+ require "dry/system"
4
4
 
5
5
  Dry::System.register_provider(
6
6
  :system,
7
- boot_path: Pathname(__dir__).join('system_components').realpath
7
+ boot_path: Pathname(__dir__).join("system_components").realpath
8
8
  )
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/system/lifecycle'
4
- require 'dry/system/settings'
5
- require 'dry/system/components/config'
6
- require 'dry/system/constants'
3
+ require "dry/system/lifecycle"
4
+ require "dry/system/settings"
5
+ require "dry/system/components/config"
6
+ require "dry/system/constants"
7
7
 
8
8
  module Dry
9
9
  module System
@@ -70,7 +70,7 @@ module Dry
70
70
  @config = nil
71
71
  @config_block = nil
72
72
  @identifier = identifier
73
- @triggers = { before: TRIGGER_MAP.dup, after: TRIGGER_MAP.dup }
73
+ @triggers = {before: TRIGGER_MAP.dup, after: TRIGGER_MAP.dup}
74
74
  @options = block ? options.merge(block: block) : options
75
75
  @namespace = options[:namespace]
76
76
  finalize = options[:finalize] || DEFAULT_FINALIZE
@@ -229,38 +229,10 @@ module Dry
229
229
  # @return [TrueClass]
230
230
  #
231
231
  # @api private
232
- def boot?
232
+ def bootable?
233
233
  true
234
234
  end
235
235
 
236
- # Return path to component's boot file
237
- #
238
- # @return [String]
239
- #
240
- # @api private
241
- def boot_file
242
- container_boot_files
243
- .detect { |path| Pathname(path).basename(RB_EXT).to_s == identifier.to_s }
244
- end
245
-
246
- # Return path to boot dir
247
- #
248
- # @return [String]
249
- #
250
- # @api private
251
- def boot_path
252
- container.boot_path
253
- end
254
-
255
- # Return all boot files defined under container's boot path
256
- #
257
- # @return [String]
258
- #
259
- # @api private
260
- def container_boot_files
261
- ::Dir[container.boot_path.join("**/#{RB_GLOB}")].sort
262
- end
263
-
264
236
  private
265
237
 
266
238
  # Return lifecycle object used for this component
@@ -21,8 +21,8 @@ module Dry
21
21
  private
22
22
 
23
23
  def method_missing(meth, value = nil)
24
- if meth.to_s.end_with?('=')
25
- @settings[meth.to_s.gsub('=', '').to_sym] = value
24
+ if meth.to_s.end_with?("=")
25
+ @settings[meth.to_s.gsub("=", "").to_sym] = value
26
26
  elsif @settings.key?(meth)
27
27
  @settings[meth]
28
28
  else
@@ -0,0 +1,202 @@
1
+ require "dry/configurable"
2
+ require "dry/system/loader"
3
+
4
+ module Dry
5
+ module System
6
+ module Config
7
+ class ComponentDir
8
+ include Dry::Configurable
9
+
10
+ # @!group Settings
11
+
12
+ # @!method auto_register=(policy)
13
+ #
14
+ # Sets the auto-registration policy for the component dir.
15
+ #
16
+ # This may be a simple boolean to enable or disable auto-registration for all
17
+ # components, or a proc accepting a `Dry::Sytem::Component` and returning a
18
+ # boolean to configure auto-registration on a per-component basis
19
+ #
20
+ # Defaults to `true`.
21
+ #
22
+ # @param policy [Boolean, Proc]
23
+ # @return [Boolean, Proc]
24
+ #
25
+ # @example
26
+ # dir.auto_register = false
27
+ #
28
+ # @example
29
+ # dir.auto_register = proc do |component|
30
+ # !component.start_with?("entities")
31
+ # end
32
+ #
33
+ # @see auto_register
34
+ # @see Component
35
+ #
36
+ # @!method auto_register
37
+ #
38
+ # Returns the configured auto-registration policy.
39
+ #
40
+ # @return [Boolean, Proc] the configured policy
41
+ #
42
+ # @see auto_register=
43
+ setting :auto_register, true
44
+
45
+ # @!method add_to_load_path=(policy)
46
+ #
47
+ # Sets whether the dir should be added to the `$LOAD_PATH` after the container
48
+ # is configured.
49
+ #
50
+ # Defaults to `true`. This may need to be set to `false` when using a class
51
+ # autoloading system.
52
+ #
53
+ # @param policy [Boolean]
54
+ # @return [Boolean]
55
+ #
56
+ # @see add_to_load_path
57
+ # @see Container.configure
58
+ #
59
+ # @!method add_to_load_path
60
+ #
61
+ # Returns the configured value.
62
+ #
63
+ # @return [Boolean]
64
+ #
65
+ # @see add_to_load_path=
66
+ setting :add_to_load_path, true
67
+
68
+ # @!method default_namespace=(leading_namespace)
69
+ #
70
+ # Sets the leading namespace segments to be stripped when registering components
71
+ # from the dir in the container.
72
+ #
73
+ # This is useful to configure when the dir contains components in a module
74
+ # namespace that you don't want repeated in their identifiers.
75
+ #
76
+ # Defaults to `nil`.
77
+ #
78
+ # @param leading_namespace [String, nil]
79
+ # @return [String, nil]
80
+ #
81
+ # @example
82
+ # dir.default_namespace = "my_app"
83
+ #
84
+ # @example
85
+ # dir.default_namespace = "my_app.admin"
86
+ #
87
+ # @see default_namespace
88
+ #
89
+ # @!method default_namespace
90
+ #
91
+ # Returns the configured value.
92
+ #
93
+ # @return [String, nil]
94
+ #
95
+ # @see default_namespace=
96
+ setting :default_namespace
97
+
98
+ # @!method loader=(loader)
99
+ #
100
+ # Sets the loader to use when registering coponents from the dir in the container.
101
+ #
102
+ # Defaults to `Dry::System::Loader`.
103
+ #
104
+ # When using a class autoloader, consider using `Dry::System::Loader::Autoloading`
105
+ #
106
+ # @param loader [#call] the loader
107
+ # @return [#call] the configured loader
108
+ #
109
+ # @see loader
110
+ # @see Loader
111
+ # @see Loader::Autoloading
112
+ #
113
+ # @!method loader
114
+ #
115
+ # Returns the configured loader.
116
+ #
117
+ # @return [#call]
118
+ #
119
+ # @see loader=
120
+ setting :loader, Dry::System::Loader
121
+
122
+ # @!method memoize=(policy)
123
+ #
124
+ # Sets whether to memoize components from the dir when registered in the
125
+ # container.
126
+ #
127
+ # This may be a simple boolean to enable or disable memoization for all
128
+ # components, or a proc accepting a `Dry::Sytem::Component` and returning a
129
+ # boolean to configure memoization on a per-component basis
130
+ #
131
+ # Defaults to `false`.
132
+ #
133
+ # @param policy [Boolean, Proc]
134
+ # @return [Boolean, Proc] the configured memoization policy
135
+ #
136
+ # @example
137
+ # dir.memoize = true
138
+ #
139
+ # @example
140
+ # dir.memoize = proc do |component|
141
+ # !component.start_with?("providers")
142
+ # end
143
+ #
144
+ # @see memoize
145
+ # @see Component
146
+ #
147
+ # @!method memoize
148
+ #
149
+ # Returns the configured memoization policy.
150
+ #
151
+ # @return [Boolean, Proc] the configured memoization policy
152
+ #
153
+ # @see memoize=
154
+ setting :memoize, false
155
+
156
+ # @!endgroup
157
+
158
+ # Returns the component dir path, relative to the configured container root
159
+ #
160
+ # @return [String] the path
161
+ attr_reader :path
162
+
163
+ # @api private
164
+ def initialize(path)
165
+ super()
166
+ @path = path
167
+ yield self if block_given?
168
+ end
169
+
170
+ # @api private
171
+ def auto_register?
172
+ !!config.auto_register
173
+ end
174
+
175
+ # Returns true if a setting has been explicitly configured and is not returning
176
+ # just a default value.
177
+ #
178
+ # This is used to determine which settings from `ComponentDirs` should be applied
179
+ # as additional defaults.
180
+ #
181
+ # @api private
182
+ def configured?(key)
183
+ config._settings[key].input_defined?
184
+ end
185
+
186
+ private
187
+
188
+ def method_missing(name, *args, &block)
189
+ if config.respond_to?(name)
190
+ config.public_send(name, *args, &block)
191
+ else
192
+ super
193
+ end
194
+ end
195
+
196
+ def respond_to_missing?(name, include_all = false)
197
+ config.respond_to?(name) || super
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end