ducktator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ require 'test/unit'
2
+ require 'test/test_basic'
3
+ require 'test/test_complex'
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
+