dry-core 0.4.9 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|