dry-core 0.4.7
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 +7 -0
- data/.gitignore +10 -0
- data/.inch.yml +4 -0
- data/.rspec +3 -0
- data/.rubocop.yml +31 -0
- data/.travis.yml +31 -0
- data/CHANGELOG.md +153 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +47 -0
- data/Rakefile +6 -0
- data/dry-core.gemspec +34 -0
- data/lib/dry-core.rb +1 -0
- data/lib/dry/core.rb +8 -0
- data/lib/dry/core/cache.rb +66 -0
- data/lib/dry/core/class_attributes.rb +83 -0
- data/lib/dry/core/class_builder.rb +105 -0
- data/lib/dry/core/constants.rb +77 -0
- data/lib/dry/core/deprecations.rb +224 -0
- data/lib/dry/core/descendants_tracker.rb +76 -0
- data/lib/dry/core/errors.rb +11 -0
- data/lib/dry/core/extensions.rb +61 -0
- data/lib/dry/core/inflector.rb +138 -0
- data/lib/dry/core/memoizable.rb +68 -0
- data/lib/dry/core/version.rb +5 -0
- metadata +126 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'dry/core/constants'
|
2
|
+
require 'dry/core/errors'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Core
|
6
|
+
# Internal support module for class-level settings
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
module ClassAttributes
|
10
|
+
include Constants
|
11
|
+
|
12
|
+
# Specify what attributes a class will use
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# class ExtraClass
|
16
|
+
# extend Dry::Core::ClassAttributes
|
17
|
+
#
|
18
|
+
# defines :hello
|
19
|
+
#
|
20
|
+
# hello 'world'
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @example with inheritance and type checking
|
24
|
+
#
|
25
|
+
# class MyClass
|
26
|
+
# extend Dry::Core::ClassAttributes
|
27
|
+
#
|
28
|
+
# defines :one, :two, type: Integer
|
29
|
+
#
|
30
|
+
# one 1
|
31
|
+
# two 2
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# class OtherClass < MyClass
|
35
|
+
# two 3
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# MyClass.one # => 1
|
39
|
+
# MyClass.two # => 2
|
40
|
+
#
|
41
|
+
# OtherClass.one # => 1
|
42
|
+
# OtherClass.two # => 3
|
43
|
+
#
|
44
|
+
# @example with dry-types
|
45
|
+
#
|
46
|
+
# class Foo
|
47
|
+
# extend Dry::Core::ClassAttributes
|
48
|
+
#
|
49
|
+
# defines :one, :two, type: Dry::Types['strict.int']
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
def defines(*args, type: Object)
|
53
|
+
mod = Module.new do
|
54
|
+
args.each do |name|
|
55
|
+
define_method(name) do |value = Undefined|
|
56
|
+
ivar = "@#{name}"
|
57
|
+
|
58
|
+
if value == Undefined
|
59
|
+
if instance_variable_defined?(ivar)
|
60
|
+
instance_variable_get(ivar)
|
61
|
+
else
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
else
|
65
|
+
raise InvalidClassAttributeValue.new(name, value) unless type === value
|
66
|
+
|
67
|
+
instance_variable_set(ivar, value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
define_method(:inherited) do |klass|
|
73
|
+
args.each { |name| klass.send(name, send(name)) }
|
74
|
+
|
75
|
+
super(klass)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
extend(mod)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Dry
|
2
|
+
module Core
|
3
|
+
# Class for generating more classes
|
4
|
+
class ClassBuilder
|
5
|
+
ParentClassMismatch = Class.new(TypeError)
|
6
|
+
|
7
|
+
attr_reader :name
|
8
|
+
attr_reader :parent
|
9
|
+
attr_reader :namespace
|
10
|
+
|
11
|
+
def initialize(name:, parent: nil, namespace: nil)
|
12
|
+
@name = name
|
13
|
+
@namespace = namespace
|
14
|
+
@parent = parent || Object
|
15
|
+
end
|
16
|
+
|
17
|
+
# Generate a class based on options
|
18
|
+
#
|
19
|
+
# @example Create anonymous class
|
20
|
+
# builder = Dry::Core::ClassBuilder.new(name: 'MyClass')
|
21
|
+
#
|
22
|
+
# klass = builder.call
|
23
|
+
# klass.name # => "MyClass"
|
24
|
+
#
|
25
|
+
# @example Create named class
|
26
|
+
# builder = Dry::Core::ClassBuilder.new(name: 'User', namespace: Entities)
|
27
|
+
#
|
28
|
+
# klass = builder.call
|
29
|
+
# klass.name # => "Entities::User"
|
30
|
+
# klass.superclass.name # => "Entities::User"
|
31
|
+
# Entities::User # => "Entities::User"
|
32
|
+
# klass.superclass == Entities::User # => true
|
33
|
+
#
|
34
|
+
# @return [Class]
|
35
|
+
def call
|
36
|
+
klass = if namespace
|
37
|
+
create_named
|
38
|
+
else
|
39
|
+
create_anonymous
|
40
|
+
end
|
41
|
+
|
42
|
+
yield(klass) if block_given?
|
43
|
+
|
44
|
+
klass
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# @api private
|
50
|
+
def create_anonymous
|
51
|
+
klass = Class.new(parent)
|
52
|
+
name = self.name
|
53
|
+
|
54
|
+
klass.singleton_class.class_eval do
|
55
|
+
define_method(:name) { name }
|
56
|
+
alias_method :inspect, :name
|
57
|
+
alias_method :to_s, :name
|
58
|
+
end
|
59
|
+
|
60
|
+
klass
|
61
|
+
end
|
62
|
+
|
63
|
+
# @api private
|
64
|
+
def create_named
|
65
|
+
name = self.name
|
66
|
+
base = create_base(namespace, name, parent)
|
67
|
+
klass = Class.new(base)
|
68
|
+
|
69
|
+
namespace.module_eval do
|
70
|
+
remove_const(name)
|
71
|
+
const_set(name, klass)
|
72
|
+
|
73
|
+
const_get(name).name if RUBY_VERSION < '2.4'
|
74
|
+
|
75
|
+
remove_const(name)
|
76
|
+
const_set(name, base)
|
77
|
+
end
|
78
|
+
|
79
|
+
klass
|
80
|
+
end
|
81
|
+
|
82
|
+
# @api private
|
83
|
+
def create_base(namespace, name, parent)
|
84
|
+
begin
|
85
|
+
namespace.const_get(name)
|
86
|
+
rescue NameError
|
87
|
+
end
|
88
|
+
|
89
|
+
if namespace.const_defined?(name, false)
|
90
|
+
existing = namespace.const_get(name)
|
91
|
+
|
92
|
+
unless existing <= parent
|
93
|
+
raise ParentClassMismatch, "#{ existing.name } must be a subclass of #{ parent.name }"
|
94
|
+
end
|
95
|
+
|
96
|
+
existing
|
97
|
+
else
|
98
|
+
klass = Class.new(parent || Object)
|
99
|
+
namespace.const_set(name, klass)
|
100
|
+
klass
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Core
|
5
|
+
# A list of constants you can use to avoid memory allocations or identity checks.
|
6
|
+
#
|
7
|
+
# @example Just include this module to your class or module
|
8
|
+
# class Foo
|
9
|
+
# def call(value = EMPTY_ARRAY)
|
10
|
+
# value.map(&:to_s)
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
module Constants
|
16
|
+
# An empty array
|
17
|
+
EMPTY_ARRAY = [].freeze
|
18
|
+
# An empty hash
|
19
|
+
EMPTY_HASH = {}.freeze
|
20
|
+
# An empty list of options
|
21
|
+
EMPTY_OPTS = {}.freeze
|
22
|
+
# An empty set
|
23
|
+
EMPTY_SET = Set.new.freeze
|
24
|
+
# An empty string
|
25
|
+
EMPTY_STRING = ''.freeze
|
26
|
+
|
27
|
+
# A special value you can use as a default to know if no arguments
|
28
|
+
# were passed to you method
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# def method(value = Undefined)
|
32
|
+
# if value == Undefined
|
33
|
+
# puts 'no args'
|
34
|
+
# else
|
35
|
+
# puts value
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
Undefined = Object.new.tap do |undefined|
|
39
|
+
def undefined.to_s
|
40
|
+
'Undefined'
|
41
|
+
end
|
42
|
+
|
43
|
+
def undefined.inspect
|
44
|
+
'Undefined'
|
45
|
+
end
|
46
|
+
|
47
|
+
# Pick a value, if the first argument is not Undefined, return it back,
|
48
|
+
# otherwise return the second arg or yield the block.
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# def method(val = Undefined)
|
52
|
+
# 1 + Undefined.default(val, 2)
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
def undefined.default(x, y = self)
|
56
|
+
if x.equal?(self)
|
57
|
+
if y.equal?(self)
|
58
|
+
yield
|
59
|
+
else
|
60
|
+
y
|
61
|
+
end
|
62
|
+
else
|
63
|
+
x
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end.freeze
|
67
|
+
|
68
|
+
def self.included(base)
|
69
|
+
super
|
70
|
+
|
71
|
+
constants.each do |const_name|
|
72
|
+
base.const_set(const_name, const_get(const_name))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Core
|
5
|
+
# An extension for issueing warnings on using deprecated methods.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
#
|
9
|
+
# class Foo
|
10
|
+
# def self.old_class_api; end
|
11
|
+
# def self.new_class_api; end
|
12
|
+
#
|
13
|
+
# deprecate_class_method :old_class_api, :new_class_api
|
14
|
+
#
|
15
|
+
# def old_api; end
|
16
|
+
# def new_api; end
|
17
|
+
#
|
18
|
+
# deprecate_method :old_api, :new_api, "old_api is no-no"
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# @example You also can use this module for your custom messages
|
22
|
+
#
|
23
|
+
# Dry::Core::Deprecations.announce("Foo", "use bar instead")
|
24
|
+
# Dry::Core::Deprecations.warn("Baz is going to be removed soon")
|
25
|
+
#
|
26
|
+
# @api public
|
27
|
+
module Deprecations
|
28
|
+
STACK = -> { caller.find { |l| l !~ %r{(lib/dry/core)|(gems)} } }
|
29
|
+
|
30
|
+
class << self
|
31
|
+
# Prints a warning
|
32
|
+
#
|
33
|
+
# @param [String] msg Warning string
|
34
|
+
def warn(msg, tag: nil)
|
35
|
+
tagged = "[#{tag || 'deprecated'}] #{msg.gsub(/^\s+/, '')}"
|
36
|
+
logger.warn(tagged)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Wraps arguments with a standard message format and prints a warning
|
40
|
+
#
|
41
|
+
# @param [Object] name what is deprecated
|
42
|
+
# @param [String] msg additional message usually containing upgrade instructions
|
43
|
+
def announce(name, msg, tag: nil)
|
44
|
+
warn(deprecation_message(name, msg), tag: tag)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @api private
|
48
|
+
def deprecation_message(name, msg)
|
49
|
+
<<-MSG
|
50
|
+
#{ name } is deprecated and will be removed in the next major version
|
51
|
+
#{ msg }
|
52
|
+
MSG
|
53
|
+
end
|
54
|
+
|
55
|
+
# @api private
|
56
|
+
def deprecated_name_message(old, new = nil, msg = nil)
|
57
|
+
if new
|
58
|
+
deprecation_message(old, <<-MSG)
|
59
|
+
Please use #{new} instead.
|
60
|
+
#{msg}
|
61
|
+
MSG
|
62
|
+
else
|
63
|
+
deprecation_message(old, msg)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns the logger used for printing warnings.
|
68
|
+
# You can provide your own with .set_logger!
|
69
|
+
#
|
70
|
+
# @param [IO] output output stream
|
71
|
+
#
|
72
|
+
# @return [Logger]
|
73
|
+
def logger(output = $stderr)
|
74
|
+
if defined?(@logger)
|
75
|
+
@logger
|
76
|
+
else
|
77
|
+
set_logger!(output)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Sets a custom logger. This is a global setting.
|
82
|
+
#
|
83
|
+
# @overload set_logger!(output)
|
84
|
+
# @param [IO] output Stream for messages
|
85
|
+
#
|
86
|
+
# @overload set_logger!
|
87
|
+
# Stream messages to stdout
|
88
|
+
#
|
89
|
+
# @overload set_logger!(logger)
|
90
|
+
# @param [#warn] logger
|
91
|
+
#
|
92
|
+
# @api public
|
93
|
+
def set_logger!(output = $stderr)
|
94
|
+
if output.respond_to?(:warn)
|
95
|
+
@logger = output
|
96
|
+
else
|
97
|
+
@logger = Logger.new(output).tap do |logger|
|
98
|
+
logger.formatter = proc { |_, _, _, msg| "#{ msg }\n" }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def [](tag)
|
104
|
+
Tagged.new(tag)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# @api private
|
109
|
+
class Tagged < Module
|
110
|
+
def initialize(tag)
|
111
|
+
@tag = tag
|
112
|
+
end
|
113
|
+
|
114
|
+
def extended(base)
|
115
|
+
base.extend Interface
|
116
|
+
base.deprecation_tag @tag
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
module Interface
|
121
|
+
# Sets/gets deprecation tag
|
122
|
+
#
|
123
|
+
# @option [String,Symbol] tag tag
|
124
|
+
def deprecation_tag(tag = nil)
|
125
|
+
if defined?(@deprecation_tag)
|
126
|
+
@deprecation_tag
|
127
|
+
else
|
128
|
+
@deprecation_tag = tag
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Issue a tagged warning message
|
133
|
+
#
|
134
|
+
# @param [String] msg warning message
|
135
|
+
def warn(msg)
|
136
|
+
Deprecations.warn(msg, tag: deprecation_tag)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Mark instance method as deprecated
|
140
|
+
#
|
141
|
+
# @param [Symbol] old_name deprecated method
|
142
|
+
# @param [Symbol] new_name replacement (not required)
|
143
|
+
# @option [String] message optional deprecation message
|
144
|
+
def deprecate(old_name, new_name = nil, message: nil)
|
145
|
+
full_msg = Deprecations.deprecated_name_message(
|
146
|
+
"#{self.name}##{old_name}",
|
147
|
+
new_name ? "#{self.name}##{new_name}" : nil,
|
148
|
+
message
|
149
|
+
)
|
150
|
+
mod = self
|
151
|
+
|
152
|
+
if new_name
|
153
|
+
undef_method old_name if method_defined?(old_name)
|
154
|
+
|
155
|
+
define_method(old_name) do |*args, &block|
|
156
|
+
mod.warn("#{ full_msg }\n#{ STACK.() }")
|
157
|
+
__send__(new_name, *args, &block)
|
158
|
+
end
|
159
|
+
else
|
160
|
+
aliased_name = :"#{old_name}_without_deprecation"
|
161
|
+
alias_method aliased_name, old_name
|
162
|
+
private aliased_name
|
163
|
+
undef_method old_name
|
164
|
+
|
165
|
+
define_method(old_name) do |*args, &block|
|
166
|
+
mod.warn("#{ full_msg }\n#{ STACK.() }")
|
167
|
+
__send__(aliased_name, *args, &block)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Mark class-level method as deprecated
|
173
|
+
#
|
174
|
+
# @param [Symbol] old_name deprecated method
|
175
|
+
# @param [Symbol] new_name replacement (not required)
|
176
|
+
# @option [String] message optional deprecation message
|
177
|
+
def deprecate_class_method(old_name, new_name = nil, message: nil)
|
178
|
+
full_msg = Deprecations.deprecated_name_message(
|
179
|
+
"#{self.name}.#{old_name}",
|
180
|
+
new_name ? "#{self.name}.#{new_name}" : nil,
|
181
|
+
message
|
182
|
+
)
|
183
|
+
|
184
|
+
meth = new_name ? method(new_name) : method(old_name)
|
185
|
+
|
186
|
+
singleton_class.instance_exec do
|
187
|
+
undef_method old_name if method_defined?(old_name)
|
188
|
+
|
189
|
+
define_method(old_name) do |*args, &block|
|
190
|
+
warn("#{ full_msg }\n#{ STACK.() }")
|
191
|
+
meth.call(*args, &block)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Mark a constant as deprecated
|
197
|
+
# @param [Symbol] constant_name constant name to be deprecated
|
198
|
+
# @option [String] message optional deprecation message
|
199
|
+
def deprecate_constant(constant_name, message: nil)
|
200
|
+
value = const_get(constant_name)
|
201
|
+
remove_const(constant_name)
|
202
|
+
|
203
|
+
full_msg = Deprecations.deprecated_name_message(
|
204
|
+
"#{self.name}::#{constant_name}",
|
205
|
+
message
|
206
|
+
)
|
207
|
+
|
208
|
+
mod = Module.new do
|
209
|
+
define_method(:const_missing) do |missing|
|
210
|
+
if missing == constant_name
|
211
|
+
warn("#{ full_msg }\n#{ STACK.() }")
|
212
|
+
value
|
213
|
+
else
|
214
|
+
super(missing)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
extend(mod)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|