dry-system 0.19.0 → 0.21.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,5 +1,10 @@
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
@@ -27,7 +32,7 @@ module Dry
27
32
  #
28
33
  # @example
29
34
  # dir.auto_register = proc do |component|
30
- # !component.start_with?("entities")
35
+ # !component.identifier.start_with?("entities")
31
36
  # end
32
37
  #
33
38
  # @see auto_register
@@ -40,7 +45,7 @@ module Dry
40
45
  # @return [Boolean, Proc] the configured policy
41
46
  #
42
47
  # @see auto_register=
43
- setting :auto_register, true
48
+ setting :auto_register, default: true
44
49
 
45
50
  # @!method add_to_load_path=(policy)
46
51
  #
@@ -63,41 +68,12 @@ module Dry
63
68
  # @return [Boolean]
64
69
  #
65
70
  # @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
71
+ setting :add_to_load_path, default: true
97
72
 
98
73
  # @!method loader=(loader)
99
74
  #
100
- # Sets the loader to use when registering coponents from the dir in the container.
75
+ # Sets the loader to use when registering components from the dir in the
76
+ # container.
101
77
  #
102
78
  # Defaults to `Dry::System::Loader`.
103
79
  #
@@ -117,7 +93,7 @@ module Dry
117
93
  # @return [#call]
118
94
  #
119
95
  # @see loader=
120
- setting :loader, Dry::System::Loader
96
+ setting :loader, default: Dry::System::Loader
121
97
 
122
98
  # @!method memoize=(policy)
123
99
  #
@@ -138,7 +114,7 @@ module Dry
138
114
  #
139
115
  # @example
140
116
  # dir.memoize = proc do |component|
141
- # !component.start_with?("providers")
117
+ # !component.identifier.start_with?("providers")
142
118
  # end
143
119
  #
144
120
  # @see memoize
@@ -151,7 +127,50 @@ module Dry
151
127
  # @return [Boolean, Proc] the configured memoization policy
152
128
  #
153
129
  # @see memoize=
154
- setting :memoize, false
130
+ setting :memoize, default: false
131
+
132
+ # @!method namespaces
133
+ #
134
+ # Returns the configured namespaces for the component dir.
135
+ #
136
+ # Allows namespaces to added on the returned object via {Namespaces#add}.
137
+ #
138
+ # @see Namespaces#add
139
+ #
140
+ # @return [Namespaces] the namespaces
141
+ setting :namespaces, default: Namespaces.new, cloneable: true
142
+
143
+ def default_namespace=(namespace)
144
+ Dry::Core::Deprecations.announce(
145
+ "Dry::System::Config::ComponentDir#default_namespace=",
146
+ "Add a namespace instead: `dir.namespaces.add #{namespace.to_s.inspect}, key: nil`",
147
+ tag: "dry-system",
148
+ uplevel: 1
149
+ )
150
+
151
+ # We don't have the configured separator here, so the best we can do is guess
152
+ # that it's a dot
153
+ namespace_path = namespace.gsub(".", PATH_SEPARATOR)
154
+
155
+ return if namespaces.namespaces[namespace_path]
156
+
157
+ namespaces.add namespace_path, key: nil
158
+ end
159
+
160
+ def default_namespace
161
+ Dry::Core::Deprecations.announce(
162
+ "Dry::System::Config::ComponentDir#default_namespace",
163
+ "Use namespaces instead, e.g. `dir.namespaces`",
164
+ tag: "dry-system",
165
+ uplevel: 1
166
+ )
167
+
168
+ ns_path = namespaces.to_a.reject(&:root?).first&.path
169
+
170
+ # We don't have the configured separator here, so the best we can do is guess
171
+ # that it's a dot
172
+ ns_path&.gsub(PATH_SEPARATOR, ".")
173
+ end
155
174
 
156
175
  # @!endgroup
157
176
 
@@ -172,15 +191,28 @@ module Dry
172
191
  !!config.auto_register
173
192
  end
174
193
 
175
- # Returns true if a setting has been explicitly configured and is not returning
176
- # just a default value.
194
+ # Returns true if the given setting has been explicitly configured by the user
177
195
  #
178
- # This is used to determine which settings from `ComponentDirs` should be applied
179
- # as additional defaults.
196
+ # This is used when determining whether to apply system-wide default values to a
197
+ # component dir (explicitly configured settings will not be overridden by
198
+ # defaults)
180
199
  #
200
+ # @param key [Symbol] the setting name
201
+ #
202
+ # @return [Boolean]
203
+ #
204
+ # @see Dry::System::Config::ComponentDirs#apply_defaults_to_dir
181
205
  # @api private
182
206
  def configured?(key)
183
- config._settings[key].input_defined?
207
+ case key
208
+ when :namespaces
209
+ # Because we mutate the default value for the `namespaces` setting, rather
210
+ # than assign a new one, to check if it's configured we must see whether any
211
+ # namespaces have been added
212
+ !config.namespaces.empty?
213
+ else
214
+ config._settings[key].input_defined?
215
+ end
184
216
  end
185
217
 
186
218
  private
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "concurrent/map"
2
- require "dry/configurable"
3
4
  require "dry/system/constants"
4
5
  require "dry/system/errors"
5
6
  require_relative "component_dir"
@@ -8,13 +9,6 @@ module Dry
8
9
  module System
9
10
  module Config
10
11
  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
12
  # @!group Settings
19
13
 
20
14
  # @!method auto_register=(value)
@@ -43,19 +37,6 @@ module Dry
43
37
  #
44
38
  # @see add_to_load_path=
45
39
 
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
40
  # @!method loader=(value)
60
41
  #
61
42
  # Sets a default `loader` value for all added component dirs
@@ -82,17 +63,34 @@ module Dry
82
63
  #
83
64
  # @see memoize=
84
65
 
66
+ # @!method namespaces
67
+ #
68
+ # Returns the default configured namespaces for all added component dirs
69
+ #
70
+ # Allows namespaces to added on the returned object via {Namespaces#add}.
71
+ #
72
+ # @see Namespaces#add
73
+ #
74
+ # @return [Namespaces] the namespaces
75
+
85
76
  # @!endgroup
86
77
 
78
+ # A ComponentDir for configuring the default values to apply to all added
79
+ # component dirs
80
+ #
81
+ # @api private
82
+ attr_reader :defaults
83
+
87
84
  # @api private
88
85
  def initialize
89
86
  @dirs = Concurrent::Map.new
87
+ @defaults = ComponentDir.new(nil)
90
88
  end
91
89
 
92
90
  # @api private
93
91
  def initialize_copy(source)
94
- super
95
92
  @dirs = source.dirs.dup
93
+ @defaults = source.defaults.dup
96
94
  end
97
95
 
98
96
  # Adds and configures a component dir
@@ -114,8 +112,8 @@ module Dry
114
112
  raise ComponentDirAlreadyAddedError, path if dirs.key?(path)
115
113
 
116
114
  dirs[path] = ComponentDir.new(path).tap do |dir|
117
- apply_defaults_to_dir(dir)
118
115
  yield dir if block_given?
116
+ apply_defaults_to_dir(dir)
119
117
  end
120
118
  end
121
119
 
@@ -143,40 +141,29 @@ module Dry
143
141
 
144
142
  private
145
143
 
146
- # Apply default settings to a component dir. This is run every time the dirs are
144
+ # Applies default settings to a component dir. This is run every time the dirs are
147
145
  # accessed to ensure defaults are applied regardless of when new component dirs
148
146
  # are added. This method must be idempotent.
149
147
  #
150
148
  # @return [void]
151
149
  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))
150
+ defaults.config.values.each do |key, _|
151
+ if defaults.configured?(key) && !dir.configured?(key)
152
+ dir.public_send(:"#{key}=", defaults.public_send(key).dup)
155
153
  end
156
154
  end
157
155
  end
158
156
 
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
157
  def method_missing(name, *args, &block)
171
- if config.respond_to?(name)
172
- config.public_send(name, *args, &block)
158
+ if defaults.respond_to?(name)
159
+ defaults.public_send(name, *args, &block)
173
160
  else
174
161
  super
175
162
  end
176
163
  end
177
164
 
178
165
  def respond_to_missing?(name, include_all = false)
179
- config.respond_to?(name) || super
166
+ defaults.respond_to?(name) || super
180
167
  end
181
168
  end
182
169
  end
@@ -0,0 +1,71 @@
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 private
25
+ class Namespace
26
+ ROOT_PATH = nil
27
+
28
+ include Dry::Equalizer(:path, :key, :const)
29
+
30
+ attr_reader :path
31
+
32
+ attr_reader :key
33
+
34
+ attr_reader :const
35
+
36
+ # Returns a namespace configured to serve as the default root namespace for a
37
+ # component dir, ensuring that all code within the dir can be loaded, regardless
38
+ # of any other explictly configured namespaces
39
+ #
40
+ # @return [Namespace] the root namespace
41
+ #
42
+ # @api private
43
+ def self.default_root
44
+ new(
45
+ path: ROOT_PATH,
46
+ key: nil,
47
+ const: nil
48
+ )
49
+ end
50
+
51
+ def initialize(path:, key:, const:)
52
+ @path = path
53
+ @key = key
54
+ @const = const
55
+ end
56
+
57
+ def root?
58
+ path == ROOT_PATH
59
+ end
60
+
61
+ def path?
62
+ !root?
63
+ end
64
+
65
+ def default_key?
66
+ key == path
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/system/errors"
4
+ require_relative "namespace"
5
+
6
+ module Dry
7
+ module System
8
+ module Config
9
+ # The configured namespaces for a ComponentDir
10
+ #
11
+ # @see Config::ComponentDir#namespaces
12
+ #
13
+ # @api private
14
+ class Namespaces
15
+ # @api private
16
+ attr_reader :namespaces
17
+
18
+ # @api private
19
+ def initialize
20
+ @namespaces = {}
21
+ end
22
+
23
+ # @api private
24
+ def initialize_copy(source)
25
+ super
26
+ @namespaces = source.namespaces.dup
27
+ end
28
+
29
+ # rubocop:disable Layout/LineLength
30
+
31
+ # Adds a component dir namespace
32
+ #
33
+ # A namespace encompasses a given sub-directory of the component dir, and
34
+ # determines (1) the leading segments of its components' registered identifiers,
35
+ # and (2) the expected constant namespace of their class constants.
36
+ #
37
+ # A namespace for a path can only be added once.
38
+ #
39
+ # @example Adding a namespace with top-level identifiers
40
+ # # Components defined within admin/ (e.g. admin/my_component.rb) will be:
41
+ # #
42
+ # # - Registered with top-level identifiers ("my_component")
43
+ # # - Expected to have constants in `Admin`, matching the namespace's path (Admin::MyComponent)
44
+ #
45
+ # namespaces.add "admin", key: nil
46
+ #
47
+ # @example Adding a namespace with top-level class constants
48
+ # # Components defined within adapters/ (e.g. adapters/my_adapter.rb) will be:
49
+ # #
50
+ # # - Registered with leading identifiers matching the namespace's path ("adapters.my_adapter")
51
+ # # - Expected to have top-level constants (::MyAdapter)
52
+ #
53
+ # namespaces.add "adapters", const: nil
54
+ #
55
+ # @example Adding a namespace with distinct identifiers and class constants
56
+ # # Components defined within `bananas/` (e.g. bananas/banana_split.rb) will be:
57
+ # #
58
+ # # - Registered with the given leading identifier ("desserts.banana_split")
59
+ # # - Expected to have constants within the given namespace (EatMe::Now::BananaSplit)
60
+ #
61
+ # namespaces.add "bananas", key: "desserts", const: "eat_me/now"
62
+ #
63
+ # @param path [String] the path to the sub-directory of source files to which this
64
+ # namespace should apply, relative to the component dir
65
+ # @param identifier [String, nil] the leading namespace to apply to the registered
66
+ # identifiers for the components. Set `nil` for the identifiers to be top-level.
67
+ # @param const [String, nil] the Ruby constant namespace to expect for constants
68
+ # defined within the components. This should be provided in underscored string
69
+ # form, e.g. "hello_there/world" for a Ruby constant of `HelloThere::World`. Set
70
+ # `nil` for the constants to be top-level.
71
+ #
72
+ # @return [Namespace] the added namespace
73
+ #
74
+ # @see Namespace
75
+ #
76
+ # @api public
77
+ def add(path, key: path, const: path)
78
+ raise NamespaceAlreadyAddedError, path if namespaces.key?(path)
79
+
80
+ namespaces[path] = Namespace.new(path: path, key: key, const: const)
81
+ end
82
+
83
+ # rubocop:enable Layout/LineLength
84
+
85
+ # Adds a root component dir namespace
86
+ #
87
+ # @see #add
88
+ #
89
+ # @api public
90
+ def root(key: nil, const: nil)
91
+ add(Namespace::ROOT_PATH, key: key, const: const)
92
+ end
93
+
94
+ # @api private
95
+ def empty?
96
+ namespaces.empty?
97
+ end
98
+
99
+ # Returns the configured namespaces as an array
100
+ #
101
+ # This adds a root namespace to the end of the array if one was not configured
102
+ # manually. This fallback ensures that all components in the component dir can be
103
+ # loaded.
104
+ #
105
+ # @return [Array<Namespace>] the namespaces
106
+ #
107
+ # @api private
108
+ def to_a
109
+ namespaces.values.tap do |arr|
110
+ arr << Namespace.default_root unless arr.any?(&:root?)
111
+ end
112
+ end
113
+
114
+ # @api private
115
+ def each(&block)
116
+ to_a.each(&block)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -8,7 +8,7 @@ module Dry
8
8
 
9
9
  RB_EXT = ".rb"
10
10
  RB_GLOB = "*.rb"
11
- PATH_SEPARATOR = "/"
11
+ PATH_SEPARATOR = File::SEPARATOR
12
12
  DEFAULT_SEPARATOR = "."
13
13
  WORD_REGEX = /\w+/.freeze
14
14
  end