dry-system 0.19.1 → 0.22.0

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.
@@ -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