dry-system 0.19.0 → 0.21.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,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