dry-core 0.4.7

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