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