ducktator 0.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.
- data/LICENSE +19 -0
- data/README +88 -0
- data/lib/ducktator.rb +598 -0
- data/test/test_basic.rb +75 -0
- data/test/test_complex.rb +48 -0
- data/test/test_ducktator.rb +3 -0
- metadata +51 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2006 Ola Bini
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
= Ducktator - the Duck Type Validator
|
2
|
+
|
3
|
+
Ducktator is a small library to enable Ruby systems to generically
|
4
|
+
validate objects introspectively. In plain speak, check certain common
|
5
|
+
methods of objects, and see if they match what your schema expects the
|
6
|
+
values to be. This capability is not necessary for most applications,
|
7
|
+
but sometimes it's highly useful. For example, validating objects that
|
8
|
+
have been serialized or marshallad. Validating what you get when
|
9
|
+
loading YAML files, so that the object graph matches what your code
|
10
|
+
does. Write test cases that expect a complicated object back. The
|
11
|
+
possibilities are many.
|
12
|
+
|
13
|
+
Ducktator can be configured either with YAML or directly using simple
|
14
|
+
Hashes. The syntax is very recursive, extensible and easy. I will use
|
15
|
+
YAML for the examples in this document, but for easier validations it
|
16
|
+
may be better just creating the +Hash+ directly.
|
17
|
+
|
18
|
+
|
19
|
+
== Validator usage
|
20
|
+
|
21
|
+
The main way into the Ducktator validator framework are a couple of
|
22
|
+
factory methods in the Ducktator namespace. To create a new +Validator+
|
23
|
+
from a YAML file you could do it like this: (in this case, there must
|
24
|
+
be a root key inside the YAML document)
|
25
|
+
|
26
|
+
Ducktator::from_file('validations.yml') # => #<Ducktator::Validator ...>
|
27
|
+
|
28
|
+
You can also create a +Validator+ from a +String+ directly.
|
29
|
+
|
30
|
+
Ducktator::from('class: String') # => #<Ducktator::Validator ...>
|
31
|
+
|
32
|
+
If you just want to do a one time validation, this can be done like
|
33
|
+
this:
|
34
|
+
|
35
|
+
Ducktator::valid?('class: String', 123) # => false
|
36
|
+
|
37
|
+
or like this:
|
38
|
+
|
39
|
+
Ducktator::valid?('class' => String, 'abc') # => true
|
40
|
+
|
41
|
+
Using the +Validator+ object is mostly as simple as calling the method
|
42
|
+
+valid?+ and send it the objects you want to validate. +valid?+ will
|
43
|
+
return +true+ only if all its arguments are valid ackording to its
|
44
|
+
validation rules:
|
45
|
+
|
46
|
+
p v # => #<Ducktator::Validator ...>
|
47
|
+
v.valid?("str1")
|
48
|
+
v.valid?("str1","str2","str3")
|
49
|
+
|
50
|
+
Validators can be combined with & and |, like this:
|
51
|
+
|
52
|
+
vx = v1 & (v2 | v3) & v4
|
53
|
+
|
54
|
+
|
55
|
+
== Validation specification
|
56
|
+
|
57
|
+
The validation specification will contain one or more validations to
|
58
|
+
check against. A validation always has a value. It can be scalar, a
|
59
|
+
sequence or a mapping. If it's a mapping, it will be interpolated as a
|
60
|
+
new specification. A simple YAML file that validates a +Hash+, that
|
61
|
+
should have +String+ keys and values that are +Array+'s with index 0
|
62
|
+
being a +Symbol+ and index 1 an +Integer+ which is maximum 256: (note
|
63
|
+
that the +root:+ is necessary, unless loading the YAML directly from a
|
64
|
+
+String+)
|
65
|
+
|
66
|
+
---
|
67
|
+
root:
|
68
|
+
class: Hash
|
69
|
+
each_key: {class: String}
|
70
|
+
each_value:
|
71
|
+
class: Array
|
72
|
+
value:
|
73
|
+
- - 0
|
74
|
+
- class: Symbol
|
75
|
+
- - 1
|
76
|
+
- class: Integer
|
77
|
+
- max: 256
|
78
|
+
|
79
|
+
More than one validation can exist in the same file, just use
|
80
|
+
+Ducktator#from_file+'s second, optional argument, which defaults to
|
81
|
+
"root".
|
82
|
+
|
83
|
+
|
84
|
+
== Author
|
85
|
+
Ola Bini <ola@ologix.com>
|
86
|
+
|
87
|
+
== License
|
88
|
+
Ducktator is distributed with a MIT license, which can be found in the file LICENSE.
|
data/lib/ducktator.rb
ADDED
@@ -0,0 +1,598 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
=begin
|
7
|
+
= Ducktator, duck type validation for Ruby
|
8
|
+
|
9
|
+
== IntroDUCKtion
|
10
|
+
Uses different constructs to make sure a Ruby object matches a specified instruction.
|
11
|
+
Could be used to validate YAML documents.
|
12
|
+
|
13
|
+
== Usage
|
14
|
+
|
15
|
+
Create a new Validator from a file:
|
16
|
+
|
17
|
+
Ducktator.from(stream) => #<Ducktator::Validator>
|
18
|
+
or
|
19
|
+
Ducktator.define(spec) => #<Ducktator::Validator>
|
20
|
+
|
21
|
+
A spec in YAML should look like this:
|
22
|
+
---
|
23
|
+
root:
|
24
|
+
class: Hash
|
25
|
+
respond_to:
|
26
|
+
- to_s
|
27
|
+
- each_with_key
|
28
|
+
each_value:
|
29
|
+
- class: Array
|
30
|
+
each: {class: [Numeric, Symbol]}
|
31
|
+
|
32
|
+
This defines a validator for every object that should be a Hash ===,
|
33
|
+
it should +respond_to?+ +:to_s+ and +:each_with_key+ and +each_value+
|
34
|
+
should yield an +Array+ object where each value is either a +Numeric+
|
35
|
+
or a +Symbol+. +===+ is always used for matching, and actually class
|
36
|
+
works the same way as match which does the same thing. The available
|
37
|
+
validations right now can be found in the different modules in
|
38
|
+
Ducktator ending with Validation. For example, there is a module
|
39
|
+
called +Ducktator::ScalarValidation+, which defines the methods
|
40
|
+
+check_class+, +check_is+ and +check_match+. These are available as
|
41
|
+
the validations +class+, +is+ and +match+. To add new validations,
|
42
|
+
just add methods following thise naming scheme to the validator in
|
43
|
+
question. The method should take an object and a spec as argument.All
|
44
|
+
of these take either a scalar value, or it's own sub validation
|
45
|
+
definition. The name root is used as the default name for validator,
|
46
|
+
but you can define how many validators you want with different names,
|
47
|
+
mix and match with yaml, etc.
|
48
|
+
|
49
|
+
Author: Ola Metodius Bini <ola@ologix.com>
|
50
|
+
License: MIT
|
51
|
+
=end
|
52
|
+
module Ducktator
|
53
|
+
# The default key to load the specification from in a YAML document
|
54
|
+
DEFAULT_ROOT='root'
|
55
|
+
|
56
|
+
# Defines a new Ducktator validator. +spec+ should be a valid
|
57
|
+
# +Ducktator::Specification+ instance
|
58
|
+
def self.define(spec)
|
59
|
+
Validator.new(spec)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Creates a new Ducktator validator. +str+ can either be a String
|
63
|
+
# containing YAML or an IO object to YAML. +root+ is the name in the
|
64
|
+
# YAML to load as root validation object
|
65
|
+
def self.from(str, root=DEFAULT_ROOT)
|
66
|
+
if str.is_a? String
|
67
|
+
from_string(str)
|
68
|
+
else
|
69
|
+
define(Specification.from_yaml(str,root))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Reads YAML specification and creates Ducktator validator from the
|
74
|
+
# filename specified in +str+. +root+ is the name in the YAML file
|
75
|
+
# to load as validation object.
|
76
|
+
def self.from_file(str, root=DEFAULT_ROOT)
|
77
|
+
define(Specification.from_file(str,root))
|
78
|
+
end
|
79
|
+
|
80
|
+
# Loads the YAML in the provided string and creates a specifiction
|
81
|
+
# directly from the hash ensuing. (Note, no root objects needed. The
|
82
|
+
# hash will be the root).
|
83
|
+
def self.from_string(str)
|
84
|
+
define(Specification.from_hash(YAML.load(str)))
|
85
|
+
end
|
86
|
+
|
87
|
+
# Checks to see if +objs+ are valid ackording to +validator+, which
|
88
|
+
# can be a +String+ with YAML data, a +Hash+ with the specification
|
89
|
+
# or a +Validator+. Returns +true+ only if all +objs+ are valid.
|
90
|
+
def self.valid?(validator, *objs)
|
91
|
+
validator = YAML.load(validator) if validator.is_a? String
|
92
|
+
validator = define(Specification.from_hash(validator)) if validator.is_a? Hash
|
93
|
+
validator.valid? *objs
|
94
|
+
end
|
95
|
+
|
96
|
+
# A Specifcation is a data container for all validation
|
97
|
+
# properties. The meat of it is in the factory methods. Any
|
98
|
+
# accessors called on a specification object will succeed, meaning
|
99
|
+
# it can handle new validation names. When parsing a YAML file or a
|
100
|
+
# +Hash+ into a +Specification+, it will recurse. Every +Hash+ it
|
101
|
+
# encounters will be transformed into a new +Specification+ object.
|
102
|
+
class Specification
|
103
|
+
# Creates a specification from the YAML +stream+ provided, with
|
104
|
+
# the +root+ provided.
|
105
|
+
def self.from_yaml(stream, root=DEFAULT_ROOT)
|
106
|
+
from_hash(YAML.load(stream)[root])
|
107
|
+
end
|
108
|
+
|
109
|
+
# Reads YAML from file with name +filename+, loading the YAML
|
110
|
+
# inside and uses the +root+ provided.
|
111
|
+
def self.from_file(filename, root=DEFAULT_ROOT)
|
112
|
+
open(filename, 'r') {|f| from_yaml(f,root) }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Recurses through the hash +h+ and creates all specification
|
116
|
+
# information from this.
|
117
|
+
def self.from_hash(h)
|
118
|
+
sp = Specification.new
|
119
|
+
h.each_key do |nm|
|
120
|
+
v = recurse_value(h[nm])
|
121
|
+
nm = 'clazz' if nm == 'class'
|
122
|
+
sp.send "#{nm}=",v
|
123
|
+
end
|
124
|
+
sp
|
125
|
+
end
|
126
|
+
|
127
|
+
# Special setter to avoid using the name +class+. This setter will
|
128
|
+
# transform the object provided into an array if it isn't already,
|
129
|
+
# and transform all array entries to +Module+
|
130
|
+
def clazz=(v)
|
131
|
+
v = [v] unless v.is_a? Array
|
132
|
+
@clazz = v.collect {|nm| Module === nm ? nm : to_class(nm) }
|
133
|
+
end
|
134
|
+
|
135
|
+
def of=(v)
|
136
|
+
@of = Module === v ? v : to_class(v)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Sets or gets all properties
|
140
|
+
def method_missing(name, *args)
|
141
|
+
name = name.to_s
|
142
|
+
if name =~ /^(.*)=$/
|
143
|
+
instance_variable_set "@#$1",args[0]
|
144
|
+
else
|
145
|
+
instance_variable_get "@#{name}"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
# Recurses down +v+, transforming +Hash+ objects into
|
151
|
+
# +Specification+.
|
152
|
+
def self.recurse_value(v)
|
153
|
+
case v
|
154
|
+
when Hash: from_hash(v)
|
155
|
+
when Array: v.collect {|i| recurse_value(i) }
|
156
|
+
else v
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# The +String+ +v+ will be transformed into a +Module+.
|
161
|
+
def to_class(v)
|
162
|
+
obj_class = Object
|
163
|
+
v.split( "::" ).each { |c| obj_class = obj_class.const_get( c ) } if v
|
164
|
+
obj_class
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
#
|
169
|
+
# Boolean validations will take a list of validations and report
|
170
|
+
# differently depending on which operation asked. The available
|
171
|
+
# validations are +or+,+xor+,+and+,+not_all+ and +not_any+. A
|
172
|
+
# typical usage of +and+ could look like this:
|
173
|
+
#
|
174
|
+
# and:
|
175
|
+
# - class: String
|
176
|
+
# - class: Enumerable
|
177
|
+
#
|
178
|
+
# which requires that the object in question is both a String and an
|
179
|
+
# Enumerable.
|
180
|
+
#
|
181
|
+
module BooleanValidation
|
182
|
+
def check_or(obj, spec)
|
183
|
+
if spec.or
|
184
|
+
spec.or.any? do |real|
|
185
|
+
check_valid(obj,real)
|
186
|
+
end
|
187
|
+
else
|
188
|
+
true
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def check_xor(obj, spec)
|
193
|
+
if spec.xor
|
194
|
+
spec.xor.partition do |real|
|
195
|
+
check_valid(obj,real)
|
196
|
+
end.first.length == 1
|
197
|
+
else
|
198
|
+
true
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def check_and(obj, spec)
|
203
|
+
if spec.and
|
204
|
+
spec.and.all? do |real|
|
205
|
+
check_valid(obj,real)
|
206
|
+
end
|
207
|
+
else
|
208
|
+
true
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def check_not_all(obj, spec)
|
213
|
+
if spec.not_all
|
214
|
+
!spec.not_all.all? do |real|
|
215
|
+
check_valid(obj,real)
|
216
|
+
end
|
217
|
+
else
|
218
|
+
true
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def check_not_any(obj, spec)
|
223
|
+
if spec.not_any
|
224
|
+
!spec.not_any.any? do |real|
|
225
|
+
check_valid(obj,real)
|
226
|
+
end
|
227
|
+
else
|
228
|
+
true
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
#
|
234
|
+
# The comparison validations contain the validations +max+ and +min+
|
235
|
+
# which will make sure the object under question is either <=
|
236
|
+
# max-value or >= min-value.
|
237
|
+
#
|
238
|
+
# If specifying max or min for an object, make sure that object
|
239
|
+
# actually can handle the <= and >= operations.
|
240
|
+
#
|
241
|
+
# Example:
|
242
|
+
#
|
243
|
+
# max: 1.1
|
244
|
+
# min: 0.2
|
245
|
+
#
|
246
|
+
module ComparisonValidation
|
247
|
+
def check_max(obj, spec)
|
248
|
+
if spec.max
|
249
|
+
return false unless obj.respond_to? :<=
|
250
|
+
obj <= spec.max
|
251
|
+
else
|
252
|
+
true
|
253
|
+
end
|
254
|
+
end
|
255
|
+
def check_min(obj, spec)
|
256
|
+
if spec.min
|
257
|
+
return false unless obj.respond_to? :>=
|
258
|
+
obj >= spec.min
|
259
|
+
else
|
260
|
+
true
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
#
|
266
|
+
# Scalar validation is one of the more common structures that other
|
267
|
+
# code will be based on. The available matches are +class+, +match+
|
268
|
+
# and +is+. Class checks if the object in question is +===+ the
|
269
|
+
# Class (or Module) in question. +match+ does the same thing, but
|
270
|
+
# will probably be used for Regexps and such. +is+ will match
|
271
|
+
# against a specific value. An example could look like this:
|
272
|
+
#
|
273
|
+
# class: String
|
274
|
+
# or:
|
275
|
+
# - match: !ruby/regexp /foo/
|
276
|
+
# - is: baz
|
277
|
+
#
|
278
|
+
module ScalarValidation
|
279
|
+
def check_class(obj, spec)
|
280
|
+
if spec.clazz
|
281
|
+
spec.clazz.any? {|v| v === obj }
|
282
|
+
else
|
283
|
+
true
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def check_match(obj, spec)
|
288
|
+
if spec.match
|
289
|
+
spec.match === obj
|
290
|
+
else
|
291
|
+
true
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def check_is(obj, spec)
|
296
|
+
if spec.is
|
297
|
+
spec.is == obj
|
298
|
+
else
|
299
|
+
true
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
#
|
305
|
+
# +EnumerableValidation+ provides common validations for objects
|
306
|
+
# that implement +each+ and friends. The available tests are +each+,
|
307
|
+
# +each_key+, +each_value+, +all+, +any+, +of+ and +none+. Most of
|
308
|
+
# these do exactly what they appear to. They walk through the
|
309
|
+
# objects, check if each object matches the specified rule and
|
310
|
+
# returns the result. +all+ and +each+ are the same thing. +none+ is
|
311
|
+
# a combination of +not+ and +any+. +of+ is a combination of each
|
312
|
+
# and class, making it easy to specify that an Array should only
|
313
|
+
# contain objects of a specific type. An example:
|
314
|
+
#
|
315
|
+
# class: Hash
|
316
|
+
# each_key: {class: String}
|
317
|
+
# each_value:
|
318
|
+
# class: Array
|
319
|
+
# each: {class: Numeric, max: 100}
|
320
|
+
#
|
321
|
+
# This validator will only succeed for Hashes where all keys are
|
322
|
+
# Strings, all values are Arrays and each element of the Array are
|
323
|
+
# Numeric and maximum 100.
|
324
|
+
#
|
325
|
+
module EnumerableValidation
|
326
|
+
def check_of(obj, spec)
|
327
|
+
if spec.of
|
328
|
+
obj.all? do |v|
|
329
|
+
spec.of === v
|
330
|
+
end
|
331
|
+
else
|
332
|
+
true
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def check_each(obj, spec)
|
337
|
+
if spec.each
|
338
|
+
return false unless obj.respond_to? :each
|
339
|
+
xe = spec.each
|
340
|
+
obj.each do |val|
|
341
|
+
return false unless recurse_check(xe,val)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
true
|
345
|
+
end
|
346
|
+
|
347
|
+
def check_each_key(obj, spec)
|
348
|
+
if spec.each_key
|
349
|
+
return false unless obj.respond_to? :each_key
|
350
|
+
xe = spec.each_key
|
351
|
+
obj.each_key do |val|
|
352
|
+
return false unless recurse_check(xe,val)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
true
|
356
|
+
end
|
357
|
+
|
358
|
+
def check_each_value(obj, spec)
|
359
|
+
if spec.each_value
|
360
|
+
return false unless obj.respond_to? :each_value
|
361
|
+
xe = spec.each_value
|
362
|
+
obj.each_value do |val|
|
363
|
+
return false unless recurse_check(xe,val)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
true
|
367
|
+
end
|
368
|
+
|
369
|
+
def check_all(obj, spec)
|
370
|
+
if spec.all
|
371
|
+
return false unless obj.respond_to? :all?
|
372
|
+
xe = spec.all
|
373
|
+
obj.all? do |val|
|
374
|
+
recurse_check(xe,val)
|
375
|
+
end
|
376
|
+
else
|
377
|
+
true
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def check_any(obj, spec)
|
382
|
+
if spec.any
|
383
|
+
return false unless obj.respond_to? :any?
|
384
|
+
xe = spec.any
|
385
|
+
xe = [xe] unless xe.is_a? Array
|
386
|
+
obj.any? do |val|
|
387
|
+
xe.any? do |chk|
|
388
|
+
if chk.is_a? Specification
|
389
|
+
check_valid(val,chk)
|
390
|
+
else
|
391
|
+
chk == val
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
else
|
396
|
+
true
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
def check_none(obj, spec)
|
401
|
+
if spec.none
|
402
|
+
return false unless obj.respond_to? :any?
|
403
|
+
xe = spec.none
|
404
|
+
xe = [xe] unless xe.is_a? Array
|
405
|
+
!obj.any? do |val|
|
406
|
+
xe.any? do |chk|
|
407
|
+
if chk.is_a? Specification
|
408
|
+
check_valid(val,chk)
|
409
|
+
else
|
410
|
+
chk == val
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
else
|
415
|
+
true
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
private
|
420
|
+
def recurse_check(xe, val)
|
421
|
+
xe = [xe] unless xe.is_a? Array
|
422
|
+
xe.all? do |chk|
|
423
|
+
if chk.is_a? Specification
|
424
|
+
check_valid(val,chk)
|
425
|
+
else
|
426
|
+
chk == val
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
#
|
433
|
+
# Validates that the object in question +respond_to?+ one or more
|
434
|
+
# methods. This validation takes either a scalar or an array. If an
|
435
|
+
# array is provided, the object must +respond_to?+ all method names
|
436
|
+
# specified. Example:
|
437
|
+
#
|
438
|
+
# or:
|
439
|
+
# - respond_to: read
|
440
|
+
# - respond_to: [to_s, to_sym]
|
441
|
+
#
|
442
|
+
# This example will match all objects that either +respond_to?+
|
443
|
+
# +read+ or +respond_to?+ +to_s+ and +to_sym+.
|
444
|
+
#
|
445
|
+
module DuckValidation
|
446
|
+
def check_respond_to(obj, spec)
|
447
|
+
if spec.respond_to
|
448
|
+
rs = spec.respond_to
|
449
|
+
rs = [rs] unless rs.is_a? Array
|
450
|
+
rs.all? do |m|
|
451
|
+
obj.respond_to? m
|
452
|
+
end
|
453
|
+
else
|
454
|
+
true
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
#
|
460
|
+
# +ValueValidation+ will check specific values on objects. It
|
461
|
+
# provides +value+, +method_value+ and +instance_value+. +value+ is
|
462
|
+
# for check objects that +respond_to?+ [], +method_value+ will call
|
463
|
+
# the accessor with the name specified and +instance_value+ will
|
464
|
+
# check the +instance_variable+ with the specified name. An example
|
465
|
+
# for value could look like this:
|
466
|
+
#
|
467
|
+
# value:
|
468
|
+
# - - version
|
469
|
+
# - class: Float
|
470
|
+
# min: 0.1
|
471
|
+
# max: 9.9
|
472
|
+
# - - foo
|
473
|
+
# - each:
|
474
|
+
# class: Hash
|
475
|
+
# each_key: {class: Class}
|
476
|
+
# each_value: {class: String}
|
477
|
+
#
|
478
|
+
# This checks the values returned by obj['version'] and obj['foo']
|
479
|
+
#
|
480
|
+
module ValueValidation
|
481
|
+
def check_value(obj, spec)
|
482
|
+
if spec.value
|
483
|
+
return false unless obj.respond_to? :[]
|
484
|
+
spec.value.all? do |key,spec|
|
485
|
+
if spec.is_a? Specification
|
486
|
+
check_valid(obj[key],spec)
|
487
|
+
else
|
488
|
+
spec == obj[key]
|
489
|
+
end
|
490
|
+
end
|
491
|
+
else
|
492
|
+
true
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
def check_method_value(obj, spec)
|
497
|
+
if spec.method_value
|
498
|
+
spec.method_value.all? do |key,spec|
|
499
|
+
if spec.is_a? Specification
|
500
|
+
check_valid(obj.send(key),spec)
|
501
|
+
else
|
502
|
+
spec == obj.send(key)
|
503
|
+
end
|
504
|
+
end
|
505
|
+
else
|
506
|
+
true
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
def check_instance_value(obj, spec)
|
511
|
+
if spec.instance_value
|
512
|
+
spec.instance_value.all? do |key,spec|
|
513
|
+
if spec.is_a? Specification
|
514
|
+
check_valid(obj.instance_variable_get("@#{key}"),spec)
|
515
|
+
else
|
516
|
+
spec == obj.instance_variable_get("@#{key}")
|
517
|
+
end
|
518
|
+
end
|
519
|
+
else
|
520
|
+
true
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
# Makes it possible to compose Validators with & and |, just like that.
|
526
|
+
module ValidatorComposable
|
527
|
+
def &(other)
|
528
|
+
AndValidator.new(self,other)
|
529
|
+
end
|
530
|
+
def |(other)
|
531
|
+
OrValidator.new(self,other)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
# The base validator class. The frame which everything is built
|
536
|
+
# around. When adding new validation, make this class Mix-in
|
537
|
+
# them. There isn't really much to this class. Almost all
|
538
|
+
# functionality is mixed-in.
|
539
|
+
class Validator
|
540
|
+
include ValidatorComposable
|
541
|
+
include ComparisonValidation,
|
542
|
+
ScalarValidation,
|
543
|
+
EnumerableValidation,
|
544
|
+
DuckValidation,
|
545
|
+
ValueValidation,
|
546
|
+
BooleanValidation
|
547
|
+
|
548
|
+
def initialize(spec)
|
549
|
+
@spec = spec
|
550
|
+
end
|
551
|
+
|
552
|
+
# Checks that all submitted objects are valid ackording to this
|
553
|
+
# validator
|
554
|
+
def valid?(*objs)
|
555
|
+
objs.all? do |obj|
|
556
|
+
check_valid obj
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
private
|
561
|
+
# The method that does the real work of finding out which methods
|
562
|
+
# to call and calling them. Also the focal point of most Validator
|
563
|
+
# recursion.
|
564
|
+
def check_valid(obj, spec=@spec)
|
565
|
+
(methods + protected_methods + private_methods).grep(/^check_/) do |name|
|
566
|
+
next if name == 'check_valid'
|
567
|
+
return false unless self.send name, obj, spec
|
568
|
+
end
|
569
|
+
true
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
# Combines two validators into one that only succeeds if both succeeds separately
|
574
|
+
class AndValidator
|
575
|
+
include ValidatorComposable
|
576
|
+
|
577
|
+
def initialize(one, two)
|
578
|
+
@one, @two = one, two
|
579
|
+
end
|
580
|
+
|
581
|
+
def valid?(*objs)
|
582
|
+
@one.valid?(*objs) && @two.valid?(*objs)
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
# Combines two validators into one that succeeds if either of them succeeds
|
587
|
+
class OrValidator
|
588
|
+
include ValidatorComposable
|
589
|
+
|
590
|
+
def initialize(one, two)
|
591
|
+
@one, @two = one, two
|
592
|
+
end
|
593
|
+
|
594
|
+
def valid?(*objs)
|
595
|
+
@one.valid?(*objs) || @two.valid?(*objs)
|
596
|
+
end
|
597
|
+
end
|
598
|
+
end
|
data/test/test_basic.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), "..", "lib")
|
2
|
+
|
3
|
+
require 'ducktator'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
class TestBasicDuctator < Test::Unit::TestCase
|
7
|
+
def test_clazz_hash
|
8
|
+
assert Ducktator.valid?('class: Hash', {})
|
9
|
+
assert Ducktator.valid?('class: [Hash, Array]', [])
|
10
|
+
assert Ducktator.valid?('class: [Hash, Array]', {})
|
11
|
+
assert !Ducktator.valid?('class: Hash', [])
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_each_clazz
|
15
|
+
assert Ducktator.valid?('each: {class: String}',[])
|
16
|
+
assert Ducktator.valid?('each: {class: String}','a'..'z')
|
17
|
+
assert !Ducktator.valid?('each: {class: String}',[123])
|
18
|
+
assert !Ducktator.valid?('each: {class: String}',['abc',123])
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_all_clazz
|
22
|
+
assert Ducktator.valid?('all: {class: String}',[])
|
23
|
+
assert Ducktator.valid?('all: {class: String}','a'..'z')
|
24
|
+
assert !Ducktator.valid?('all: {class: String}',[123])
|
25
|
+
assert !Ducktator.valid?('all: {class: String}',['abc',123])
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_any_clazz
|
29
|
+
assert Ducktator.valid?('any: {class: String}','a'..'z')
|
30
|
+
assert Ducktator.valid?('any: {class: String}',['abc',123])
|
31
|
+
assert !Ducktator.valid?('any: {class: String}',[])
|
32
|
+
assert !Ducktator.valid?('any: {class: String}',[123])
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_none_clazz
|
36
|
+
assert !Ducktator.valid?('none: {class: String}','a'..'z')
|
37
|
+
assert Ducktator.valid?('none: {class: String}',[])
|
38
|
+
assert Ducktator.valid?('none: {class: String}',[123])
|
39
|
+
assert !Ducktator.valid?('none: {class: String}',['abc',123])
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_match
|
43
|
+
assert Ducktator.valid?({'match' => /abc/},'aabcd')
|
44
|
+
assert !Ducktator.valid?({'match' => /abc/},'aadcd')
|
45
|
+
assert Ducktator.valid?('match: abc','abc')
|
46
|
+
assert !Ducktator.valid?('match: abc','adbc')
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_each_value_clazz
|
50
|
+
assert Ducktator.valid?('each_value: {class: String}',{})
|
51
|
+
assert Ducktator.valid?('each_value: {class: String}',{123 => 'hello'})
|
52
|
+
assert Ducktator.valid?('each_value: {class: String}',{123 => 'hello', 321 => 'goodbye'})
|
53
|
+
assert !Ducktator.valid?('each_value: {class: String}',{'hello' => 123, 321 => 'goodbye'})
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_each_key_clazz
|
57
|
+
assert Ducktator.valid?('each_key: {class: String}',{})
|
58
|
+
assert Ducktator.valid?('each_key: {class: String}',{'hello' => 123})
|
59
|
+
assert Ducktator.valid?('each_key: {class: String}',{'hello' => 123, 'goodbye' => 321})
|
60
|
+
assert !Ducktator.valid?('each_key: {class: String}',{'hello' => 123, 321 => 'goodbye'})
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_is
|
64
|
+
assert Ducktator.valid?('is: abc', 'abc')
|
65
|
+
assert !Ducktator.valid?('is: abcd', 'abc')
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_respond_to
|
69
|
+
assert Ducktator.valid?('respond_to: to_s', 'abc')
|
70
|
+
assert Ducktator.valid?('respond_to: [to_s]', 'abc')
|
71
|
+
assert Ducktator.valid?('respond_to: [to_s, hash]', 'abc')
|
72
|
+
assert !Ducktator.valid?('respond_to: try1', 'abc')
|
73
|
+
assert !Ducktator.valid?('respond_to: [try1]', 'abc')
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), "..", "lib")
|
2
|
+
|
3
|
+
require 'ducktator'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
class TestComplexDuctator < Test::Unit::TestCase
|
7
|
+
TESTOBJ1 = {
|
8
|
+
'abc' => [
|
9
|
+
{Class => 'Clazz',
|
10
|
+
Module => 'Mod'},
|
11
|
+
{Regexp => 'Reggie',
|
12
|
+
Numeric => 'Num'}
|
13
|
+
],
|
14
|
+
'version' => 0.1
|
15
|
+
}
|
16
|
+
|
17
|
+
VALIDATOR1 = {
|
18
|
+
'each_key' => { 'class' => String },
|
19
|
+
'value' => [['version',{ 'class' => Float,
|
20
|
+
'min' => 0.1,
|
21
|
+
'max' => 9.9 }],
|
22
|
+
['abc',{ 'each' => { 'class' => Hash,
|
23
|
+
'each_key' => {'class' => Class},
|
24
|
+
'each_value' => ['class' => String]}}]
|
25
|
+
]}
|
26
|
+
|
27
|
+
VALIDATOR2 = <<VAL;
|
28
|
+
---
|
29
|
+
each_key: {class: String}
|
30
|
+
value:
|
31
|
+
- - version
|
32
|
+
- class: Float
|
33
|
+
min: 0.1
|
34
|
+
max: 9.9
|
35
|
+
- - abc
|
36
|
+
- each:
|
37
|
+
class: Hash
|
38
|
+
each_key: {class: Class}
|
39
|
+
each_value: {class: String}
|
40
|
+
VAL
|
41
|
+
|
42
|
+
def test_validation1
|
43
|
+
assert Ducktator.valid?(VALIDATOR1,TESTOBJ1)
|
44
|
+
assert Ducktator.valid?(VALIDATOR2,TESTOBJ1)
|
45
|
+
assert !Ducktator.valid?(VALIDATOR1,{ })
|
46
|
+
assert !Ducktator.valid?(VALIDATOR2,{ })
|
47
|
+
end
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: ducktator
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2006-09-19 00:00:00 +02:00
|
8
|
+
summary: Duck Type Validator - your own Ruby type dictator
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: ola@ologix.com
|
12
|
+
homepage: http://ducktator.rubyforge.org/
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Ola Bini
|
31
|
+
files:
|
32
|
+
- lib/ducktator.rb
|
33
|
+
- test/test_ducktator.rb
|
34
|
+
- test/test_basic.rb
|
35
|
+
- test/test_complex.rb
|
36
|
+
- LICENSE
|
37
|
+
- README
|
38
|
+
test_files: []
|
39
|
+
|
40
|
+
rdoc_options: []
|
41
|
+
|
42
|
+
extra_rdoc_files: []
|
43
|
+
|
44
|
+
executables: []
|
45
|
+
|
46
|
+
extensions: []
|
47
|
+
|
48
|
+
requirements: []
|
49
|
+
|
50
|
+
dependencies: []
|
51
|
+
|