dry-system 0.19.1 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -47,9 +47,9 @@ module Dry
47
47
  class Bootable
48
48
  DEFAULT_FINALIZE = proc {}
49
49
 
50
- # @!attribute [r] identifier
51
- # @return [Symbol] component's unique identifier
52
- attr_reader :identifier
50
+ # @!attribute [r] key
51
+ # @return [Symbol] component's unique name
52
+ attr_reader :name
53
53
 
54
54
  # @!attribute [r] options
55
55
  # @return [Hash] component's options
@@ -66,10 +66,10 @@ module Dry
66
66
  TRIGGER_MAP = Hash.new { |h, k| h[k] = [] }.freeze
67
67
 
68
68
  # @api private
69
- def initialize(identifier, options = {}, &block)
69
+ def initialize(name, options = {}, &block)
70
70
  @config = nil
71
71
  @config_block = nil
72
- @identifier = identifier
72
+ @name = name
73
73
  @triggers = {before: TRIGGER_MAP.dup, after: TRIGGER_MAP.dup}
74
74
  @options = block ? options.merge(block: block) : options
75
75
  @namespace = options[:namespace]
@@ -149,7 +149,7 @@ module Dry
149
149
  if block
150
150
  @settings_block = block
151
151
  elsif @settings_block
152
- @settings = Settings::DSL.new(identifier, &@settings_block).call
152
+ @settings = Settings::DSL.new(&@settings_block).call
153
153
  else
154
154
  @settings
155
155
  end
@@ -211,8 +211,8 @@ module Dry
211
211
  # @return [Dry::Struct]
212
212
  #
213
213
  # @api private
214
- def new(identifier, new_options = EMPTY_HASH)
215
- self.class.new(identifier, options.merge(new_options))
214
+ def new(name, new_options = EMPTY_HASH)
215
+ self.class.new(name, options.merge(new_options))
216
216
  end
217
217
 
218
218
  # Return a new instance with updated options
@@ -221,16 +221,7 @@ module Dry
221
221
  #
222
222
  # @api private
223
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
224
+ self.class.new(name, options.merge(new_options))
234
225
  end
235
226
 
236
227
  private
@@ -256,7 +247,7 @@ module Dry
256
247
  when String, Symbol
257
248
  container.namespace(namespace) { |c| return c }
258
249
  when true
259
- container.namespace(identifier) { |c| return c }
250
+ container.namespace(name) { |c| return c }
260
251
  when nil
261
252
  container
262
253
  else
@@ -1,9 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "dry/configurable"
4
+ require "dry/core/deprecations"
5
+ require "dry/system/constants"
2
6
  require "dry/system/loader"
7
+ require_relative "namespaces"
3
8
 
4
9
  module Dry
5
10
  module System
6
11
  module Config
12
+ # @api public
7
13
  class ComponentDir
8
14
  include Dry::Configurable
9
15
 
@@ -27,11 +33,12 @@ module Dry
27
33
  #
28
34
  # @example
29
35
  # dir.auto_register = proc do |component|
30
- # !component.start_with?("entities")
36
+ # !component.identifier.start_with?("entities")
31
37
  # end
32
38
  #
33
39
  # @see auto_register
34
40
  # @see Component
41
+ # @api public
35
42
  #
36
43
  # @!method auto_register
37
44
  #
@@ -40,7 +47,8 @@ module Dry
40
47
  # @return [Boolean, Proc] the configured policy
41
48
  #
42
49
  # @see auto_register=
43
- setting :auto_register, true
50
+ # @api public
51
+ setting :auto_register, default: true
44
52
 
45
53
  # @!method add_to_load_path=(policy)
46
54
  #
@@ -55,6 +63,7 @@ module Dry
55
63
  #
56
64
  # @see add_to_load_path
57
65
  # @see Container.configure
66
+ # @api public
58
67
  #
59
68
  # @!method add_to_load_path
60
69
  #
@@ -63,41 +72,13 @@ module Dry
63
72
  # @return [Boolean]
64
73
  #
65
74
  # @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
75
+ # @api public
76
+ setting :add_to_load_path, default: true
97
77
 
98
78
  # @!method loader=(loader)
99
79
  #
100
- # Sets the loader to use when registering coponents from the dir in the container.
80
+ # Sets the loader to use when registering components from the dir in the
81
+ # container.
101
82
  #
102
83
  # Defaults to `Dry::System::Loader`.
103
84
  #
@@ -109,6 +90,7 @@ module Dry
109
90
  # @see loader
110
91
  # @see Loader
111
92
  # @see Loader::Autoloading
93
+ # @api public
112
94
  #
113
95
  # @!method loader
114
96
  #
@@ -117,7 +99,8 @@ module Dry
117
99
  # @return [#call]
118
100
  #
119
101
  # @see loader=
120
- setting :loader, Dry::System::Loader
102
+ # @api public
103
+ setting :loader, default: Dry::System::Loader
121
104
 
122
105
  # @!method memoize=(policy)
123
106
  #
@@ -138,11 +121,12 @@ module Dry
138
121
  #
139
122
  # @example
140
123
  # dir.memoize = proc do |component|
141
- # !component.start_with?("providers")
124
+ # !component.identifier.start_with?("providers")
142
125
  # end
143
126
  #
144
127
  # @see memoize
145
128
  # @see Component
129
+ # @api public
146
130
  #
147
131
  # @!method memoize
148
132
  #
@@ -151,7 +135,54 @@ module Dry
151
135
  # @return [Boolean, Proc] the configured memoization policy
152
136
  #
153
137
  # @see memoize=
154
- setting :memoize, false
138
+ # @api public
139
+ setting :memoize, default: false
140
+
141
+ # @!method namespaces
142
+ #
143
+ # Returns the configured namespaces for the component dir.
144
+ #
145
+ # Allows namespaces to added on the returned object via {Namespaces#add}.
146
+ #
147
+ # @return [Namespaces] the namespaces
148
+ #
149
+ # @see Namespaces#add
150
+ # @api public
151
+ setting :namespaces, default: Namespaces.new, cloneable: true
152
+
153
+ # @api public
154
+ def default_namespace=(namespace)
155
+ Dry::Core::Deprecations.announce(
156
+ "Dry::System::Config::ComponentDir#default_namespace=",
157
+ "Add a namespace instead: `dir.namespaces.add #{namespace.to_s.inspect}, key: nil`",
158
+ tag: "dry-system",
159
+ uplevel: 1
160
+ )
161
+
162
+ # We don't have the configured separator here, so the best we can do is guess
163
+ # that it's a dot
164
+ namespace_path = namespace.gsub(".", PATH_SEPARATOR)
165
+
166
+ return if namespaces.namespaces[namespace_path]
167
+
168
+ namespaces.add namespace_path, key: nil
169
+ end
170
+
171
+ # @api public
172
+ def default_namespace
173
+ Dry::Core::Deprecations.announce(
174
+ "Dry::System::Config::ComponentDir#default_namespace",
175
+ "Use namespaces instead, e.g. `dir.namespaces`",
176
+ tag: "dry-system",
177
+ uplevel: 1
178
+ )
179
+
180
+ ns_path = namespaces.to_a.reject(&:root?).first&.path
181
+
182
+ # We don't have the configured separator here, so the best we can do is guess
183
+ # that it's a dot
184
+ ns_path&.gsub(PATH_SEPARATOR, ".")
185
+ end
155
186
 
156
187
  # @!endgroup
157
188
 
@@ -160,7 +191,7 @@ module Dry
160
191
  # @return [String] the path
161
192
  attr_reader :path
162
193
 
163
- # @api private
194
+ # @api public
164
195
  def initialize(path)
165
196
  super()
166
197
  @path = path
@@ -172,15 +203,28 @@ module Dry
172
203
  !!config.auto_register
173
204
  end
174
205
 
175
- # Returns true if a setting has been explicitly configured and is not returning
176
- # just a default value.
206
+ # Returns true if the given setting has been explicitly configured by the user
177
207
  #
178
- # This is used to determine which settings from `ComponentDirs` should be applied
179
- # as additional defaults.
208
+ # This is used when determining whether to apply system-wide default values to a
209
+ # component dir (explicitly configured settings will not be overridden by
210
+ # defaults)
180
211
  #
212
+ # @param key [Symbol] the setting name
213
+ #
214
+ # @return [Boolean]
215
+ #
216
+ # @see Dry::System::Config::ComponentDirs#apply_defaults_to_dir
181
217
  # @api private
182
218
  def configured?(key)
183
- config._settings[key].input_defined?
219
+ case key
220
+ when :namespaces
221
+ # Because we mutate the default value for the `namespaces` setting, rather
222
+ # than assign a new one, to check if it's configured we must see whether any
223
+ # namespaces have been added
224
+ !config.namespaces.empty?
225
+ else
226
+ config._settings[key].input_defined?
227
+ end
184
228
  end
185
229
 
186
230
  private
@@ -1,5 +1,5 @@
1
- require "concurrent/map"
2
- require "dry/configurable"
1
+ # frozen_string_literal: true
2
+
3
3
  require "dry/system/constants"
4
4
  require "dry/system/errors"
5
5
  require_relative "component_dir"
@@ -7,14 +7,10 @@ require_relative "component_dir"
7
7
  module Dry
8
8
  module System
9
9
  module Config
10
+ # The configured component dirs for a container
11
+ #
12
+ # @api public
10
13
  class ComponentDirs
11
- include Dry::Configurable
12
-
13
- # Settings from ComponentDir are configured here as defaults for all added dirs
14
- ComponentDir._settings.each do |setting|
15
- _settings << setting.dup
16
- end
17
-
18
14
  # @!group Settings
19
15
 
20
16
  # @!method auto_register=(value)
@@ -43,19 +39,6 @@ module Dry
43
39
  #
44
40
  # @see add_to_load_path=
45
41
 
46
- # @!method default_namespace=(value)
47
- #
48
- # Sets a default `default_namespace` value for all added component dirs
49
- #
50
- # @see ComponentDir.default_namespace
51
- # @see default_namespace
52
- #
53
- # @!method default_namespace
54
- #
55
- # Returns the configured default `default_namespace`
56
- #
57
- # @see default_namespace=
58
-
59
42
  # @!method loader=(value)
60
43
  #
61
44
  # Sets a default `loader` value for all added component dirs
@@ -82,54 +65,141 @@ module Dry
82
65
  #
83
66
  # @see memoize=
84
67
 
68
+ # rubocop:disable Layout/LineLength
69
+
70
+ # @!method namespaces
71
+ #
72
+ # Returns the default configured namespaces for all added component dirs
73
+ #
74
+ # Allows namespaces to added on the returned object via {Dry::System::Config::Namespaces#add}.
75
+ #
76
+ # @see Dry::System::Config::Namespaces#add
77
+ #
78
+ # @return [Namespaces] the namespaces
79
+
80
+ # rubocop:enable Layout/LineLength
81
+
85
82
  # @!endgroup
86
83
 
84
+ # A ComponentDir for configuring the default values to apply to all added
85
+ # component dirs
86
+ #
87
+ # @see #method_missing
88
+ # @api private
89
+ attr_reader :defaults
90
+
91
+ # Creates a new component dirs
92
+ #
87
93
  # @api private
88
94
  def initialize
89
- @dirs = Concurrent::Map.new
95
+ @dirs = {}
96
+ @defaults = ComponentDir.new(nil)
90
97
  end
91
98
 
92
99
  # @api private
93
100
  def initialize_copy(source)
94
- super
95
- @dirs = source.dirs.dup
101
+ @dirs = source.dirs.map { |path, dir| [path, dir.dup] }.to_h
102
+ @defaults = source.defaults.dup
103
+ end
104
+
105
+ # Returns and optionally yields a previously added component dir
106
+ #
107
+ # @param path [String] the path for the component dir
108
+ # @yieldparam dir [ComponentDir] the component dir
109
+ #
110
+ # @return [ComponentDir] the component dir
111
+ #
112
+ # @api public
113
+ def dir(path)
114
+ dirs[path].tap do |dir|
115
+ # Defaults can be (re-)applied first, since the dir has already been added
116
+ apply_defaults_to_dir(dir) if dir
117
+ yield dir if block_given?
118
+ end
96
119
  end
120
+ alias_method :[], :dir
97
121
 
98
- # Adds and configures a component dir
122
+ # @overload add(path)
123
+ # Adds and configures a component dir for the given path
124
+ #
125
+ # @param path [String] the path for the component dir, relative to the configured
126
+ # container root
127
+ # @yieldparam dir [ComponentDir] the component dir to configure
99
128
  #
100
- # @param path [String] the path for the component dir, relative to the configured
101
- # container root
129
+ # @return [ComponentDir] the added component dir
102
130
  #
103
- # @yieldparam dir [ComponentDir] the component dir to configure
131
+ # @example
132
+ # component_dirs.add "lib" do |dir|
133
+ # dir.default_namespace = "my_app"
134
+ # end
104
135
  #
105
- # @return [ComponentDir] the added component dir
136
+ # @see ComponentDir
137
+ # @api public
106
138
  #
107
- # @example
108
- # component_dirs.add "lib" do |dir|
109
- # dir.default_namespace = "my_app"
110
- # end
139
+ # @overload add(dir)
140
+ # Adds a configured component dir
111
141
  #
112
- # @see ComponentDir
113
- def add(path)
142
+ # @param dir [ComponentDir] the configured component dir
143
+ #
144
+ # @return [ComponentDir] the added component dir
145
+ #
146
+ # @example
147
+ # dir = Dry::System::ComponentDir.new("lib")
148
+ # component_dirs.add dir
149
+ #
150
+ # @see ComponentDir
151
+ # @api public
152
+ def add(path_or_dir)
153
+ path, dir_to_add = path_and_dir(path_or_dir)
154
+
114
155
  raise ComponentDirAlreadyAddedError, path if dirs.key?(path)
115
156
 
116
- dirs[path] = ComponentDir.new(path).tap do |dir|
157
+ dirs[path] = dir_to_add.tap do |dir|
158
+ # Defaults must be applied after yielding, since the dir is being newly added,
159
+ # and must have its configuration fully in place before we can know which
160
+ # defaults to apply
161
+ yield dir if path_or_dir == path && block_given?
117
162
  apply_defaults_to_dir(dir)
118
- yield dir if block_given?
119
163
  end
120
164
  end
121
165
 
122
- # Returns the added component dirs, with default settings applied
166
+ # Deletes and returns a previously added component dir
167
+ #
168
+ # @param path [String] the path for the component dir
123
169
  #
124
- # @return [Hash<String, ComponentDir>] the component dirs as a hash, keyed by path
125
- def dirs
126
- @dirs.each { |_, dir| apply_defaults_to_dir(dir) }
170
+ # @return [ComponentDir] the removed component dir
171
+ #
172
+ # @api public
173
+ def delete(path)
174
+ dirs.delete(path)
127
175
  end
128
176
 
177
+ # Returns the paths of the component dirs
178
+ #
179
+ # @return [Array<String>] the component dir paths
180
+ #
181
+ # @api public
182
+ def paths
183
+ dirs.keys
184
+ end
185
+
186
+ # Returns the count of component dirs
187
+ #
188
+ # @return [Integer]
189
+ #
190
+ # @api public
191
+ def length
192
+ dirs.length
193
+ end
194
+ alias_method :size, :length
195
+
129
196
  # Returns the added component dirs, with default settings applied
130
197
  #
131
198
  # @return [Array<ComponentDir>]
199
+ #
200
+ # @api public
132
201
  def to_a
202
+ dirs.each { |_, dir| apply_defaults_to_dir(dir) }
133
203
  dirs.values
134
204
  end
135
205
 
@@ -137,46 +207,69 @@ module Dry
137
207
  # argument.
138
208
  #
139
209
  # @yieldparam dir [ComponentDir] the yielded component dir
210
+ #
211
+ # @api public
140
212
  def each(&block)
141
213
  to_a.each(&block)
142
214
  end
143
215
 
216
+ protected
217
+
218
+ # Returns the hash of component dirs, keyed by their paths
219
+ #
220
+ # Recently changed default configuration may not be applied to these dirs. Use
221
+ # #to_a or #each to access dirs with default configuration fully applied.
222
+ #
223
+ # This method exists to encapsulate the instance variable and to serve the needs
224
+ # of #initialize_copy
225
+ #
226
+ # @return [Hash{String => ComponentDir}]
227
+ #
228
+ # @api private
229
+ attr_reader :dirs
230
+
144
231
  private
145
232
 
146
- # Apply default settings to a component dir. This is run every time the dirs are
233
+ # Converts a path string or pre-built component dir into a path and dir tuple
234
+ #
235
+ # @param path_or_dir [String,ComponentDir]
236
+ #
237
+ # @return [Array<(String, ComponentDir)>]
238
+ #
239
+ # @see #add
240
+ def path_and_dir(path_or_dir)
241
+ if path_or_dir.is_a?(ComponentDir)
242
+ dir = path_or_dir
243
+ [dir.path, dir]
244
+ else
245
+ path = path_or_dir
246
+ [path, ComponentDir.new(path)]
247
+ end
248
+ end
249
+
250
+ # Applies default settings to a component dir. This is run every time the dirs are
147
251
  # accessed to ensure defaults are applied regardless of when new component dirs
148
252
  # are added. This method must be idempotent.
149
253
  #
150
254
  # @return [void]
151
255
  def apply_defaults_to_dir(dir)
152
- dir.config.values.each do |key, _value|
153
- if configured?(key) && !dir.configured?(key)
154
- dir.public_send(:"#{key}=", public_send(key))
256
+ defaults.config.values.each do |key, _|
257
+ if defaults.configured?(key) && !dir.configured?(key)
258
+ dir.public_send(:"#{key}=", defaults.public_send(key).dup)
155
259
  end
156
260
  end
157
261
  end
158
262
 
159
- # Returns true if a setting has been explicitly configured and is not returning
160
- # just a default value.
161
- #
162
- # This is used to determine which settings should be applied to added component
163
- # dirs as additional defaults.
164
- #
165
- # @api private
166
- def configured?(key)
167
- config._settings[key].input_defined?
168
- end
169
-
170
263
  def method_missing(name, *args, &block)
171
- if config.respond_to?(name)
172
- config.public_send(name, *args, &block)
264
+ if defaults.respond_to?(name)
265
+ defaults.public_send(name, *args, &block)
173
266
  else
174
267
  super
175
268
  end
176
269
  end
177
270
 
178
271
  def respond_to_missing?(name, include_all = false)
179
- config.respond_to?(name) || super
272
+ defaults.respond_to?(name) || super
180
273
  end
181
274
  end
182
275
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+
5
+ module Dry
6
+ module System
7
+ module Config
8
+ # A configured namespace for a component dir
9
+ #
10
+ # Namespaces consist of three elements:
11
+ #
12
+ # - The `path` within the component dir to which its namespace rules should apply.
13
+ # - A `key`, which determines the leading part of the key used to register
14
+ # each component in the container.
15
+ # - A `const`, which is the Ruby namespace expected to contain the class constants
16
+ # defined within each component's source file. This value is expected to be an
17
+ # "underscored" string, intended to be run through the configured inflector to be
18
+ # converted into a real constant (e.g. `"foo_bar/baz"` will become `FooBar::Baz`)
19
+ #
20
+ # Namespaces are added and configured for a component dir via {Namespaces#add}.
21
+ #
22
+ # @see Namespaces#add
23
+ #
24
+ # @api public
25
+ class Namespace
26
+ ROOT_PATH = nil
27
+
28
+ include Dry::Equalizer(:path, :key, :const)
29
+
30
+ # @api public
31
+ attr_reader :path
32
+
33
+ # @api public
34
+ attr_reader :key
35
+
36
+ # @api public
37
+ attr_reader :const
38
+
39
+ # Returns a namespace configured to serve as the default root namespace for a
40
+ # component dir, ensuring that all code within the dir can be loaded, regardless
41
+ # of any other explictly configured namespaces
42
+ #
43
+ # @return [Namespace] the root namespace
44
+ #
45
+ # @api private
46
+ def self.default_root
47
+ new(
48
+ path: ROOT_PATH,
49
+ key: nil,
50
+ const: nil
51
+ )
52
+ end
53
+
54
+ # @api private
55
+ def initialize(path:, key:, const:)
56
+ @path = path
57
+ @key = key
58
+ @const = const
59
+ end
60
+
61
+ # @api public
62
+ def root?
63
+ path == ROOT_PATH
64
+ end
65
+
66
+ # @api public
67
+ def path?
68
+ !root?
69
+ end
70
+
71
+ # @api private
72
+ def default_key?
73
+ key == path
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end