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.
@@ -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