valligator 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +8 -0
- data/README.md +254 -0
- data/Rakefile +5 -0
- data/VERSION +1 -0
- data/lib/valligator.rb +393 -0
- data/lib/valligator_helper.rb +12 -0
- data/lib/version.rb +3 -0
- data/test/lib/valligator/asserts_test.rb +60 -0
- data/test/lib/valligator/errors_test.rb +40 -0
- data/test/lib/valligator/is_instance_of_test.rb +58 -0
- data/test/lib/valligator/method_missing_test.rb +43 -0
- data/test/lib/valligator/speaks_test.rb +58 -0
- data/test/test_helper.rb +5 -0
- metadata +83 -0
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
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
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
|
data/lib/version.rb
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
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: []
|