dry-core 0.4.9 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +150 -34
- data/LICENSE +20 -0
- data/README.md +15 -47
- data/dry-core.gemspec +29 -28
- data/lib/dry-core.rb +1 -1
- data/lib/dry/core.rb +1 -1
- data/lib/dry/core/cache.rb +3 -2
- data/lib/dry/core/class_attributes.rb +31 -11
- data/lib/dry/core/class_builder.rb +3 -7
- data/lib/dry/core/constants.rb +11 -9
- data/lib/dry/core/deprecations.rb +32 -21
- data/lib/dry/core/descendants_tracker.rb +1 -1
- data/lib/dry/core/equalizer.rb +152 -0
- data/lib/dry/core/extensions.rb +1 -1
- data/lib/dry/core/inflector.rb +4 -3
- data/lib/dry/core/memoizable.rb +170 -37
- data/lib/dry/core/version.rb +1 -1
- metadata +21 -25
- data/.codeclimate.yml +0 -15
- data/.gitignore +0 -11
- data/.inch.yml +0 -4
- data/.rspec +0 -3
- data/.travis.yml +0 -34
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -21
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -8
data/lib/dry-core.rb
CHANGED
data/lib/dry/core.rb
CHANGED
data/lib/dry/core/cache.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "concurrent/map"
|
4
4
|
|
5
5
|
module Dry
|
6
6
|
module Core
|
@@ -46,7 +46,8 @@ module Dry
|
|
46
46
|
# this means you shouldn't pass Procs in args unless you're sure
|
47
47
|
# they are always the same instances, otherwise you introduce a memory leak
|
48
48
|
#
|
49
|
-
# @return [Object] block's return value (cached for subsequent calls with
|
49
|
+
# @return [Object] block's return value (cached for subsequent calls with
|
50
|
+
# the same argument values)
|
50
51
|
def fetch_or_store(*args, &block)
|
51
52
|
cache.fetch_or_store(args.hash, &block)
|
52
53
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "dry/core/constants"
|
4
|
+
require "dry/core/errors"
|
5
5
|
|
6
6
|
module Dry
|
7
7
|
module Core
|
@@ -51,22 +51,42 @@ module Dry
|
|
51
51
|
# defines :one, :two, type: Dry::Types['strict.int']
|
52
52
|
# end
|
53
53
|
#
|
54
|
-
|
55
|
-
|
54
|
+
# @example with coercion using Proc
|
55
|
+
#
|
56
|
+
# class Bar
|
57
|
+
# extend Dry::Core::ClassAttributes
|
58
|
+
#
|
59
|
+
# defines :one, coerce: proc { |value| value.to_s }
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @example with coercion using dry-types
|
63
|
+
#
|
64
|
+
# class Bar
|
65
|
+
# extend Dry::Core::ClassAttributes
|
66
|
+
#
|
67
|
+
# defines :one, coerce: Dry::Types['coercible.string']
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
def defines(*args, type: ::Object, coerce: IDENTITY) # rubocop:disable Metrics/PerceivedComplexity
|
71
|
+
unless coerce.respond_to?(:call)
|
72
|
+
raise ::ArgumentError, "Non-callable coerce option: #{coerce.inspect}"
|
73
|
+
end
|
74
|
+
|
75
|
+
mod = ::Module.new do
|
56
76
|
args.each do |name|
|
57
|
-
|
58
|
-
ivar = "@#{name}"
|
77
|
+
ivar = :"@#{name}"
|
59
78
|
|
60
|
-
|
79
|
+
define_method(name) do |value = Undefined|
|
80
|
+
if Undefined.equal?(value)
|
61
81
|
if instance_variable_defined?(ivar)
|
62
82
|
instance_variable_get(ivar)
|
63
|
-
else
|
83
|
+
else # rubocop:disable Style/EmptyElse
|
64
84
|
nil
|
65
85
|
end
|
86
|
+
elsif type === value # rubocop:disable Style/CaseEquality
|
87
|
+
instance_variable_set(ivar, coerce.call(value))
|
66
88
|
else
|
67
|
-
raise InvalidClassAttributeValue.new(name, value)
|
68
|
-
|
69
|
-
instance_variable_set(ivar, value)
|
89
|
+
raise InvalidClassAttributeValue.new(name, value)
|
70
90
|
end
|
71
91
|
end
|
72
92
|
end
|
@@ -6,9 +6,7 @@ module Dry
|
|
6
6
|
class ClassBuilder
|
7
7
|
ParentClassMismatch = Class.new(TypeError)
|
8
8
|
|
9
|
-
attr_reader :name
|
10
|
-
attr_reader :parent
|
11
|
-
attr_reader :namespace
|
9
|
+
attr_reader :name, :parent, :namespace
|
12
10
|
|
13
11
|
def initialize(name:, parent: nil, namespace: nil)
|
14
12
|
@name = name
|
@@ -72,8 +70,6 @@ module Dry
|
|
72
70
|
remove_const(name)
|
73
71
|
const_set(name, klass)
|
74
72
|
|
75
|
-
const_get(name).name if RUBY_VERSION < '2.4'
|
76
|
-
|
77
73
|
remove_const(name)
|
78
74
|
const_set(name, base)
|
79
75
|
end
|
@@ -85,14 +81,14 @@ module Dry
|
|
85
81
|
def create_base(namespace, name, parent)
|
86
82
|
begin
|
87
83
|
namespace.const_get(name)
|
88
|
-
rescue NameError
|
84
|
+
rescue NameError # rubocop:disable Lint/SuppressedException
|
89
85
|
end
|
90
86
|
|
91
87
|
if namespace.const_defined?(name, false)
|
92
88
|
existing = namespace.const_get(name)
|
93
89
|
|
94
90
|
unless existing <= parent
|
95
|
-
raise ParentClassMismatch, "#{
|
91
|
+
raise ParentClassMismatch, "#{existing.name} must be a subclass of #{parent.name}"
|
96
92
|
end
|
97
93
|
|
98
94
|
existing
|
data/lib/dry/core/constants.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "set"
|
4
4
|
|
5
5
|
module Dry
|
6
6
|
module Core
|
@@ -23,16 +23,18 @@ module Dry
|
|
23
23
|
# An empty list of options
|
24
24
|
EMPTY_OPTS = {}.freeze
|
25
25
|
# An empty set
|
26
|
-
EMPTY_SET = Set.new.freeze
|
26
|
+
EMPTY_SET = ::Set.new.freeze
|
27
27
|
# An empty string
|
28
|
-
EMPTY_STRING =
|
28
|
+
EMPTY_STRING = ""
|
29
|
+
# Identity function
|
30
|
+
IDENTITY = (-> x { x }).freeze
|
29
31
|
|
30
32
|
# A special value you can use as a default to know if no arguments
|
31
|
-
# were passed to
|
33
|
+
# were passed to the method
|
32
34
|
#
|
33
35
|
# @example
|
34
36
|
# def method(value = Undefined)
|
35
|
-
# if value
|
37
|
+
# if Undefined.equal?(value)
|
36
38
|
# puts 'no args'
|
37
39
|
# else
|
38
40
|
# puts value
|
@@ -40,16 +42,16 @@ module Dry
|
|
40
42
|
# end
|
41
43
|
Undefined = Object.new.tap do |undefined|
|
42
44
|
# @api private
|
43
|
-
Self = -> { Undefined }
|
45
|
+
Self = -> { Undefined } # rubocop:disable Lint/ConstantDefinitionInBlock
|
44
46
|
|
45
47
|
# @api public
|
46
48
|
def undefined.to_s
|
47
|
-
|
49
|
+
"Undefined"
|
48
50
|
end
|
49
51
|
|
50
52
|
# @api public
|
51
53
|
def undefined.inspect
|
52
|
-
|
54
|
+
"Undefined"
|
53
55
|
end
|
54
56
|
|
55
57
|
# Pick a value, if the first argument is not Undefined, return it back,
|
@@ -60,7 +62,7 @@ module Dry
|
|
60
62
|
# 1 + Undefined.default(val, 2)
|
61
63
|
# end
|
62
64
|
#
|
63
|
-
def undefined.default(x, y = self)
|
65
|
+
def undefined.default(x, y = self) # rubocop:disable Naming/MethodParameterName
|
64
66
|
if equal?(x)
|
65
67
|
if equal?(y)
|
66
68
|
yield
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "logger"
|
4
4
|
|
5
5
|
module Dry
|
6
6
|
module Core
|
@@ -17,7 +17,7 @@ module Dry
|
|
17
17
|
# def old_api; end
|
18
18
|
# def new_api; end
|
19
19
|
#
|
20
|
-
#
|
20
|
+
# deprecate :old_api, :new_api, message: "old_api is no-no"
|
21
21
|
# end
|
22
22
|
#
|
23
23
|
# @example You also can use this module for your custom messages
|
@@ -33,24 +33,34 @@ module Dry
|
|
33
33
|
# Prints a warning
|
34
34
|
#
|
35
35
|
# @param [String] msg Warning string
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
# @param [String] tag Tag to help identify the source of the warning.
|
37
|
+
# Defaults to "deprecated"
|
38
|
+
# @param [Integer] Caller frame to add to the message
|
39
|
+
def warn(msg, tag: nil, uplevel: nil)
|
40
|
+
caller_info = uplevel.nil? ? nil : "#{caller_locations(uplevel + 2, 1)[0]} "
|
41
|
+
tag = "[#{tag || "deprecated"}] "
|
42
|
+
hint = msg.gsub(/^\s+/, "")
|
43
|
+
|
44
|
+
logger.warn("#{caller_info}#{tag}#{hint}")
|
39
45
|
end
|
40
46
|
|
41
47
|
# Wraps arguments with a standard message format and prints a warning
|
42
48
|
#
|
43
49
|
# @param [Object] name what is deprecated
|
44
50
|
# @param [String] msg additional message usually containing upgrade instructions
|
45
|
-
def announce(name, msg, tag: nil)
|
46
|
-
|
51
|
+
def announce(name, msg, tag: nil, uplevel: nil)
|
52
|
+
# Bump the uplevel (if provided) by one to account for the uplevel calculation
|
53
|
+
# taking place one frame deeper in `.warn`
|
54
|
+
uplevel += 1 if uplevel
|
55
|
+
|
56
|
+
warn(deprecation_message(name, msg), tag: tag, uplevel: uplevel)
|
47
57
|
end
|
48
58
|
|
49
59
|
# @api private
|
50
60
|
def deprecation_message(name, msg)
|
51
61
|
<<-MSG
|
52
|
-
#{
|
53
|
-
#{
|
62
|
+
#{name} is deprecated and will be removed in the next major version
|
63
|
+
#{msg}
|
54
64
|
MSG
|
55
65
|
end
|
56
66
|
|
@@ -92,12 +102,12 @@ module Dry
|
|
92
102
|
# @param [#warn] logger
|
93
103
|
#
|
94
104
|
# @api public
|
95
|
-
def set_logger!(output = $stderr)
|
105
|
+
def set_logger!(output = $stderr) # rubocop:disable Naming/AccessorMethodName
|
96
106
|
if output.respond_to?(:warn)
|
97
107
|
@logger = output
|
98
108
|
else
|
99
109
|
@logger = Logger.new(output).tap do |logger|
|
100
|
-
logger.formatter = proc { |_, _, _, msg| "#{
|
110
|
+
logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
|
101
111
|
end
|
102
112
|
end
|
103
113
|
end
|
@@ -108,8 +118,9 @@ module Dry
|
|
108
118
|
end
|
109
119
|
|
110
120
|
# @api private
|
111
|
-
class Tagged < Module
|
121
|
+
class Tagged < ::Module
|
112
122
|
def initialize(tag)
|
123
|
+
super()
|
113
124
|
@tag = tag
|
114
125
|
end
|
115
126
|
|
@@ -145,8 +156,8 @@ module Dry
|
|
145
156
|
# @option [String] message optional deprecation message
|
146
157
|
def deprecate(old_name, new_name = nil, message: nil)
|
147
158
|
full_msg = Deprecations.deprecated_name_message(
|
148
|
-
"#{
|
149
|
-
new_name ? "#{
|
159
|
+
"#{name}##{old_name}",
|
160
|
+
new_name ? "#{name}##{new_name}" : nil,
|
150
161
|
message
|
151
162
|
)
|
152
163
|
mod = self
|
@@ -155,7 +166,7 @@ module Dry
|
|
155
166
|
undef_method old_name if method_defined?(old_name)
|
156
167
|
|
157
168
|
define_method(old_name) do |*args, &block|
|
158
|
-
mod.warn("#{
|
169
|
+
mod.warn("#{full_msg}\n#{STACK.()}")
|
159
170
|
__send__(new_name, *args, &block)
|
160
171
|
end
|
161
172
|
else
|
@@ -165,7 +176,7 @@ module Dry
|
|
165
176
|
undef_method old_name
|
166
177
|
|
167
178
|
define_method(old_name) do |*args, &block|
|
168
|
-
mod.warn("#{
|
179
|
+
mod.warn("#{full_msg}\n#{STACK.()}")
|
169
180
|
__send__(aliased_name, *args, &block)
|
170
181
|
end
|
171
182
|
end
|
@@ -178,8 +189,8 @@ module Dry
|
|
178
189
|
# @option [String] message optional deprecation message
|
179
190
|
def deprecate_class_method(old_name, new_name = nil, message: nil)
|
180
191
|
full_msg = Deprecations.deprecated_name_message(
|
181
|
-
"#{
|
182
|
-
new_name ? "#{
|
192
|
+
"#{name}.#{old_name}",
|
193
|
+
new_name ? "#{name}.#{new_name}" : nil,
|
183
194
|
message
|
184
195
|
)
|
185
196
|
|
@@ -189,7 +200,7 @@ module Dry
|
|
189
200
|
undef_method old_name if method_defined?(old_name)
|
190
201
|
|
191
202
|
define_method(old_name) do |*args, &block|
|
192
|
-
warn("#{
|
203
|
+
warn("#{full_msg}\n#{STACK.()}")
|
193
204
|
meth.call(*args, &block)
|
194
205
|
end
|
195
206
|
end
|
@@ -203,14 +214,14 @@ module Dry
|
|
203
214
|
remove_const(constant_name)
|
204
215
|
|
205
216
|
full_msg = Deprecations.deprecated_name_message(
|
206
|
-
"#{
|
217
|
+
"#{name}::#{constant_name}",
|
207
218
|
message
|
208
219
|
)
|
209
220
|
|
210
221
|
mod = Module.new do
|
211
222
|
define_method(:const_missing) do |missing|
|
212
223
|
if missing == constant_name
|
213
|
-
warn("#{
|
224
|
+
warn("#{full_msg}\n#{STACK.()}")
|
214
225
|
value
|
215
226
|
else
|
216
227
|
super(missing)
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
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
|
+
module Core
|
16
|
+
# Define equality, equivalence and inspection methods
|
17
|
+
class Equalizer < ::Module
|
18
|
+
# Initialize an Equalizer with the given keys
|
19
|
+
#
|
20
|
+
# Will use the keys with which it is initialized to define #cmp?,
|
21
|
+
# #hash, and #inspect
|
22
|
+
#
|
23
|
+
# @param [Array<Symbol>] keys
|
24
|
+
# @param [Hash] options
|
25
|
+
# @option options [Boolean] :inspect whether to define #inspect method
|
26
|
+
# @option options [Boolean] :immutable whether to memoize #hash method
|
27
|
+
#
|
28
|
+
# @return [undefined]
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
def initialize(*keys, **options)
|
32
|
+
super()
|
33
|
+
@keys = keys.uniq
|
34
|
+
define_methods(**options)
|
35
|
+
freeze
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Hook called when module is included
|
41
|
+
#
|
42
|
+
# @param [Module] descendant
|
43
|
+
# the module or class including Equalizer
|
44
|
+
#
|
45
|
+
# @return [self]
|
46
|
+
#
|
47
|
+
# @api private
|
48
|
+
def included(descendant)
|
49
|
+
super
|
50
|
+
descendant.include Methods
|
51
|
+
end
|
52
|
+
|
53
|
+
# Define the equalizer methods based on #keys
|
54
|
+
#
|
55
|
+
# @param [Boolean] inspect whether to define #inspect method
|
56
|
+
# @param [Boolean] immutable whether to memoize #hash method
|
57
|
+
#
|
58
|
+
# @return [undefined]
|
59
|
+
#
|
60
|
+
# @api private
|
61
|
+
def define_methods(inspect: true, immutable: false)
|
62
|
+
define_cmp_method
|
63
|
+
define_hash_method(immutable: immutable)
|
64
|
+
define_inspect_method if inspect
|
65
|
+
end
|
66
|
+
|
67
|
+
# Define an #cmp? method based on the instance's values identified by #keys
|
68
|
+
#
|
69
|
+
# @return [undefined]
|
70
|
+
#
|
71
|
+
# @api private
|
72
|
+
def define_cmp_method
|
73
|
+
keys = @keys
|
74
|
+
define_method(:cmp?) do |comparator, other|
|
75
|
+
keys.all? do |key|
|
76
|
+
__send__(key).public_send(comparator, other.__send__(key))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
private :cmp?
|
80
|
+
end
|
81
|
+
|
82
|
+
# Define a #hash method based on the instance's values identified by #keys
|
83
|
+
#
|
84
|
+
# @return [undefined]
|
85
|
+
#
|
86
|
+
# @api private
|
87
|
+
def define_hash_method(immutable:)
|
88
|
+
calculate_hash = ->(obj) { @keys.map { |key| obj.__send__(key) }.push(obj.class).hash }
|
89
|
+
if immutable
|
90
|
+
define_method(:hash) do
|
91
|
+
@__hash__ ||= calculate_hash.call(self)
|
92
|
+
end
|
93
|
+
define_method(:freeze) do
|
94
|
+
hash
|
95
|
+
super()
|
96
|
+
end
|
97
|
+
else
|
98
|
+
define_method(:hash) do
|
99
|
+
calculate_hash.call(self)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Define an inspect method that reports the values of the instance's keys
|
105
|
+
#
|
106
|
+
# @return [undefined]
|
107
|
+
#
|
108
|
+
# @api private
|
109
|
+
def define_inspect_method
|
110
|
+
keys = @keys
|
111
|
+
define_method(:inspect) do
|
112
|
+
klass = self.class
|
113
|
+
name = klass.name || klass.inspect
|
114
|
+
"#<#{name}#{keys.map { |key| " #{key}=#{__send__(key).inspect}" }.join}>"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# The comparison methods
|
119
|
+
module Methods
|
120
|
+
# Compare the object with other object for equality
|
121
|
+
#
|
122
|
+
# @example
|
123
|
+
# object.eql?(other) # => true or false
|
124
|
+
#
|
125
|
+
# @param [Object] other
|
126
|
+
# the other object to compare with
|
127
|
+
#
|
128
|
+
# @return [Boolean]
|
129
|
+
#
|
130
|
+
# @api public
|
131
|
+
def eql?(other)
|
132
|
+
instance_of?(other.class) && cmp?(__method__, other)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Compare the object with other object for equivalency
|
136
|
+
#
|
137
|
+
# @example
|
138
|
+
# object == other # => true or false
|
139
|
+
#
|
140
|
+
# @param [Object] other
|
141
|
+
# the other object to compare with
|
142
|
+
#
|
143
|
+
# @return [Boolean]
|
144
|
+
#
|
145
|
+
# @api public
|
146
|
+
def ==(other)
|
147
|
+
other.is_a?(self.class) && cmp?(__method__, other)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|