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