dry-core 0.7.1 → 1.0.1

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.
@@ -0,0 +1,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/hash"
4
+ require "dry/core/constants"
5
+
6
+ module Dry
7
+ module Core
8
+ class Container
9
+ include Dry::Core::Constants
10
+
11
+ # @api public
12
+ Error = Class.new(StandardError)
13
+
14
+ # Error raised when key is not defined in the registry
15
+ #
16
+ # @api public
17
+ KeyError = Class.new(::KeyError)
18
+
19
+ if defined?(DidYouMean::KeyErrorChecker)
20
+ DidYouMean.correct_error(KeyError, DidYouMean::KeyErrorChecker)
21
+ end
22
+
23
+ # Mixin to expose Inversion of Control (IoC) container behaviour
24
+ #
25
+ # @example
26
+ #
27
+ # class MyClass
28
+ # extend Dry::Core::Container::Mixin
29
+ # end
30
+ #
31
+ # MyClass.register(:item, 'item')
32
+ # MyClass.resolve(:item)
33
+ # => 'item'
34
+ #
35
+ # class MyObject
36
+ # include Dry::Core::Container::Mixin
37
+ # end
38
+ #
39
+ # container = MyObject.new
40
+ # container.register(:item, 'item')
41
+ # container.resolve(:item)
42
+ # => 'item'
43
+ #
44
+ # @api public
45
+ #
46
+ # rubocop:disable Metrics/ModuleLength
47
+ module Mixin
48
+ PREFIX_NAMESPACE = lambda do |namespace, key, config|
49
+ [namespace, key].join(config.namespace_separator)
50
+ end
51
+
52
+ # @private
53
+ def self.extended(base)
54
+ hooks_mod = ::Module.new do
55
+ def inherited(subclass)
56
+ subclass.instance_variable_set(:@_container, @_container.dup)
57
+ super
58
+ end
59
+ end
60
+
61
+ base.class_eval do
62
+ extend Configuration
63
+ extend hooks_mod
64
+
65
+ @_container = ::Concurrent::Hash.new
66
+ end
67
+ end
68
+
69
+ # @private
70
+ module Initializer
71
+ def initialize(*args, &block)
72
+ @_container = ::Concurrent::Hash.new
73
+ super
74
+ end
75
+ end
76
+
77
+ # @private
78
+ def self.included(base)
79
+ base.class_eval do
80
+ extend Configuration
81
+ prepend Initializer
82
+
83
+ def config
84
+ self.class.config
85
+ end
86
+ end
87
+ end
88
+
89
+ # Register an item with the container to be resolved later
90
+ #
91
+ # @param [Mixed] key
92
+ # The key to register the container item with (used to resolve)
93
+ # @param [Mixed] contents
94
+ # The item to register with the container (if no block given)
95
+ # @param [Hash] options
96
+ # Options to pass to the registry when registering the item
97
+ # @yield
98
+ # If a block is given, contents will be ignored and the block
99
+ # will be registered instead
100
+ #
101
+ # @return [Dry::Core::Container::Mixin] self
102
+ #
103
+ # @api public
104
+ def register(key, contents = nil, options = EMPTY_HASH, &block)
105
+ if block_given?
106
+ item = block
107
+ options = contents if contents.is_a?(::Hash)
108
+ else
109
+ item = contents
110
+ end
111
+
112
+ config.registry.call(_container, key, item, options)
113
+
114
+ self
115
+ rescue FrozenError
116
+ raise FrozenError,
117
+ "can't modify frozen #{self.class} (when attempting to register '#{key}')"
118
+ end
119
+
120
+ # Resolve an item from the container
121
+ #
122
+ # @param [Mixed] key
123
+ # The key for the item you wish to resolve
124
+ # @yield
125
+ # Fallback block to call when a key is missing. Its result will be returned
126
+ # @yieldparam [Mixed] key Missing key
127
+ #
128
+ # @return [Mixed]
129
+ #
130
+ # @api public
131
+ def resolve(key, &block)
132
+ config.resolver.call(_container, key, &block)
133
+ end
134
+
135
+ # Resolve an item from the container
136
+ #
137
+ # @param [Mixed] key
138
+ # The key for the item you wish to resolve
139
+ #
140
+ # @return [Mixed]
141
+ #
142
+ # @api public
143
+ # @see Dry::Core::Container::Mixin#resolve
144
+ def [](key)
145
+ resolve(key)
146
+ end
147
+
148
+ # Merge in the items of the other container
149
+ #
150
+ # @param [Dry::Core::Container] other
151
+ # The other container to merge in
152
+ # @param [Symbol, nil] namespace
153
+ # Namespace to prefix other container items with, defaults to nil
154
+ #
155
+ # @return [Dry::Core::Container::Mixin] self
156
+ #
157
+ # @api public
158
+ def merge(other, namespace: nil, &block)
159
+ if namespace
160
+ _container.merge!(
161
+ other._container.each_with_object(::Concurrent::Hash.new) { |(key, item), hsh|
162
+ hsh[PREFIX_NAMESPACE.call(namespace, key, config)] = item
163
+ },
164
+ &block
165
+ )
166
+ else
167
+ _container.merge!(other._container, &block)
168
+ end
169
+
170
+ self
171
+ end
172
+
173
+ # Check whether an item is registered under the given key
174
+ #
175
+ # @param [Mixed] key
176
+ # The key you wish to check for registration with
177
+ #
178
+ # @return [Bool]
179
+ #
180
+ # @api public
181
+ def key?(key)
182
+ config.resolver.key?(_container, key)
183
+ end
184
+
185
+ # An array of registered names for the container
186
+ #
187
+ # @return [Array<String>]
188
+ #
189
+ # @api public
190
+ def keys
191
+ config.resolver.keys(_container)
192
+ end
193
+
194
+ # Calls block once for each key in container, passing the key as a parameter.
195
+ #
196
+ # If no block is given, an enumerator is returned instead.
197
+ #
198
+ # @return [Dry::Core::Container::Mixin] self
199
+ #
200
+ # @api public
201
+ def each_key(&block)
202
+ config.resolver.each_key(_container, &block)
203
+ self
204
+ end
205
+
206
+ # Calls block once for each key/value pair in the container, passing the key and
207
+ # the registered item parameters.
208
+ #
209
+ # If no block is given, an enumerator is returned instead.
210
+ #
211
+ # @return [Enumerator]
212
+ #
213
+ # @api public
214
+ #
215
+ # @note In discussions with other developers, it was felt that being able to iterate
216
+ # over not just the registered keys, but to see what was registered would be
217
+ # very helpful. This is a step toward doing that.
218
+ def each(&block)
219
+ config.resolver.each(_container, &block)
220
+ end
221
+
222
+ # Decorates an item from the container with specified decorator
223
+ #
224
+ # @return [Dry::Core::Container::Mixin] self
225
+ #
226
+ # @api public
227
+ def decorate(key, with: nil, &block)
228
+ key = key.to_s
229
+ original = _container.delete(key) do
230
+ raise KeyError, "Nothing registered with the key #{key.inspect}"
231
+ end
232
+
233
+ if with.is_a?(Class)
234
+ decorator = with.method(:new)
235
+ elsif block.nil? && !with.respond_to?(:call)
236
+ raise Error, "Decorator needs to be a Class, block, or respond to the `call` method"
237
+ else
238
+ decorator = with || block
239
+ end
240
+
241
+ _container[key] = original.map(decorator)
242
+ self
243
+ end
244
+
245
+ # Evaluate block and register items in namespace
246
+ #
247
+ # @param [Mixed] namespace
248
+ # The namespace to register items in
249
+ #
250
+ # @return [Dry::Core::Container::Mixin] self
251
+ #
252
+ # @api public
253
+ def namespace(namespace, &block)
254
+ ::Dry::Core::Container::NamespaceDSL.new(
255
+ self,
256
+ namespace,
257
+ config.namespace_separator,
258
+ &block
259
+ )
260
+
261
+ self
262
+ end
263
+
264
+ # Import a namespace
265
+ #
266
+ # @param [Dry::Core::Container::Namespace] namespace
267
+ # The namespace to import
268
+ #
269
+ # @return [Dry::Core::Container::Mixin] self
270
+ #
271
+ # @api public
272
+ def import(namespace)
273
+ namespace(namespace.name, &namespace.block)
274
+
275
+ self
276
+ end
277
+
278
+ # Freeze the container. Nothing can be registered after freezing
279
+ #
280
+ # @api public
281
+ def freeze
282
+ super
283
+ _container.freeze
284
+ self
285
+ end
286
+
287
+ # @private no, really
288
+ def _container
289
+ @_container
290
+ end
291
+
292
+ # @api public
293
+ def dup
294
+ copy = super
295
+ copy.instance_variable_set(:@_container, _container.dup)
296
+ copy
297
+ end
298
+
299
+ # @api public
300
+ def clone
301
+ copy = super
302
+ unless copy.frozen?
303
+ copy.instance_variable_set(:@_container, _container.dup)
304
+ end
305
+ copy
306
+ end
307
+ end
308
+ # rubocop:enable Metrics/ModuleLength
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Core
5
+ class Container
6
+ # Create a namespace to be imported
7
+ #
8
+ # @example
9
+ #
10
+ # ns = Dry::Core::Container::Namespace.new('name') do
11
+ # register('item', 'item')
12
+ # end
13
+ #
14
+ # container = Dry::Core::Container.new
15
+ #
16
+ # container.import(ns)
17
+ #
18
+ # container.resolve('name.item')
19
+ # => 'item'
20
+ #
21
+ #
22
+ # @api public
23
+ class Namespace
24
+ # @return [Mixed] The namespace (name)
25
+ attr_reader :name
26
+
27
+ # @return [Proc] The block to be executed when the namespace is imported
28
+ attr_reader :block
29
+
30
+ # Create a new namespace
31
+ #
32
+ # @param [Mixed] name
33
+ # The name of the namespace
34
+ # @yield
35
+ # The block to evaluate when the namespace is imported
36
+ #
37
+ # @return [Dry::Core::Container::Namespace]
38
+ #
39
+ # @api public
40
+ def initialize(name, &block)
41
+ @name = name
42
+ @block = block
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ module Dry
6
+ module Core
7
+ class Container
8
+ # @api private
9
+ class NamespaceDSL < ::SimpleDelegator
10
+ # DSL for defining namespaces
11
+ #
12
+ # @param [Dry::Core::Container::Mixin] container
13
+ # The container
14
+ # @param [String] namespace
15
+ # The namespace (name)
16
+ # @param [String] namespace_separator
17
+ # The namespace separator
18
+ # @yield
19
+ # The block to evaluate to define the namespace
20
+ #
21
+ # @return [Mixed]
22
+ #
23
+ # @api private
24
+ def initialize(container, namespace, namespace_separator, &block)
25
+ @namespace = namespace
26
+ @namespace_separator = namespace_separator
27
+
28
+ super(container)
29
+
30
+ if block.arity.zero?
31
+ instance_eval(&block)
32
+ else
33
+ yield self
34
+ end
35
+ end
36
+
37
+ def register(key, *args, &block)
38
+ super(namespaced(key), *args, &block)
39
+ end
40
+
41
+ def namespace(namespace, &block)
42
+ super(namespaced(namespace), &block)
43
+ end
44
+
45
+ def import(namespace)
46
+ namespace(namespace.name, &namespace.block)
47
+
48
+ self
49
+ end
50
+
51
+ def resolve(key)
52
+ super(namespaced(key))
53
+ end
54
+
55
+ private
56
+
57
+ def namespaced(key)
58
+ [@namespace, key].join(@namespace_separator)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Core
5
+ class Container
6
+ # Default registry for registering items with the container
7
+ #
8
+ # @api public
9
+ class Registry
10
+ # @private
11
+ def initialize
12
+ @_mutex = ::Mutex.new
13
+ end
14
+
15
+ # Register an item with the container to be resolved later
16
+ #
17
+ # @param [Concurrent::Hash] container
18
+ # The container
19
+ # @param [Mixed] key
20
+ # The key to register the container item with (used to resolve)
21
+ # @param [Mixed] item
22
+ # The item to register with the container
23
+ # @param [Hash] options
24
+ # @option options [Symbol] :call
25
+ # Whether the item should be called when resolved
26
+ #
27
+ # @raise [Dry::Core::Container::KeyError]
28
+ # If an item is already registered with the given key
29
+ #
30
+ # @return [Mixed]
31
+ #
32
+ # @api public
33
+ def call(container, key, item, options)
34
+ key = key.to_s.dup.freeze
35
+
36
+ @_mutex.synchronize do
37
+ if container.key?(key)
38
+ raise KeyError, "There is already an item registered with the key #{key.inspect}"
39
+ end
40
+
41
+ container[key] = factory.call(item, options)
42
+ end
43
+ end
44
+
45
+ # @api private
46
+ def factory
47
+ @factory ||= Container::Item::Factory.new
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Core
5
+ class Container
6
+ # Default resolver for resolving items from container
7
+ #
8
+ # @api public
9
+ class Resolver
10
+ # Resolve an item from the container
11
+ #
12
+ # @param [Concurrent::Hash] container
13
+ # The container
14
+ # @param [Mixed] key
15
+ # The key for the item you wish to resolve
16
+ # @yield
17
+ # Fallback block to call when a key is missing. Its result will be returned
18
+ # @yieldparam [Mixed] key Missing key
19
+ #
20
+ # @raise [KeyError]
21
+ # If the given key is not registered with the container (and no block provided)
22
+ #
23
+ #
24
+ # @return [Mixed]
25
+ #
26
+ # @api public
27
+ def call(container, key)
28
+ item = container.fetch(key.to_s) do
29
+ if block_given?
30
+ return yield(key)
31
+ else
32
+ raise KeyError.new(%(key not found: "#{key}"), key: key.to_s, receiver: container)
33
+ end
34
+ end
35
+
36
+ item.call
37
+ end
38
+
39
+ # Check whether an items is registered under the given key
40
+ #
41
+ # @param [Concurrent::Hash] container
42
+ # The container
43
+ # @param [Mixed] key
44
+ # The key you wish to check for registration with
45
+ #
46
+ # @return [Bool]
47
+ #
48
+ # @api public
49
+ def key?(container, key)
50
+ container.key?(key.to_s)
51
+ end
52
+
53
+ # An array of registered names for the container
54
+ #
55
+ # @return [Array]
56
+ #
57
+ # @api public
58
+ def keys(container)
59
+ container.keys
60
+ end
61
+
62
+ # Calls block once for each key in container, passing the key as a parameter.
63
+ #
64
+ # If no block is given, an enumerator is returned instead.
65
+ #
66
+ # @return Hash
67
+ #
68
+ # @api public
69
+ def each_key(container, &block)
70
+ container.each_key(&block)
71
+ end
72
+
73
+ # Calls block once for each key in container, passing the key and
74
+ # the registered item parameters.
75
+ #
76
+ # If no block is given, an enumerator is returned instead.
77
+ #
78
+ # @return Key, Value
79
+ #
80
+ # @api public
81
+ # @note In discussions with other developers, it was felt that being able
82
+ # to iterate over not just the registered keys, but to see what was
83
+ # registered would be very helpful. This is a step toward doing that.
84
+ def each(container, &block)
85
+ container.map { |key, value| [key, value.call] }.each(&block)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Core
5
+ class Container
6
+ module Stub
7
+ # Overrides resolve to look into stubbed keys first
8
+ #
9
+ # @api public
10
+ def resolve(key)
11
+ _stubs.fetch(key.to_s) { super }
12
+ end
13
+
14
+ # Add a stub to the container
15
+ def stub(key, value, &block)
16
+ unless key?(key)
17
+ raise ArgumentError, "cannot stub #{key.to_s.inspect} - no such key in container"
18
+ end
19
+
20
+ _stubs[key.to_s] = value
21
+
22
+ if block
23
+ yield
24
+ unstub(key)
25
+ end
26
+
27
+ self
28
+ end
29
+
30
+ # Remove stubbed keys from the container
31
+ def unstub(*keys)
32
+ keys = _stubs.keys if keys.empty?
33
+ keys.each { |key| _stubs.delete(key.to_s) }
34
+ end
35
+
36
+ # Stubs have already been enabled turning this into a noop
37
+ def enable_stubs!
38
+ # DO NOTHING
39
+ end
40
+
41
+ private
42
+
43
+ # Stubs container
44
+ def _stubs
45
+ @_stubs ||= {}
46
+ end
47
+ end
48
+
49
+ module Mixin
50
+ # Enable stubbing functionality into the current container
51
+ def enable_stubs!
52
+ extend ::Dry::Core::Container::Stub
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Core
5
+ # Thread-safe object registry
6
+ #
7
+ # @example
8
+ #
9
+ # container = Dry::Core::Container.new
10
+ # container.register(:item, 'item')
11
+ # container.resolve(:item)
12
+ # => 'item'
13
+ #
14
+ # container.register(:item1, -> { 'item' })
15
+ # container.resolve(:item1)
16
+ # => 'item'
17
+ #
18
+ # container.register(:item2, -> { 'item' }, call: false)
19
+ # container.resolve(:item2)
20
+ # => #<Proc:0x007f33b169e998@(irb):10 (lambda)>
21
+ #
22
+ # @api public
23
+ class Container
24
+ include Container::Mixin
25
+ end
26
+ end
27
+ end
@@ -4,7 +4,7 @@ require "logger"
4
4
 
5
5
  module Dry
6
6
  module Core
7
- # An extension for issueing warnings on using deprecated methods.
7
+ # An extension for issuing warnings on using deprecated methods.
8
8
  #
9
9
  # @example
10
10
  #
@@ -1,17 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dry
4
- # Build an equalizer module for the inclusion in other class
5
- #
6
- # ## Credits
7
- #
8
- # Equalizer has been originally imported from the equalizer gem created by Dan Kubb
9
- #
10
- # @api public
11
- def self.Equalizer(*keys, **options)
12
- Dry::Core::Equalizer.new(*keys, **options)
13
- end
14
-
15
4
  module Core
16
5
  # Define equality, equivalence and inspection methods
17
6
  class Equalizer < ::Module
@@ -149,4 +138,19 @@ module Dry
149
138
  end
150
139
  end
151
140
  end
141
+
142
+ # Old modules that depend on dry/core/equalizer may miss
143
+ # this method if dry/core is not required explicitly
144
+ unless singleton_class.method_defined?(:Equalizer)
145
+ # Build an equalizer module for the inclusion in other class
146
+ #
147
+ # ## Credits
148
+ #
149
+ # Equalizer has been originally imported from the equalizer gem created by Dan Kubb
150
+ #
151
+ # @api public
152
+ def self.Equalizer(*keys, **options)
153
+ Dry::Core::Equalizer.new(*keys, **options)
154
+ end
155
+ end
152
156
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Dry
4
4
  module Core
5
- class InvalidClassAttributeValue < StandardError
5
+ class InvalidClassAttributeValueError < StandardError
6
6
  def initialize(name, value)
7
7
  super(
8
8
  "Value #{value.inspect} is invalid for class attribute #{name.inspect}"