dry-core 0.8.1 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d3ee16c97a04071316e718d8277a4a0ac5aea083b37ac7944d65fd3fd78dfc2
4
- data.tar.gz: 44e0e7cb408530f47add47481dd393ba8544e2bcb41256fa0d88af915e7ce547
3
+ metadata.gz: 518b0050fe58a57f837980c65799982ee76a803f684288b58bb47fb7a59e91f6
4
+ data.tar.gz: 77db1195074bfd0e6de6a33a0e3b14b25c7d82e0cab202a9738d1a1ffa7f28de
5
5
  SHA512:
6
- metadata.gz: 0a9787d9d8514c54564448b7e38a5c52810dfd95f098c399c0067928c221357ee772273ca8d8d258e9305679ad3d2ff552b3ea38318a8872136e36d7c434d0b3
7
- data.tar.gz: 8f073e6e1c574404f6df1b699470432e4406129a20d2a8d27b0de10db52a8363beb5aaa086fb4c8c629402e3d1d5735715c5126e38384e20519dbee4f30a233f
6
+ metadata.gz: 9dedf1b3b3b2f1b08a0d8b2fd382d9b96bc2dafdce30a53a3cef75869bc037090bd9b30a16f032d075dd95a55212f25f6ef055af1bf1473b5083609524c65fbb
7
+ data.tar.gz: 861672b79b3286c54d4886a02b861488e30d86e9bb8ab625cd1eac32cf45b571c51e61537c69a5d48d5cd45a9f87682dc8602b7f84bb9b908774e0e8db6906f3
data/CHANGELOG.md CHANGED
@@ -1,6 +1,47 @@
1
1
  <!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
2
2
 
3
- ## 0.8.0
3
+ ## 1.0.0 2022-11-04
4
+
5
+
6
+ ### Added
7
+
8
+ - Import dry-container as `Dry::Core::Container` (via #77) (@solnic)
9
+
10
+
11
+ [Compare v0.9.1...v1.0.0](https://github.com/dry-rb/dry-core/compare/v0.9.1...v1.0.0)
12
+
13
+ ## 0.9.1 2022-10-18
14
+
15
+
16
+ ### Changed
17
+
18
+ - Correct missing constant for IDENTITY (issue #75 fixed via #76) (@poloka)
19
+
20
+ [Compare v0.9.0...v0.9.1](https://github.com/dry-rb/dry-core/compare/v0.9.0...v0.9.1)
21
+
22
+ ## 0.9.0 2022-10-15
23
+
24
+
25
+ ### Changed
26
+
27
+ - dry-core now uses zeitwerk for autoloading (@solnic)
28
+
29
+ [Compare v0.8.1...v0.9.0](https://github.com/dry-rb/dry-core/compare/v0.8.1...v0.9.0)
30
+
31
+ ## 0.8.1 2022-07-27
32
+
33
+
34
+ ### Fixed
35
+
36
+ - [memoizable] plays better with inheritance.
37
+ There were cases when cached values from base claesses were used, see #70 (@flash-gordon)
38
+
39
+
40
+
41
+ [Compare v0.8.0...v0.8.1](https://github.com/dry-rb/dry-core/compare/v0.8.0...v0.8.1)
42
+
43
+ ## 0.8.0 2022-07-15
44
+
4
45
 
5
46
  ### Added
6
47
 
@@ -12,7 +53,7 @@
12
53
  This changes the order of returned subclasses (immediate subclasses now go first) (@flash-gordon)
13
54
 
14
55
 
15
- [Compare v0.7.1...v0.8.0](https://github.com/dry-rb/dry-core/compare/v0.7.1...master)
56
+ [Compare v0.7.1...v0.8.0](https://github.com/dry-rb/dry-core/compare/v0.7.1...v0.8.0)
16
57
 
17
58
  ## 0.7.1 2021-07-10
18
59
 
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015-2021 dry-rb team
3
+ Copyright (c) 2015-2022 dry-rb team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
data/dry-core.gemspec CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
 
31
31
  # to update dependencies edit project.yml
32
32
  spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
33
+ spec.add_runtime_dependency "zeitwerk", "~> 2.6"
33
34
 
34
35
  spec.add_development_dependency "bundler"
35
36
  spec.add_development_dependency "rake"
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/core/constants"
4
- require "dry/core/errors"
5
4
 
6
5
  module Dry
7
6
  module Core
@@ -10,7 +9,6 @@ module Dry
10
9
  # @api public
11
10
  module ClassAttributes
12
11
  include Constants
13
-
14
12
  # Specify what attributes a class will use
15
13
  #
16
14
  # @example
@@ -86,7 +84,7 @@ module Dry
86
84
  elsif type === value # rubocop:disable Style/CaseEquality
87
85
  instance_variable_set(ivar, coerce.call(value))
88
86
  else
89
- raise InvalidClassAttributeValue.new(name, value)
87
+ raise InvalidClassAttributeValueError.new(name, value)
90
88
  end
91
89
  end
92
90
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Core
5
+ class Container
6
+ # @api public
7
+ class Config
8
+ DEFAULT_NAMESPACE_SEPARATOR = "."
9
+ DEFAULT_RESOLVER = Resolver.new
10
+ DEFAULT_REGISTRY = Registry.new
11
+
12
+ # @api public
13
+ attr_accessor :namespace_separator
14
+
15
+ # @api public
16
+ attr_accessor :resolver
17
+
18
+ # @api public
19
+ attr_accessor :registry
20
+
21
+ # @api private
22
+ def initialize(
23
+ namespace_separator: DEFAULT_NAMESPACE_SEPARATOR,
24
+ resolver: DEFAULT_RESOLVER,
25
+ registry: DEFAULT_REGISTRY
26
+ )
27
+ @namespace_separator = namespace_separator
28
+ @resolver = resolver
29
+ @registry = registry
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Core
5
+ class Container
6
+ # @api public
7
+ module Configuration
8
+ # Use dry/configurable if it's available
9
+ begin
10
+ require "dry/configurable"
11
+
12
+ # @api private
13
+ def self.extended(klass)
14
+ super
15
+ klass.class_eval do
16
+ extend Dry::Configurable
17
+
18
+ setting :namespace_separator, default: Config::DEFAULT_NAMESPACE_SEPARATOR
19
+ setting :resolver, default: Config::DEFAULT_RESOLVER
20
+ setting :registry, default: Config::DEFAULT_REGISTRY
21
+ end
22
+ end
23
+ rescue LoadError
24
+ # @api private
25
+ def config
26
+ @config ||= Container::Config.new
27
+ end
28
+ end
29
+
30
+ # @api private
31
+ def configure
32
+ yield config
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Core
5
+ class Container
6
+ class Item
7
+ # Callable class to returns a item call
8
+ #
9
+ # @api public
10
+ #
11
+ class Callable < Item
12
+ # Returns the result of item call or item
13
+ #
14
+ # @return [Mixed]
15
+ def call
16
+ callable? ? item.call : item
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Core
5
+ class Container
6
+ class Item
7
+ # Factory for create an Item to register inside of container
8
+ #
9
+ # @api public
10
+ class Factory
11
+ # Creates an Item Memoizable or Callable
12
+ # @param [Mixed] item
13
+ # @param [Hash] options
14
+ #
15
+ # @raise [Dry::Core::Container::Error]
16
+ #
17
+ # @return [Dry::Core::Container::Item::Base]
18
+ def call(item, options = {})
19
+ if options[:memoize]
20
+ Item::Memoizable.new(item, options)
21
+ else
22
+ Item::Callable.new(item, options)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Core
5
+ class Container
6
+ class Item
7
+ # Memoizable class to store and execute item calls
8
+ #
9
+ # @api public
10
+ #
11
+ class Memoizable < Item
12
+ # @return [Mutex] the stored mutex
13
+ attr_reader :memoize_mutex
14
+
15
+ # Returns a new Memoizable instance
16
+ #
17
+ # @param [Mixed] item
18
+ # @param [Hash] options
19
+ #
20
+ # @raise [Dry::Core::Container::Error]
21
+ #
22
+ # @return [Dry::Core::Container::Item::Base]
23
+ def initialize(item, options = {})
24
+ super
25
+ raise_not_supported_error unless callable?
26
+
27
+ @memoize_mutex = ::Mutex.new
28
+ end
29
+
30
+ # Returns the result of item call using a syncronized mutex
31
+ #
32
+ # @return [Dry::Core::Container::Item::Base]
33
+ def call
34
+ memoize_mutex.synchronize do
35
+ @memoized_item ||= item.call
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # @private
42
+ def raise_not_supported_error
43
+ raise ::Dry::Core::Container::Error, "Memoize only supported for a block or a proc"
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Core
5
+ class Container
6
+ # Base class to abstract Memoizable and Callable implementations
7
+ #
8
+ # @api abstract
9
+ #
10
+ class Item
11
+ # @return [Mixed] the item to be solved later
12
+ attr_reader :item
13
+
14
+ # @return [Hash] the options to memoize, call or no.
15
+ attr_reader :options
16
+
17
+ # @api abstract
18
+ def initialize(item, options = {})
19
+ @item = item
20
+ @options = {
21
+ call: item.is_a?(::Proc) && item.parameters.empty?
22
+ }.merge(options)
23
+ end
24
+
25
+ # @api abstract
26
+ def call
27
+ raise NotImplementedError
28
+ end
29
+
30
+ # @private
31
+ def value?
32
+ !callable?
33
+ end
34
+
35
+ # @private
36
+ def callable?
37
+ options[:call]
38
+ end
39
+
40
+ # Build a new item with transformation applied
41
+ #
42
+ # @private
43
+ def map(func)
44
+ if callable?
45
+ self.class.new(-> { func.(item.call) }, options)
46
+ else
47
+ self.class.new(func.(item), options)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -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
@@ -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}"
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/core/deprecations"
4
-
5
3
  module Dry
6
4
  module Core
7
5
  module Memoizable
@@ -18,7 +16,7 @@ module Dry
18
16
  super
19
17
 
20
18
  memoizer = base.ancestors.find { _1.is_a?(Memoizer) }
21
- base.prepend(memoizer.dup)
19
+ base.prepend(memoizer.dup) if memoizer
22
20
  end
23
21
  end
24
22
 
@@ -91,7 +89,7 @@ module Dry
91
89
  define_method(method.name) do
92
90
  value = super()
93
91
 
94
- if kernel[:frozen].bind(self).call
92
+ if kernel[:frozen].bind_call(self)
95
93
  # It's not possible to modify singleton classes
96
94
  # of frozen objects
97
95
  mod.remove_method(method.name)
@@ -113,8 +111,8 @@ module Dry
113
111
  # expect :)
114
112
  attr_name = :"__memozed_#{key}__"
115
113
  ivar_name = :"@#{attr_name}"
116
- kernel[:ivar_set].bind(self).(ivar_name, value)
117
- eigenclass = kernel[:singleton].bind(self).call
114
+ kernel[:ivar_set].bind_call(self, ivar_name, value)
115
+ eigenclass = kernel[:singleton].bind_call(self)
118
116
  eigenclass.attr_reader(attr_name)
119
117
  eigenclass.alias_method(method.name, attr_name)
120
118
  eigenclass.remove_method(attr_name)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module Core
5
- VERSION = "0.8.1"
5
+ VERSION = "1.0.0"
6
6
  end
7
7
  end
data/lib/dry/core.rb CHANGED
@@ -1,10 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "zeitwerk"
4
+
5
+ require "dry/core/constants"
6
+ require "dry/core/errors"
3
7
  require "dry/core/version"
4
8
 
5
9
  # :nodoc:
6
10
  module Dry
7
11
  # :nodoc:
8
12
  module Core
13
+ include Constants
14
+
15
+ def self.loader
16
+ @loader ||= Zeitwerk::Loader.new.tap do |loader|
17
+ root = File.expand_path("..", __dir__)
18
+ loader.tag = "dry-core"
19
+ loader.inflector = Zeitwerk::GemInflector.new("#{root}/dry-core.rb")
20
+ loader.push_dir(root)
21
+ loader.ignore(
22
+ "#{root}/dry-core.rb",
23
+ "#{root}/dry/core/{constants,errors,version}.rb"
24
+ )
25
+ loader.inflector.inflect("namespace_dsl" => "NamespaceDSL")
26
+ end
27
+ end
28
+
29
+ loader.setup
30
+ end
31
+
32
+ # See dry/core/equalizer.rb
33
+ unless singleton_class.method_defined?(:Equalizer)
34
+ # Build an equalizer module for the inclusion in other class
35
+ #
36
+ # ## Credits
37
+ #
38
+ # Equalizer has been originally imported from the equalizer gem created by Dan Kubb
39
+ #
40
+ # @api public
41
+ def self.Equalizer(*keys, **options)
42
+ Dry::Core::Equalizer.new(*keys, **options)
43
+ end
9
44
  end
10
45
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikita Shilnikov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-27 00:00:00.000000000 Z
11
+ date: 2022-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: zeitwerk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.6'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -84,6 +98,19 @@ files:
84
98
  - lib/dry/core/class_attributes.rb
85
99
  - lib/dry/core/class_builder.rb
86
100
  - lib/dry/core/constants.rb
101
+ - lib/dry/core/container.rb
102
+ - lib/dry/core/container/config.rb
103
+ - lib/dry/core/container/configuration.rb
104
+ - lib/dry/core/container/item.rb
105
+ - lib/dry/core/container/item/callable.rb
106
+ - lib/dry/core/container/item/factory.rb
107
+ - lib/dry/core/container/item/memoizable.rb
108
+ - lib/dry/core/container/mixin.rb
109
+ - lib/dry/core/container/namespace.rb
110
+ - lib/dry/core/container/namespace_dsl.rb
111
+ - lib/dry/core/container/registry.rb
112
+ - lib/dry/core/container/resolver.rb
113
+ - lib/dry/core/container/stub.rb
87
114
  - lib/dry/core/deprecations.rb
88
115
  - lib/dry/core/descendants_tracker.rb
89
116
  - lib/dry/core/equalizer.rb