valligator 1.0.1

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 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: []