valligator 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 901e61d04df742c8b66f23ae13d25fe593b1f67a
4
+ data.tar.gz: c51339d7634a55baa18c4d2f66deb7f28a7ba2a0
5
+ SHA512:
6
+ metadata.gz: e12044a5195fb8e1eded23cf1cedbfed7dd28e248815bb40f6ecc9ec491f0f4531bbbf544a8ba2dfd0ef1a4496b0282f135fe6377c92d987e1f3ec3df6f556c4
7
+ data.tar.gz: 7e2c676687fae5c74f833663dd533c8dda491fe471a680ffe5cfdd2a074ab2b7a19e4c02effbb75193dbb88970f4fe6b96ef5c553de0251f4fe25d1b1c0a1572
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rake'
4
+
5
+ group :test do
6
+ gem 'minitest'
7
+ gem 'minitest-reporters'
8
+ end
data/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # Valligator
2
+
3
+
4
+ ## Ruby objects validator
5
+
6
+ In ruby we often run into a case when we need to validate method parameter types, check if their values are in the
7
+ required boundaries, or to perform any other crazy variable validations. The valligator an attempt to simplify the problem.
8
+
9
+
10
+ ## Requirements
11
+
12
+ Ruby 2.0+
13
+
14
+
15
+ ## Installation
16
+
17
+ Add it to your Gemfile
18
+
19
+ ```
20
+ gem "valligator"
21
+ ```
22
+
23
+ Or install manually:
24
+
25
+ ```
26
+ gem install valligator
27
+ ```
28
+
29
+ ## Statements
30
+
31
+ There are 3 validations (a.k.a. statements) that the Valligator supports:
32
+
33
+ #### speaks
34
+
35
+ ```
36
+ testee.speaks(*methods)
37
+ testee.does_not_speak(*methods)
38
+ ```
39
+ - _methods_ are symbols
40
+
41
+ The validations passes when testee responds to all (or none in negative case) the methods from the list.
42
+
43
+ #### is_instance_of
44
+ ```
45
+ testee.is_instance_of(*classes)
46
+ testee.is_not_instance_of(*classes)
47
+ ```
48
+ - _classes_ a list of ruby classes
49
+
50
+ The validations passes when testee is an instance of any class (or not an instance of all the classes in negative case).
51
+
52
+ #### asserts
53
+ ```
54
+ testee.asserts(method, *method_args, &block)
55
+ testee.asserts_not(method, *method_args, &block)
56
+ ```
57
+ - _method_ a method to be calld on testee
58
+ - _method_args_ (optional) the method arguments
59
+ - _block_ (optional) a block to be invoked in the context of the _method_ response
60
+
61
+ When the _block_ is not provided the validation passes if the _method_, called with _method_args_ on the testee, returns truthy response (not false or nil).
62
+
63
+ When the _block_ is provided the validation passes if the _block_, called in the context of the value returned by the _method_ called with _method_args_, returns truthy (not false or nil) value.
64
+
65
+ If it does not sound clear, then it is something like this:
66
+ ```
67
+ testee = Valligator.new(:foo)
68
+ testee.has(:size){self == 10}
69
+
70
+ # is the same as:
71
+
72
+ value = :foo.size
73
+ raise !!value.instance_eval { self == 10 }
74
+ ```
75
+
76
+ I use _instance_eval_ so that the _value_ could be assessed as _self_, and one would not need to access it using standard block params definition ({|value| value == 10 }).
77
+
78
+ **asserts** has two aliases: **is** and **has**. The negative form **asserts_not** also has its own clones: **is_not**, **does_not_have**. All the methods are absolutely identical, just use what ever sounds more grammatically correct: _is(:active?)_, _has(:apples)_, _asserts(:respond_to?, :foo)_, etc.
79
+
80
+ ## Method chaining
81
+
82
+ Each statement, if it does not fail, returns an instance of the Valligator, so that they can be chained:
83
+
84
+ ```
85
+ testee.is_instance_of(String).is_not(:empty?).has(:size){self > 10}.speaks(:to_s)
86
+ ```
87
+
88
+ ## Errors
89
+
90
+ When validation fails a Valligator::ValidationError is raised. The error message contains the full path of the
91
+ passed validations. If the validation above would fail on _is_instance_of_ statement the error message wold look like:
92
+
93
+ ```
94
+ Valligator::ValidationError: at testee#1.is_instance_of
95
+ ```
96
+
97
+ but if it would fail on _has_ then the erro would be
98
+
99
+
100
+ ```
101
+ Valligator::ValidationError: at testee#1.is_instance_of.is_not.has
102
+ ```
103
+
104
+ You can provide a testee name when you instantiate a Valligator instance, and the name will be used in the error message instead of 'testee#x'
105
+
106
+ ```
107
+ testee = Valligator.new('Very long string', 'Short', names: ['long', 'short'])
108
+ testee.is_instance_of(String).has(:size){self > 10}
109
+ #=> Valligator::ValidationError: at `short.is_instance_of.has'
110
+ ```
111
+
112
+ ## Examples
113
+
114
+ Validate that testee is an instance of String
115
+ ```
116
+ Valligator.new('foo').is_instance_of(String) #=> OK
117
+ ```
118
+
119
+ Validate that all testees respond to :to_s and :upcase methods
120
+ ```
121
+ testees = ['foo', 'bar', :baz]
122
+ Valligator.new(*testees).speaks(:to_s, :upcase) #=> OK
123
+ ```
124
+
125
+ Validate that all testees have size == 3 and start with 'b' and they are Strings
126
+ ```
127
+ testees = ['boo', 'bar', :baz]
128
+ Valligator.new(*testees).has(:size){self == 3}.has(:[], 0){self == 'b'}.is_instance_of(String)
129
+ #=> Valligator::ValidationError: at testee#3.has.has.is_instance_of'
130
+ ```
131
+
132
+ Validate that all hash values are Integers <= 2
133
+ ```
134
+ h = { foo: 1, bar: 2, baz: 3 }
135
+ Valligator.new(*h.values, names: h.keys).is_instance_of(Integer).asserts(:<= , 2)
136
+ #=> Valligator::ValidationError: at `baz.is_instance_of.asserts'
137
+ ```
138
+
139
+ ## More examples
140
+
141
+ How about a completely synthetic example:
142
+
143
+ ```
144
+ def charge!(payee, payment_gateway, order, currency, logger)
145
+ # FIXME: I want validations before processing to the charge method
146
+ charge(payee, payment_gateway, order, currency, logger)
147
+ end
148
+
149
+ ```
150
+
151
+ And we would like to make sure that:
152
+
153
+ * Payee:
154
+ - is an instance of either a User or a RemoteSystem model
155
+ - it is not blocked
156
+ - it is a confirmed user
157
+ - it has payment method registred
158
+ - it can pay in a requested currency
159
+ * Payment gateway:
160
+ - is active
161
+ - it can accept payment in the payment method that the user supports
162
+ * Order
163
+ - is not deleted
164
+ - its status is set to :pending
165
+ - its corresponding OrderItem records are not empty
166
+ * OrderItems
167
+ - are in the same currency that was passed with the method call
168
+ - their price makes sence
169
+ * Logger
170
+ - is an IO object
171
+ - it is not closed
172
+ - the file it writes to is not '/dev/null'
173
+ * Currency
174
+ - equal to :usd
175
+
176
+ The most straightforward way to code this may look like the one below (yeah, Sandi Metz would hate it starting from the line # [6](https://robots.thoughtbot.com/sandi-metz-rules-for-developers)):
177
+
178
+ ```
179
+ def charge!(payee, payment_gateway, order, currency, logger)
180
+ if !(payee.is_a?(User) || payee.is_a?(RemoteSystem)) || payee.blocked? || !payee.confirmed? || !payee.payment_method || !payee.can_pay_in?(currency)
181
+ raise(ArgumentError, 'Payee is not either a User or a RemoteSystem or is blocked or is not confirmed, or does not have a payment method set')
182
+ end
183
+ if !payment_gateway.active? || !payment_gateway.respond_to?(payee.payment_method)
184
+ raise(ArgumentError, 'Payment gateway cannot charge users or is not active')
185
+ end
186
+ if order.deleted? || order.status != :pending || order.order_items.empty?
187
+ raise(ArgumentError, 'Order is deleted or is not in pending state or does not have any items in it')
188
+ end
189
+ order.order_items.each do |item|
190
+ if item.currency != currency || item.price <= 0
191
+ raise(ArgumentError, 'There are order items not in USD or with a negative price')
192
+ end
193
+ end
194
+ if !logger.is_a?(IO) || logger.closed? || logger.path == '/dev/null'
195
+ raise(ArgumentError, 'Logger is not an IO instance or closed or writes to nowhere')
196
+ end
197
+ if currency != :usd
198
+ raise(ArgumentError, 'Currency is expected to be set to USD')
199
+ end
200
+
201
+ charge(payee, payment_gateway, order, currency, logger)
202
+ end
203
+ ```
204
+
205
+ Using the Valligator we can write all above as:
206
+
207
+ ```
208
+ require 'valligator'
209
+
210
+ def charge!(payee, payment_gateway, order, currency, logger)
211
+ Valligator.new(user).is_instance_of(User, RemoteSystem).is_not(:blocked?).is(:confirmed?).has(:payment_method).asserts(:can_pay_in?, currency)
212
+ Valligator.new(payment_gateway).is(:active?).speaks(payee.payment_method)
213
+ Valligator.new(order).is_not(:deleted?).has(:status) { self == :pending }.does_not_have(:order_items) { empty? }
214
+ Valligator.new(*order.items).has(:currency){ self == currency }.has(:price) { self > 0 }
215
+ Valligator.new(logger).is_instance_of(IO).is_not(:closed?).has(:path) { self != 'dev/null'}
216
+ Valligator.new(currency).asserts(:==, :usd)
217
+
218
+ charge(payee, payment_gateway, order, currency, logger)
219
+ end
220
+ ```
221
+
222
+ or a little bit shorter using a handy _v_ method, provided by _Valligator::Helper_, and a more natural way of
223
+ writing statements:
224
+
225
+ ```
226
+ require 'valligator'
227
+ include Valligator::Helper
228
+
229
+ def charge!(payee, payment_gateway, order, logger, currency)
230
+ v(user).is_instance_of(User, RemoteSystem).is_not_blocked?.is_confirmed?.has_payment_method.asserts_can_pay_in?(currency)
231
+ v(payment_gateway).is_active?.speaks(payee.payment_method)
232
+ v(order).is_not_deleted?.has_status{ self == :pending }.does_not_have_order_items { empty? }
233
+ v(*order.items).has_currency{ self == :usd }.has_price { self > 0 }
234
+ v(logger).is_instance_of(IO).is_not_closed?.has_path { self != 'dev/null'}
235
+ v(currency).asserts(:==, :usd)
236
+
237
+ charge(payee, payment_gateway, order, currency, logger)
238
+ end
239
+ ```
240
+
241
+ ## Tests
242
+
243
+ ```
244
+ rake test
245
+ ```
246
+
247
+ ## License
248
+
249
+ [MIT](https://opensource.org/licenses/mit-license.php) <br/>
250
+
251
+
252
+ ## Author
253
+
254
+ Konstantin Dzreev, [konstantin-dzreev](https://github.com/konstantin-dzreev)
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.pattern = "test/**/*_test.rb"
5
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.1
data/lib/valligator.rb ADDED
@@ -0,0 +1,393 @@
1
+ require_relative 'valligator_helper'
2
+
3
+ class Valligator
4
+ Error = Class.new(StandardError)
5
+ ValidationError = Class.new(Error)
6
+
7
+ INFINITY = 1/0.0
8
+
9
+ attr_reader :testees
10
+ attr_reader :names
11
+ attr_reader :stack
12
+
13
+
14
+ # Creates a new Valligator instance
15
+ #
16
+ # @param [Array<Object>] testees One or more objects to be tested
17
+ # @param option [Array<String>] :names Testee names
18
+ # @return [Valligator]
19
+ #
20
+ # @example
21
+ # # validate that testee is an instance of String
22
+ # Valligator.new('foo').is_instance_of(String) #=> OK
23
+ #
24
+ # @example
25
+ # # validate that all testees respond to :to_s and :upcase methods
26
+ # Valligator.new('foo', 'bar', :baz).speaks(:to_s, :upcase) #=> OK
27
+ #
28
+ # @example
29
+ # # validate that all testees have size == 3 and start with 'b' and they are Strings
30
+ # testees = ['boo', 'bar', :baz]
31
+ # Valligator.new(*testees).has(:size){self == 3}.has(:[], 0){self == 'b'}.is_instance_of(String)
32
+ # #=> Valligator::ValidationError: at `testee#3.has.has.is_instance_of'
33
+ #
34
+ # @example
35
+ # # validate that all hash values are Integers <= 2
36
+ # h = { foo: 1, bar: 2, baz: 3 }
37
+ # Valligator.new(*h.values, names: h.keys).is_instance_of(Integer).asserts(:<= , 2)
38
+ # #=> Valligator::ValidationError: at `baz.is_instance_of.asserts'
39
+ #
40
+ def initialize(*testees, names: nil)
41
+ @testees = testees
42
+ @names = Array(names)
43
+ @stack = []
44
+ end
45
+
46
+
47
+ # @private
48
+ def method_missing(method, *args, &block)
49
+ case
50
+ when method[/^does_not_speak_(.+)?$/] then does_not_speak(*args.unshift($1.to_sym))
51
+ when method[/^speaks_(.+)?$/ ] then speaks(*args.unshift($1.to_sym))
52
+ when method[/^asserts_not_(.+)?$/] then asserts_not($1.to_sym, *args, &block)
53
+ when method[/^asserts_(.+)?$/] then asserts($1.to_sym, *args, &block)
54
+ when method[/^does_not_have_(.+)?$/] then does_not_have($1.to_sym, *args, &block)
55
+ when method[/^has_(.+)?$/] then has($1.to_sym, *args, &block)
56
+ when method[/^is_not_(.+)?$/] then is_not($1.to_sym, *args, &block)
57
+ when method[/^is_(.+)?$/] then is($1.to_sym, *args, &block)
58
+ else super(method, *args, &block)
59
+ end
60
+ end
61
+
62
+
63
+ # Passes when the testee is an instance of either of the classes
64
+ #
65
+ # @param [Array<Class>] classes
66
+ # @return [Valligator]
67
+ # @raise [Valligator::ValidationError]
68
+ #
69
+ # @example
70
+ # Valligator.new('foo').is_instance_of(Integer, String) #=> OK
71
+ # Valligator.new('foo').is_instance_of(Integer, Array) #=> Valligator::ValidationError
72
+ #
73
+ def is_instance_of(*classes)
74
+ clone._is_instance_of(__method__, *classes)
75
+ end
76
+
77
+
78
+ # Passes when the testee is not an instance of all of the classes
79
+ #
80
+ # @param [Array<Class>] classes
81
+ # @return [Valligator]
82
+ # @raise [Valligator::ValidationError]
83
+ #
84
+ # @example
85
+ # Valligator.new('foo').is_not_instance_of(Integer, String) #=> Valligator::ValidationError
86
+ # Valligator.new('foo').is_not_instance_of(Integer, Array) #=> OK
87
+ #
88
+ def is_not_instance_of(*classes)
89
+ clone._is_instance_of(__method__, *classes)
90
+ end
91
+
92
+
93
+ # Passes when the testee responds to all the methods
94
+ #
95
+ # @param [Array<Symbols>] methods
96
+ # @return [Valligator]
97
+ # @raise [Valligator::ValidationError]
98
+ #
99
+ # @example
100
+ # Valligator.new('foo').speaks(:size, :empty?) #=> OK
101
+ # Valligator.new('foo').speaks(:size, :foo) #=> Valligator::ValidationError
102
+ #
103
+ def speaks(*methods)
104
+ clone._speaks(__method__, *methods)
105
+ end
106
+
107
+
108
+ # Passes when the testee does not respond to all the methods
109
+ #
110
+ # @param [Array<Symbols>] methods
111
+ # @return [Valligator]
112
+ # @raise [Valligator::ValidationError]
113
+ #
114
+ # @example
115
+ # Valligator.new('foo').does_not_speak(:foo, :boo) #=> OK
116
+ # Valligator.new('foo').does_not_speak(:foo, :size) #=> Valligator::ValidationError
117
+ #
118
+ def does_not_speak(*methods)
119
+ clone._speaks(__method__, *methods)
120
+ end
121
+
122
+
123
+ # When no block given it passes if the testee, called with a given method and arguments, returns truthy value.
124
+ #
125
+ # When block is given then it calls the testee with the given method and arguments.
126
+ # Then it calls the block in the context of the value returned above, and if the block returns truthy value the
127
+ # validation passes.
128
+ #
129
+ # P.S. Truthy value is anything but nil or false.
130
+ #
131
+ # @param [Symbol] method
132
+ # @param [Array<Object>] args
133
+ # @yield [Object]
134
+ # @raise [Valligator::ValidationError]
135
+ #
136
+ # @example
137
+ # Valligator.new('foo').asserts(:size) #=> OK
138
+ # Valligator.new('foo').asserts(:[], 0) #=> OK
139
+ # Valligator.new('foo').asserts(:[], 0) {self == 'f'} #=> OK
140
+ # Valligator.new('foo').asserts(:empty?) #=> Valligator::ValidationError
141
+ # Valligator.new('foo').asserts(:[], 100) #=> Valligator::ValidationError
142
+ # Valligator.new('foo').asserts(:[], 0) {self == 'F'} #=> Valligator::ValidationError
143
+ #
144
+ # @see #is
145
+ # @see #has
146
+ #
147
+ def asserts(method, *args, &block)
148
+ clone._asserts(__method__, method, *args, &block)
149
+ end
150
+
151
+
152
+ # When no block given it passes if the testee, called with a given method and arguments, returns falsy value.
153
+ #
154
+ # When block is given then it calls the testee with the given method and arguments.
155
+ # Then it calls the block in the context of the value returned above, and if the block returns falsy value the
156
+ # validation passes.
157
+ #
158
+ # P.S. Falsy value is either nil or false.
159
+ #
160
+ # @param [Symbol] method
161
+ # @param [Array<Object>] args
162
+ # @yield [Object]
163
+ # @raise [Valligator::ValidationError]
164
+ #
165
+ # @example
166
+ # Valligator.new('foo').asserts_not(:size) #=> Valligator::ValidationError
167
+ # Valligator.new('foo').asserts_not(:[], 0) #=> Valligator::ValidationError
168
+ # Valligator.new('foo').asserts_not(:[], 0) {self == 'f'} #=> Valligator::ValidationError
169
+ # Valligator.new('foo').asserts_not(:empty?) #=> OK
170
+ # Valligator.new('foo').asserts_not(:[], 100) #=> OK
171
+ # Valligator.new('foo').asserts_not(:[], 0) {self == 'F'} #=> OK
172
+ #
173
+ # @see #is_not
174
+ # @see #does_not_have
175
+ #
176
+ def asserts_not(method, *args, &block)
177
+ clone._asserts(__method__, method, *args, &block)
178
+ end
179
+
180
+
181
+ # Is an alias for {#asserts} method
182
+ #
183
+ def is(method, *args, &block)
184
+ clone._asserts(__method__, method, *args, &block)
185
+ end
186
+
187
+
188
+ # Is an alias for {#asserts_not} method
189
+ #
190
+ def is_not(method, *args, &block)
191
+ clone._asserts(__method__, method, *args, &block)
192
+ end
193
+
194
+
195
+ # Is an alias for {#asserts} method
196
+ #
197
+ def has(method, *args, &block)
198
+ clone._asserts(__method__, method, *args, &block)
199
+ end
200
+
201
+
202
+ # Is an alias for {#asserts_not} method
203
+ #
204
+ def does_not_have(method, *args, &block)
205
+ clone._asserts(__method__, method, *args, &block)
206
+ end
207
+
208
+
209
+ protected
210
+
211
+
212
+ # Returns testee name by its index
213
+ #
214
+ # @param [Integer] idx
215
+ # @return [String]
216
+ #
217
+ def name_by_idx(idx)
218
+ @names && @names[idx] || ('testee#%d' % (idx+1))
219
+ end
220
+
221
+
222
+ # Adds method to the stack of the tested ones
223
+ #
224
+ # @param [Symbol] method_name
225
+ # @return [void]
226
+ #
227
+ def push(method_name)
228
+ @stack << method_name
229
+ end
230
+
231
+
232
+ # Raises requested exception
233
+ #
234
+ # @param [Class] exception
235
+ # @param option [Integer] idx Testee index
236
+ # @param option [nil,String] msg Error explanation (when required)
237
+ #
238
+ def error(exception, idx=0, msg=nil)
239
+ msg += ' ' if msg
240
+ raise(exception, "%sat `%s.%s'" % [msg, name_by_idx(idx), @stack.join('.')])
241
+ end
242
+
243
+
244
+ # Calls the given block for each testee and each item from the list.
245
+ #
246
+ # @param optional list [Array]
247
+ #
248
+ # @yield [testee, idx, list_item]
249
+ # @yieldparam testee [Object]
250
+ # @yieldparam idx [Integer] testee index
251
+ # @yieldparam optional list_item [Integer] when list is given
252
+ #
253
+ # @return [void]
254
+ #
255
+ def each(*list)
256
+ list << nil if list.empty?
257
+ @testees.each_with_index do |testee, idx|
258
+ list.each do |list_item|
259
+ yield(testee, idx, list_item)
260
+ end
261
+ end
262
+ end
263
+
264
+
265
+ # Clones current instance.
266
+ #
267
+ # @return [Valligator]
268
+ #
269
+ def clone
270
+ self.class.new(*@testees, names: @names).tap do |v|
271
+ v.stack.push(*stack)
272
+ end
273
+ end
274
+
275
+
276
+ #------------------------------------------
277
+ # Validations
278
+ #------------------------------------------
279
+
280
+
281
+ # Validates number of arguments
282
+ #
283
+ # @param [Array<Object>] args
284
+ # @param [Integer] expected
285
+ # @return [void]
286
+ # @raise [ArgumentError]
287
+ #
288
+ def validate_number_of_arguments(args, expected)
289
+ return if expected === args.size
290
+
291
+ expected == expected.first if expected.is_a?(Range) && expected.size == 1
292
+ error(ArgumentError, 0, 'wrong number of arguments (%s for %s)' % [args.size, expected])
293
+ end
294
+
295
+
296
+ # Validates argument type
297
+ #
298
+ # @param [Array<Class>] classes
299
+ # @param [Array<Object>] args
300
+ # @param [Integer] arg_idx
301
+ # @return [void]
302
+ # @raise [ArgumentError]
303
+ #
304
+ def validate_argument_type(classes, arg, arg_idx)
305
+ return if classes.any? { |klass| arg.is_a?(klass) }
306
+ classes = classes.map { |klass| klass.inspect }.join(' or ')
307
+ error(ArgumentError, 0, 'wrong argument type (arg#%d is a %s instead of %s)' % [arg_idx, arg.class.name, classes])
308
+ end
309
+
310
+
311
+ # Validates if object responds to a method
312
+ #
313
+ # @param [Object] object
314
+ # @param [Array<Object>] args
315
+ # @param [Integer] idx
316
+ # @return [void]
317
+ # @raise [ArgumentError]
318
+ #
319
+ def validate_respond_to(object, method, idx)
320
+ return if object.respond_to?(method)
321
+ str = object.to_s
322
+ str = str[0..-1] + '...' if str.size > 20
323
+ error(NoMethodError, idx, 'undefined method `%s\' for %s:%s' % [method, str, object.class.name])
324
+ end
325
+
326
+
327
+ #------------------------------------------
328
+ # Statements
329
+ #------------------------------------------
330
+
331
+
332
+ # @private
333
+ # @see #is_instance_of
334
+ #
335
+ def _is_instance_of(statement, *classes)
336
+ push(statement)
337
+ equality = !statement[/not/]
338
+
339
+ matches = [false] * @testees.count
340
+
341
+ validate_number_of_arguments(classes, 1..INFINITY)
342
+ classes.each_with_index { |klass, idx| validate_argument_type([Class], klass, idx+1) }
343
+
344
+ each(*classes) { |testee, idx, klass| matches[idx] = true if testee.is_a?(klass) }
345
+ matches.each_with_index { |match, idx| error(ValidationError, idx) if matches[idx] != equality }
346
+ self
347
+ end
348
+
349
+
350
+ # @private
351
+ # @see #speaks
352
+ #
353
+ def _speaks(statement, *methods)
354
+ push(statement)
355
+ equality = !statement[/not/]
356
+
357
+ validate_number_of_arguments(methods, 1..INFINITY)
358
+
359
+ methods.each_with_index do |arg, idx|
360
+ validate_argument_type([Symbol], arg, idx+1)
361
+ end
362
+
363
+ each(*methods) do |testee, idx, method|
364
+ next if testee.respond_to?(method) == equality
365
+
366
+ error(ValidationError, idx)
367
+ end
368
+ self
369
+ end
370
+
371
+
372
+ # @private
373
+ # @see #speaks
374
+ #
375
+ def _asserts(statement, method, *args, &block)
376
+ push(statement)
377
+ equality = !statement[/not/]
378
+
379
+ each do |testee, idx|
380
+ validate_respond_to(testee, method, idx)
381
+ response = testee.__send__(method, *args)
382
+ begin
383
+ # Execute block in the context on the response so that one could retrieve it using self keyword.
384
+ response = response.instance_eval(&block) if block
385
+ rescue
386
+ # This error will have 'cause' set to the original exception raised in the block
387
+ error(ValidationError, idx)
388
+ end
389
+ error(ValidationError, idx) if !!response != equality
390
+ end
391
+ self
392
+ end
393
+ end
@@ -0,0 +1,12 @@
1
+ class Valligator
2
+ module Helper
3
+
4
+ def valligate(*testees, names: nil)
5
+ Valligator.new(*testees, names: names)
6
+ end
7
+
8
+
9
+ alias_method :v, :valligate
10
+
11
+ end
12
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Valligator
2
+ VERSION = '1.0'
3
+ end
@@ -0,0 +1,60 @@
1
+ require_relative '../../test_helper'
2
+
3
+
4
+ class TestAsserts < Minitest::Test
5
+ include Valligator::Helper
6
+
7
+ def error
8
+ Valligator::ValidationError
9
+ end
10
+
11
+
12
+ positive_statements = [:asserts, :has, :is]
13
+ negative_statements = [:asserts_not, :does_not_have, :is_not]
14
+
15
+
16
+ positive_statements.each do |method|
17
+ define_method 'test_that__%s__returns_an_instance_of_valligator' % method do
18
+ assert_instance_of Valligator, v(:a).send(method, :size)
19
+ end
20
+
21
+
22
+ define_method 'test_that__%s__passes_when_testee_returns_truthy_value' % method do
23
+ v(:a).send(method, :to_s)
24
+ v(:a).send(method, :[], 0)
25
+ v(:a).send(method, :size){self == 1}
26
+ v(:a).send(method, :to_s).send(method, :[], 0).send(method, :size){self == 1}
27
+ end
28
+
29
+
30
+ define_method 'test_that__%s__fails_when_testee_returns_falsy_value' % method do
31
+ assert_raises(error) { v(:a).send(method, :empty?) }
32
+ assert_raises(error) { v(:a).send(method, :[], 1) }
33
+ assert_raises(error) { v(:a).send(method, :size){self != 1} }
34
+ assert_raises(error) { v(:a).send(method, :to_s).send(method, :[], 0).send(method, :size){self != 1} }
35
+ end
36
+ end
37
+
38
+
39
+ negative_statements.each do |method|
40
+ define_method 'test_that__%s__returns_an_instance_of_valligator' % method do
41
+ assert_instance_of Valligator, v(:a).send(method, :empty?)
42
+ end
43
+
44
+
45
+ define_method 'test_that__%s__passes_when_testee_returns_falsy_value' % method do
46
+ v(:a).send(method, :empty?)
47
+ v(:a).send(method, :[], 1)
48
+ v(:a).send(method, :size){self != 1}
49
+ v(:a).send(method, :empty?).send(method, :[], 1).send(method, :size){self != 1}
50
+ end
51
+
52
+
53
+ define_method 'test_that__%s__fails_when_testee_returns_truthy_value' % method do
54
+ assert_raises(error) { v(:a).send(method, :to_s) }
55
+ assert_raises(error) { v(:a).send(method, :[], 0) }
56
+ assert_raises(error) { v(:a).send(method, :size){self == 1} }
57
+ assert_raises(error) { v(:a).send(method, :empty?).send(method, :[], 1).send(method, :size){self == 1} }
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ require_relative '../../test_helper'
2
+
3
+
4
+ class TestMethodMissing < Minitest::Test
5
+ include Valligator::Helper
6
+
7
+
8
+ def error
9
+ Valligator::ValidationError
10
+ end
11
+
12
+
13
+ def test_without_testee_name
14
+ expected = "at `testee#1.speaks'"
15
+ err = assert_raises(error) { v(:foo).speaks(:foo) }
16
+ assert_equal expected, err.message
17
+ end
18
+
19
+
20
+ def test_with_testee_name
21
+ expected = "at `i-have-a-name.speaks'"
22
+ err = assert_raises(error) { v(:foo, names: 'i-have-a-name').speaks(:foo) }
23
+ assert_equal expected, err.message
24
+ end
25
+
26
+
27
+ def test_long_path
28
+ expected = "at `testee#1.speaks.asserts_not.has.is_instance_of.speaks'"
29
+ err = assert_raises(error) do |variable|
30
+ v(:foo).speaks(:to_s).\
31
+ asserts_not(:empty?).\
32
+ has(:size){self > 1}.\
33
+ is_instance_of(Symbol).\
34
+ speaks(:it_dies_here).\
35
+ asserts(:thould_not_reach_this)
36
+ end
37
+ assert_equal expected, err.message
38
+ end
39
+
40
+ end
@@ -0,0 +1,58 @@
1
+ require_relative '../../test_helper'
2
+
3
+
4
+ class TestIsInstanceOf < Minitest::Test
5
+ include Valligator::Helper
6
+
7
+ def error
8
+ Valligator::ValidationError
9
+ end
10
+
11
+
12
+ def test_that__is_instance_of__fails_on_wrong_number_of_arguments
13
+ expected = "wrong number of arguments (0 for 1..Infinity) at `testee#1.is_instance_of'"
14
+ err = assert_raises(ArgumentError) { v(:a).is_instance_of }
15
+ assert_equal expected, err.message
16
+ end
17
+
18
+
19
+ def test_that__is_instance_of__fails_on_wrong_argument_type
20
+ expected = "wrong argument type (arg#1 is a Fixnum instead of Class) at `testee#1.is_instance_of'"
21
+ err = assert_raises(ArgumentError) { v(:a).is_instance_of(1) }
22
+ assert_equal expected, err.message
23
+ end
24
+
25
+
26
+ def test_that__is_instance_of__returns_an_instance_of_valligator
27
+ assert_instance_of Valligator, v(:a).is_instance_of(Symbol)
28
+ end
29
+
30
+
31
+ def test_that__is_instance_of__passes_when_there_is_a_match
32
+ v(:a).is_instance_of(Symbol)
33
+ v(:a).is_instance_of(String, Symbol)
34
+ v(:a).is_instance_of(Symbol).is_instance_of(Symbol)
35
+ end
36
+
37
+
38
+ def test_that__is_instance_of__fails_when_there_is_no_match
39
+ assert_raises(error) { v(:a).is_instance_of(String) }
40
+ assert_raises(error) { v(:a).is_instance_of(String, Integer) }
41
+ assert_raises(error) { v(:a).is_instance_of(Symbol).is_instance_of(String) }
42
+ end
43
+
44
+
45
+ def test_that__is_not_instance_of__passes_when_there_is_no_match
46
+ v(:a).is_not_instance_of(String)
47
+ v(:a).is_not_instance_of(String, Integer)
48
+ v(:a).is_not_instance_of(String).is_not_instance_of(NilClass)
49
+ end
50
+
51
+
52
+ def test_that__is_not_instance_of__fails_when_there_is_a_match
53
+ assert_raises(error) { v(:a).is_not_instance_of(Symbol) }
54
+ assert_raises(error) { v(:a).is_not_instance_of(String, Symbol) }
55
+ assert_raises(error) { v(:a).is_not_instance_of(String).is_not_instance_of(Symbol) }
56
+ end
57
+
58
+ end
@@ -0,0 +1,43 @@
1
+ require_relative '../../test_helper'
2
+
3
+
4
+ class TestMethodMissing < Minitest::Test
5
+ include Valligator::Helper
6
+
7
+
8
+ def error
9
+ Valligator::ValidationError
10
+ end
11
+
12
+
13
+ def test__speaks_something
14
+ v(:foo).speaks_to_s
15
+ assert_raises(error) { v(:foo).speaks_to_i }
16
+ end
17
+
18
+
19
+ def test__does_not_speak_something
20
+ v(:foo).does_not_speak_to_i
21
+ assert_raises(error) { v(:foo).does_not_speak_to_s }
22
+ end
23
+
24
+
25
+ [:asserts, :is, :has].each do |method|
26
+ define_method 'test__%s_something' % method do
27
+ m1 = '%s_to_s' % method
28
+ m2 = '%s_empty?' % method
29
+ v(:foo).send(m1)
30
+ assert_raises(error) { v(:foo).send(m2) }
31
+ end
32
+ end
33
+
34
+
35
+ [:asserts_not, :is_not, :does_not_have].each do |method|
36
+ define_method 'test__%s_something' % method do
37
+ m1 = '%s_empty?' % method
38
+ m2 = '%s_to_s' % method
39
+ v(:foo).send(m1)
40
+ assert_raises(error) { v(:foo).send(m2) }
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,58 @@
1
+ require_relative '../../test_helper'
2
+
3
+
4
+ class TestSpeaks < Minitest::Test
5
+ include Valligator::Helper
6
+
7
+ def error
8
+ Valligator::ValidationError
9
+ end
10
+
11
+
12
+ def test_that__speaks__fails_on_wrong_number_of_arguments
13
+ expected = "wrong number of arguments (0 for 1..Infinity) at `testee#1.speaks'"
14
+ err = assert_raises(ArgumentError) { v(:a).speaks }
15
+ assert_equal expected, err.message
16
+ end
17
+
18
+
19
+ def test_that__speaks__fails_on_wrong_argument_type
20
+ expected = "wrong argument type (arg#1 is a Fixnum instead of Symbol) at `testee#1.speaks'"
21
+ err = assert_raises(ArgumentError) { v(:a).speaks(1) }
22
+ assert_equal expected, err.message
23
+ end
24
+
25
+
26
+ def test_that__speaks__returns_an_instance_of_valligator
27
+ assert_instance_of Valligator, v(:a).speaks(:to_s)
28
+ end
29
+
30
+
31
+ def test_that__speaks__passes_when_there_is_a_match
32
+ v(:a).speaks(:to_s)
33
+ v(:a).speaks(:to_s, :size)
34
+ v(:a).speaks(:to_s).speaks(:size)
35
+ end
36
+
37
+
38
+ def test_that__speaks__fails_when_there_is_no_match
39
+ assert_raises(error) { v(:a).speaks(:to_i) }
40
+ assert_raises(error) { v(:a).speaks(:to_s, :foo) }
41
+ assert_raises(error) { v(:a).speaks(:to_s).speaks(:foo) }
42
+ end
43
+
44
+
45
+ def test_that__does_not_speak__passes_when_there_is_no_match
46
+ v(:a).does_not_speak(:foo)
47
+ v(:a).does_not_speak(:foo, :boo)
48
+ v(:a).does_not_speak(:foo).does_not_speak(:boo)
49
+ end
50
+
51
+
52
+ def test_that__does_not_speak__fails_when_there_is_a_match
53
+ assert_raises(error) { v(:a).does_not_speak(:to_s) }
54
+ assert_raises(error) { v(:a).does_not_speak(:foo, :to_s) }
55
+ assert_raises(error) { v(:a).does_not_speak(:foo).does_not_speak(:to_s) }
56
+ end
57
+
58
+ end
@@ -0,0 +1,5 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/reporters'
3
+ require_relative '../lib/valligator'
4
+
5
+ Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new({ color: true })]
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: valligator
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Dzreev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-09-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest-reporters
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Allows one to implement object validations without writing too much code
42
+ email: k.dzreyev@gmail.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - Gemfile
48
+ - README.md
49
+ - Rakefile
50
+ - VERSION
51
+ - lib/valligator.rb
52
+ - lib/valligator_helper.rb
53
+ - lib/version.rb
54
+ - test/lib/valligator/asserts_test.rb
55
+ - test/lib/valligator/errors_test.rb
56
+ - test/lib/valligator/is_instance_of_test.rb
57
+ - test/lib/valligator/method_missing_test.rb
58
+ - test/lib/valligator/speaks_test.rb
59
+ - test/test_helper.rb
60
+ homepage: https://github.com/konstantin-dzreev/valligator
61
+ licenses: []
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 2.5.1
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Ruby objects validator
83
+ test_files: []