dry-core 0.4.7 → 0.6.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 +4 -4
- data/CHANGELOG.md +162 -43
- data/LICENSE +20 -0
- data/README.md +18 -36
- data/dry-core.gemspec +31 -28
- data/lib/dry-core.rb +3 -1
- data/lib/dry/core.rb +3 -1
- data/lib/dry/core/cache.rb +3 -1
- data/lib/dry/core/class_attributes.rb +32 -10
- data/lib/dry/core/class_builder.rb +3 -3
- data/lib/dry/core/constants.rb +52 -12
- data/lib/dry/core/deprecations.rb +23 -14
- data/lib/dry/core/descendants_tracker.rb +3 -1
- data/lib/dry/core/equalizer.rb +151 -0
- data/lib/dry/core/errors.rb +2 -0
- data/lib/dry/core/extensions.rb +3 -1
- data/lib/dry/core/inflector.rb +6 -3
- data/lib/dry/core/memoizable.rb +75 -35
- data/lib/dry/core/version.rb +3 -1
- metadata +25 -30
- data/.gitignore +0 -10
- data/.inch.yml +0 -4
- data/.rspec +0 -3
- data/.rubocop.yml +0 -31
- data/.travis.yml +0 -31
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -22
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -6
data/lib/dry-core.rb
CHANGED
data/lib/dry/core.rb
CHANGED
data/lib/dry/core/cache.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/core/constants"
|
4
|
+
require "dry/core/errors"
|
3
5
|
|
4
6
|
module Dry
|
5
7
|
module Core
|
@@ -49,22 +51,42 @@ module Dry
|
|
49
51
|
# defines :one, :two, type: Dry::Types['strict.int']
|
50
52
|
# end
|
51
53
|
#
|
52
|
-
|
53
|
-
|
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)
|
71
|
+
unless coerce.respond_to?(:call)
|
72
|
+
raise ::ArgumentError, "Non-callable coerce option: #{coerce.inspect}"
|
73
|
+
end
|
74
|
+
|
75
|
+
mod = ::Module.new do
|
54
76
|
args.each do |name|
|
55
|
-
|
56
|
-
ivar = "@#{name}"
|
77
|
+
ivar = :"@#{name}"
|
57
78
|
|
58
|
-
|
79
|
+
define_method(name) do |value = Undefined|
|
80
|
+
if Undefined.equal?(value)
|
59
81
|
if instance_variable_defined?(ivar)
|
60
82
|
instance_variable_get(ivar)
|
61
83
|
else
|
62
84
|
nil
|
63
85
|
end
|
86
|
+
elsif type === value
|
87
|
+
instance_variable_set(ivar, coerce.call(value))
|
64
88
|
else
|
65
|
-
raise InvalidClassAttributeValue.new(name, value)
|
66
|
-
|
67
|
-
instance_variable_set(ivar, value)
|
89
|
+
raise InvalidClassAttributeValue.new(name, value)
|
68
90
|
end
|
69
91
|
end
|
70
92
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Dry
|
2
4
|
module Core
|
3
5
|
# Class for generating more classes
|
@@ -70,8 +72,6 @@ module Dry
|
|
70
72
|
remove_const(name)
|
71
73
|
const_set(name, klass)
|
72
74
|
|
73
|
-
const_get(name).name if RUBY_VERSION < '2.4'
|
74
|
-
|
75
75
|
remove_const(name)
|
76
76
|
const_set(name, base)
|
77
77
|
end
|
@@ -90,7 +90,7 @@ module Dry
|
|
90
90
|
existing = namespace.const_get(name)
|
91
91
|
|
92
92
|
unless existing <= parent
|
93
|
-
raise ParentClassMismatch, "#{
|
93
|
+
raise ParentClassMismatch, "#{existing.name} must be a subclass of #{parent.name}"
|
94
94
|
end
|
95
95
|
|
96
96
|
existing
|
data/lib/dry/core/constants.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
2
4
|
|
3
5
|
module Dry
|
4
6
|
module Core
|
@@ -6,6 +8,7 @@ module Dry
|
|
6
8
|
#
|
7
9
|
# @example Just include this module to your class or module
|
8
10
|
# class Foo
|
11
|
+
# include Dry::Core::Constants
|
9
12
|
# def call(value = EMPTY_ARRAY)
|
10
13
|
# value.map(&:to_s)
|
11
14
|
# end
|
@@ -20,41 +23,48 @@ module Dry
|
|
20
23
|
# An empty list of options
|
21
24
|
EMPTY_OPTS = {}.freeze
|
22
25
|
# An empty set
|
23
|
-
EMPTY_SET = Set.new.freeze
|
26
|
+
EMPTY_SET = ::Set.new.freeze
|
24
27
|
# An empty string
|
25
|
-
EMPTY_STRING =
|
28
|
+
EMPTY_STRING = "".freeze
|
29
|
+
# Identity function
|
30
|
+
IDENTITY = (-> x { x }).freeze
|
26
31
|
|
27
32
|
# A special value you can use as a default to know if no arguments
|
28
|
-
# were passed to
|
33
|
+
# were passed to the method
|
29
34
|
#
|
30
35
|
# @example
|
31
36
|
# def method(value = Undefined)
|
32
|
-
# if value
|
37
|
+
# if Undefined.equal?(value)
|
33
38
|
# puts 'no args'
|
34
39
|
# else
|
35
40
|
# puts value
|
36
41
|
# end
|
37
42
|
# end
|
38
43
|
Undefined = Object.new.tap do |undefined|
|
44
|
+
# @api private
|
45
|
+
Self = -> { Undefined }
|
46
|
+
|
47
|
+
# @api public
|
39
48
|
def undefined.to_s
|
40
|
-
|
49
|
+
"Undefined"
|
41
50
|
end
|
42
51
|
|
52
|
+
# @api public
|
43
53
|
def undefined.inspect
|
44
|
-
|
54
|
+
"Undefined"
|
45
55
|
end
|
46
56
|
|
47
57
|
# Pick a value, if the first argument is not Undefined, return it back,
|
48
58
|
# otherwise return the second arg or yield the block.
|
49
59
|
#
|
50
60
|
# @example
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
61
|
+
# def method(val = Undefined)
|
62
|
+
# 1 + Undefined.default(val, 2)
|
63
|
+
# end
|
54
64
|
#
|
55
65
|
def undefined.default(x, y = self)
|
56
|
-
if
|
57
|
-
if
|
66
|
+
if equal?(x)
|
67
|
+
if equal?(y)
|
58
68
|
yield
|
59
69
|
else
|
60
70
|
y
|
@@ -63,6 +73,36 @@ module Dry
|
|
63
73
|
x
|
64
74
|
end
|
65
75
|
end
|
76
|
+
|
77
|
+
# Map a non-undefined value
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# def add_five(val = Undefined)
|
81
|
+
# Undefined.map(val) { |x| x + 5 }
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
def undefined.map(value)
|
85
|
+
if equal?(value)
|
86
|
+
self
|
87
|
+
else
|
88
|
+
yield(value)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# @api public
|
93
|
+
def undefined.dup
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
# @api public
|
98
|
+
def undefined.clone
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
# @api public
|
103
|
+
def undefined.coalesce(*args)
|
104
|
+
args.find(Self) { |x| !equal?(x) }
|
105
|
+
end
|
66
106
|
end.freeze
|
67
107
|
|
68
108
|
def self.included(base)
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
2
4
|
|
3
5
|
module Dry
|
4
6
|
module Core
|
@@ -15,7 +17,7 @@ module Dry
|
|
15
17
|
# def old_api; end
|
16
18
|
# def new_api; end
|
17
19
|
#
|
18
|
-
#
|
20
|
+
# deprecate :old_api, :new_api, message: "old_api is no-no"
|
19
21
|
# end
|
20
22
|
#
|
21
23
|
# @example You also can use this module for your custom messages
|
@@ -31,24 +33,31 @@ module Dry
|
|
31
33
|
# Prints a warning
|
32
34
|
#
|
33
35
|
# @param [String] msg Warning string
|
34
|
-
|
35
|
-
|
36
|
-
|
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[uplevel]
|
41
|
+
tag = "[#{tag || "deprecated"}]"
|
42
|
+
hint = msg.gsub(/^\s+/, "")
|
43
|
+
logger.warn(
|
44
|
+
[caller_info, tag, hint].compact.join(" ")
|
45
|
+
)
|
37
46
|
end
|
38
47
|
|
39
48
|
# Wraps arguments with a standard message format and prints a warning
|
40
49
|
#
|
41
50
|
# @param [Object] name what is deprecated
|
42
51
|
# @param [String] msg additional message usually containing upgrade instructions
|
43
|
-
def announce(name, msg, tag: nil)
|
44
|
-
warn(deprecation_message(name, msg), tag: tag)
|
52
|
+
def announce(name, msg, tag: nil, uplevel: nil)
|
53
|
+
warn(deprecation_message(name, msg), tag: tag, uplevel: uplevel)
|
45
54
|
end
|
46
55
|
|
47
56
|
# @api private
|
48
57
|
def deprecation_message(name, msg)
|
49
58
|
<<-MSG
|
50
|
-
#{
|
51
|
-
#{
|
59
|
+
#{name} is deprecated and will be removed in the next major version
|
60
|
+
#{msg}
|
52
61
|
MSG
|
53
62
|
end
|
54
63
|
|
@@ -95,7 +104,7 @@ module Dry
|
|
95
104
|
@logger = output
|
96
105
|
else
|
97
106
|
@logger = Logger.new(output).tap do |logger|
|
98
|
-
logger.formatter = proc { |_, _, _, msg| "#{
|
107
|
+
logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
|
99
108
|
end
|
100
109
|
end
|
101
110
|
end
|
@@ -153,7 +162,7 @@ module Dry
|
|
153
162
|
undef_method old_name if method_defined?(old_name)
|
154
163
|
|
155
164
|
define_method(old_name) do |*args, &block|
|
156
|
-
mod.warn("#{
|
165
|
+
mod.warn("#{full_msg}\n#{STACK.()}")
|
157
166
|
__send__(new_name, *args, &block)
|
158
167
|
end
|
159
168
|
else
|
@@ -163,7 +172,7 @@ module Dry
|
|
163
172
|
undef_method old_name
|
164
173
|
|
165
174
|
define_method(old_name) do |*args, &block|
|
166
|
-
mod.warn("#{
|
175
|
+
mod.warn("#{full_msg}\n#{STACK.()}")
|
167
176
|
__send__(aliased_name, *args, &block)
|
168
177
|
end
|
169
178
|
end
|
@@ -187,7 +196,7 @@ module Dry
|
|
187
196
|
undef_method old_name if method_defined?(old_name)
|
188
197
|
|
189
198
|
define_method(old_name) do |*args, &block|
|
190
|
-
warn("#{
|
199
|
+
warn("#{full_msg}\n#{STACK.()}")
|
191
200
|
meth.call(*args, &block)
|
192
201
|
end
|
193
202
|
end
|
@@ -208,7 +217,7 @@ module Dry
|
|
208
217
|
mod = Module.new do
|
209
218
|
define_method(:const_missing) do |missing|
|
210
219
|
if missing == constant_name
|
211
|
-
warn("#{
|
220
|
+
warn("#{full_msg}\n#{STACK.()}")
|
212
221
|
value
|
213
222
|
else
|
214
223
|
super(missing)
|
@@ -0,0 +1,151 @@
|
|
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
|
+
@keys = keys.uniq
|
33
|
+
define_methods(**options)
|
34
|
+
freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Hook called when module is included
|
40
|
+
#
|
41
|
+
# @param [Module] descendant
|
42
|
+
# the module or class including Equalizer
|
43
|
+
#
|
44
|
+
# @return [self]
|
45
|
+
#
|
46
|
+
# @api private
|
47
|
+
def included(descendant)
|
48
|
+
super
|
49
|
+
descendant.include Methods
|
50
|
+
end
|
51
|
+
|
52
|
+
# Define the equalizer methods based on #keys
|
53
|
+
#
|
54
|
+
# @param [Boolean] inspect whether to define #inspect method
|
55
|
+
# @param [Boolean] immutable whether to memoize #hash method
|
56
|
+
#
|
57
|
+
# @return [undefined]
|
58
|
+
#
|
59
|
+
# @api private
|
60
|
+
def define_methods(inspect: true, immutable: false)
|
61
|
+
define_cmp_method
|
62
|
+
define_hash_method(immutable: immutable)
|
63
|
+
define_inspect_method if inspect
|
64
|
+
end
|
65
|
+
|
66
|
+
# Define an #cmp? method based on the instance's values identified by #keys
|
67
|
+
#
|
68
|
+
# @return [undefined]
|
69
|
+
#
|
70
|
+
# @api private
|
71
|
+
def define_cmp_method
|
72
|
+
keys = @keys
|
73
|
+
define_method(:cmp?) do |comparator, other|
|
74
|
+
keys.all? do |key|
|
75
|
+
__send__(key).public_send(comparator, other.__send__(key))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
private :cmp?
|
79
|
+
end
|
80
|
+
|
81
|
+
# Define a #hash method based on the instance's values identified by #keys
|
82
|
+
#
|
83
|
+
# @return [undefined]
|
84
|
+
#
|
85
|
+
# @api private
|
86
|
+
def define_hash_method(immutable:)
|
87
|
+
calculate_hash = ->(obj) { @keys.map { |key| obj.__send__(key) }.push(obj.class).hash }
|
88
|
+
if immutable
|
89
|
+
define_method(:hash) do
|
90
|
+
@__hash__ ||= calculate_hash.call(self)
|
91
|
+
end
|
92
|
+
define_method(:freeze) do
|
93
|
+
hash
|
94
|
+
super()
|
95
|
+
end
|
96
|
+
else
|
97
|
+
define_method(:hash) do
|
98
|
+
calculate_hash.call(self)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Define an inspect method that reports the values of the instance's keys
|
104
|
+
#
|
105
|
+
# @return [undefined]
|
106
|
+
#
|
107
|
+
# @api private
|
108
|
+
def define_inspect_method
|
109
|
+
keys = @keys
|
110
|
+
define_method(:inspect) do
|
111
|
+
klass = self.class
|
112
|
+
name = klass.name || klass.inspect
|
113
|
+
"#<#{name}#{keys.map { |key| " #{key}=#{__send__(key).inspect}" }.join}>"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# The comparison methods
|
118
|
+
module Methods
|
119
|
+
# Compare the object with other object for equality
|
120
|
+
#
|
121
|
+
# @example
|
122
|
+
# object.eql?(other) # => true or false
|
123
|
+
#
|
124
|
+
# @param [Object] other
|
125
|
+
# the other object to compare with
|
126
|
+
#
|
127
|
+
# @return [Boolean]
|
128
|
+
#
|
129
|
+
# @api public
|
130
|
+
def eql?(other)
|
131
|
+
instance_of?(other.class) && cmp?(__method__, other)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Compare the object with other object for equivalency
|
135
|
+
#
|
136
|
+
# @example
|
137
|
+
# object == other # => true or false
|
138
|
+
#
|
139
|
+
# @param [Object] other
|
140
|
+
# the other object to compare with
|
141
|
+
#
|
142
|
+
# @return [Boolean]
|
143
|
+
#
|
144
|
+
# @api public
|
145
|
+
def ==(other)
|
146
|
+
other.is_a?(self.class) && cmp?(__method__, other)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|