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 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
+