dry-system 0.15.0 → 0.19.1

Sign up to get free protection for your applications and to get access to all the features.
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