duck-hunt 0.0.3

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.
Files changed (63) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +526 -0
  3. data/Rakefile +15 -0
  4. data/lib/duck-hunt.rb +17 -0
  5. data/lib/duck-hunt/hash_helpers.rb +28 -0
  6. data/lib/duck-hunt/properties.rb +13 -0
  7. data/lib/duck-hunt/properties/array.rb +81 -0
  8. data/lib/duck-hunt/properties/boolean.rb +10 -0
  9. data/lib/duck-hunt/properties/float.rb +10 -0
  10. data/lib/duck-hunt/properties/integer.rb +9 -0
  11. data/lib/duck-hunt/properties/nested_hash.rb +61 -0
  12. data/lib/duck-hunt/properties/nil.rb +15 -0
  13. data/lib/duck-hunt/properties/property.rb +85 -0
  14. data/lib/duck-hunt/properties/string.rb +10 -0
  15. data/lib/duck-hunt/properties/validator_lookup.rb +27 -0
  16. data/lib/duck-hunt/schemas.rb +8 -0
  17. data/lib/duck-hunt/schemas/array_schema.rb +254 -0
  18. data/lib/duck-hunt/schemas/hash_schema.rb +135 -0
  19. data/lib/duck-hunt/schemas/property_lookup.rb +32 -0
  20. data/lib/duck-hunt/schemas/schema_definition.rb +25 -0
  21. data/lib/duck-hunt/string_helpers.rb +25 -0
  22. data/lib/duck-hunt/validators.rb +16 -0
  23. data/lib/duck-hunt/validators/accepted_values.rb +19 -0
  24. data/lib/duck-hunt/validators/divisible_by.rb +19 -0
  25. data/lib/duck-hunt/validators/equal_to.rb +19 -0
  26. data/lib/duck-hunt/validators/greater_than.rb +19 -0
  27. data/lib/duck-hunt/validators/greater_than_or_equal_to.rb +19 -0
  28. data/lib/duck-hunt/validators/less_than.rb +19 -0
  29. data/lib/duck-hunt/validators/less_than_or_equal_to.rb +19 -0
  30. data/lib/duck-hunt/validators/matches.rb +18 -0
  31. data/lib/duck-hunt/validators/not_divisible_by.rb +19 -0
  32. data/lib/duck-hunt/validators/not_equal_to.rb +19 -0
  33. data/lib/duck-hunt/validators/rejected_values.rb +19 -0
  34. data/lib/duck-hunt/validators/validator.rb +16 -0
  35. data/lib/duck-hunt/version.rb +3 -0
  36. data/test/properties/array_test.rb +837 -0
  37. data/test/properties/boolean_test.rb +37 -0
  38. data/test/properties/float_test.rb +49 -0
  39. data/test/properties/integer_test.rb +48 -0
  40. data/test/properties/nested_hash_test.rb +465 -0
  41. data/test/properties/nil_test.rb +30 -0
  42. data/test/properties/property_test.rb +193 -0
  43. data/test/properties/string_test.rb +24 -0
  44. data/test/properties/validator_lookup_test.rb +25 -0
  45. data/test/schemas/array_schema_test.rb +797 -0
  46. data/test/schemas/hash_schema_test.rb +264 -0
  47. data/test/schemas/property_lookup_test.rb +41 -0
  48. data/test/schemas/schema_definition_test.rb +51 -0
  49. data/test/test_helper.rb +29 -0
  50. data/test/test_helper/test_classes.rb +74 -0
  51. data/test/validators/accepted_values_test.rb +46 -0
  52. data/test/validators/divisible_by_test.rb +38 -0
  53. data/test/validators/equal_to_test.rb +38 -0
  54. data/test/validators/greater_than_or_equal_to_test.rb +39 -0
  55. data/test/validators/greater_than_test.rb +39 -0
  56. data/test/validators/less_than_or_equal_to_test.rb +40 -0
  57. data/test/validators/less_than_test.rb +39 -0
  58. data/test/validators/matches_test.rb +43 -0
  59. data/test/validators/not_divisible_by_test.rb +38 -0
  60. data/test/validators/not_equal_to_test.rb +38 -0
  61. data/test/validators/rejected_values_test.rb +46 -0
  62. data/test/validators/validator_test.rb +23 -0
  63. metadata +196 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Thomas Cannon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,526 @@
1
+ # Duck Hunt
2
+ ## A dependency-free object validator for Ruby
3
+
4
+ Duck Typing is pretty fantastic, but sometimes you need to be sure your inputs match what you expect.
5
+
6
+ REST APIs are a great example: you've defined the structure of your endpoints and the parameters you expect, down to the datatype. Now you *could* throw in a bunch of conditionals to check input parameters, but that gets out of hand quickly and is a nightmare to maintain.
7
+
8
+ What if you could define how an object should look, and check if you're getting back what you expect.
9
+
10
+ So instead of:
11
+
12
+ ~~~ruby
13
+ def create
14
+ unless params[:user].present? and params[:user][:name].present? and params[:user][:age].present? and params[:user][:age].is_a? Integer and params[:user][:age] > 0 # ... you get the point
15
+ head :bad_request and return
16
+ end
17
+
18
+ # After that mouthful, actually do something
19
+
20
+ end
21
+ ~~~
22
+
23
+ You had:
24
+
25
+ ~~~ruby
26
+ module UserSchemas
27
+ def create
28
+ DuckHunt::Schemas::HashSchema.define :strict_mode => true do |user|
29
+ user.string "name", :required => true, :allow_nil => false
30
+ user.integer "age", :required => true, :allow_nil => false, :greater_than => 0
31
+ user.nested_hash "address", :required => true do |address|
32
+ address.string "state", :required => true
33
+ address.string "city", :required => false
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ class UserAPI
40
+ def create
41
+ head :bad_request and return unless UserSchemas.create.validate?(params[:user])
42
+
43
+ # the rest of your API call
44
+ end
45
+ end
46
+ ~~~
47
+
48
+ It's also *blazing fast*, since it's dependency-free and deals with plain Ruby objects.
49
+
50
+
51
+ ## Installation notes
52
+
53
+ Just add the following to your Gemfile:
54
+
55
+ ~~~ruby
56
+ gem 'duck-hunt'
57
+ ~~~
58
+
59
+ ## Requirements
60
+
61
+ Ruby 1.8.7+
62
+
63
+ That's it. This library was designed to be dependency-free, built entirely with Ruby. There are some parts that have been borrowed from activesupport, but they're baked into the library.
64
+
65
+
66
+ ## How it works
67
+
68
+ A schema has multiple properties, which can have multiple validators. That sounds complex, but the syntax is designed to help you understand exactly how an object's defined.
69
+
70
+ ### Schemas
71
+
72
+ Schemas are the top-level structure of the object. There are two types of Schemas: a Hash and an Array. These are the two types of objects you'll be checking.
73
+
74
+ You define a schema using the following syntax:
75
+
76
+ ~~~ruby
77
+ DuckHunt::Schemas::HashSchema.define do |hash|
78
+ # define hash key/valaue pairings here
79
+ end
80
+
81
+ # OR
82
+
83
+ DuckHunt::Schemas::ArraySchema.define do |array|
84
+ # define array entry properties here
85
+ end
86
+ ~~~
87
+
88
+ `define` returns an instance of the schema defined by the block you gave it. Calling `validate?` on this instance with a ruby object validates the object against the and returns a boolean. If the object is not valid, the `errors` method returns the errors explaining what went wrong
89
+
90
+ ~~~ruby
91
+ schema = DuckHunt::Schemas::HashSchema.define do |x|
92
+ x.string "name"
93
+ end
94
+
95
+ schema.validate?(:name => "hey")
96
+ #=> true
97
+
98
+ schema.validate?(:name => 12)
99
+ #=> false
100
+
101
+ schema.errors
102
+ #=> {"name"=>["wrong type"]}
103
+ ~~~
104
+
105
+ #### Hash Schemas
106
+
107
+ When using Duck Hunt to validate hashes, you're asking "does this hash have the following keys, and is the key's value what I expect?"
108
+
109
+ The basic syntax for defining a hash schema is:
110
+
111
+ ~~~ruby
112
+ DuckHunt::Schemas::HashSchema.define do |x|
113
+ #x.key_type "key_name", :any_other => 1, :validators => true
114
+
115
+ x.string "name", :matches => /\w+\s\w+/
116
+ x.string "title", :required => false
117
+ end
118
+ ~~~
119
+
120
+ Any property added to the hash schema is required by default. You can change that behavior by adding `:required => false` to the property definition. For clarity, I recommend always setting the `:required` option.
121
+
122
+ A property can only be defined in a schema once. Otherwise, a `DuckHunt:::PropertyAlreadyDefined` exception is thrown.
123
+
124
+ There are two types of validation for hash schemas: Strict and Relaxed. The validation type is controlled by the `:strict_mode` option in the `define` method.
125
+
126
+ ##### Strict Validation
127
+
128
+ Strict Validation is the default type of validation for a Hash Schema. It validates that the object does not have any keys that are not defined in the schema:
129
+
130
+ ~~~ruby
131
+ strict_schema = DuckHunt::Schemas::HashSchema.define, :strict_mode => true do |x|
132
+ x.string "name"
133
+ end
134
+
135
+ strict_schema.validate?({:name => "Jane"})
136
+ #=> true
137
+
138
+ strict_schema.validate?({:name => "Jane", :age => 21})
139
+ #=> false
140
+
141
+ strict_schema.errors
142
+ #=> {"base"=>["has properties not defined in schema"]}
143
+ ~~~
144
+
145
+ ##### Relaxed Validation
146
+
147
+ Relaxed validation does not care if the object has keys that are not defined in the schema:
148
+
149
+ ~~~ruby
150
+ relaxed_schema = DuckHunt::Schemas::HashSchema.define, :strict_mode => false do |x|
151
+ x.string "name"
152
+ end
153
+
154
+ relaxed_schema.validate?({:name => "Jane"})
155
+ #=> true
156
+
157
+ relaxed_schema.validate?({:name => "Jane", :age => 21})
158
+ #=> true
159
+ ~~~
160
+
161
+
162
+ ##### Allowing a nil object
163
+
164
+ If you don't care whether the object is `nil` or not, you can set `:allow_nil => true` in the `define` method:
165
+
166
+ ~~~ruby
167
+ nil_schema = DuckHunt::Schemas::HashSchema.define, :allow_nil => false do |x|
168
+ x.string "name"
169
+ end
170
+
171
+ nil_schema.validate?({:name => "Jane"})
172
+ #=> true
173
+
174
+ nil_schema.validate?(nil)
175
+ #=> true
176
+ ~~~
177
+
178
+ #### Array Schemas
179
+
180
+ When using Duck Hunt to validate hashes, you're asking "does this array contain the values that I expect?" There are two types of Array Schemas, each with vastly different definitions and behaviors.
181
+
182
+ ##### Single-type Arrays
183
+
184
+ A single type array means that every item in the array has the same type and matches the same properties.
185
+
186
+ You define a single type array by adding a single property in the schema definition:
187
+
188
+ ~~~ruby
189
+ schema = DuckHunt::Schemas::ArraySchema.define do |x|
190
+ x.integer
191
+ end
192
+
193
+ schema.validate?([1,2,3])
194
+ #=> true
195
+
196
+ schema.validate?([1,"whoops",3])
197
+ #=> false
198
+
199
+ schema.errors
200
+ #=> {"1"=>["wrong type"]}
201
+ ~~~
202
+
203
+ You can also set a minmum size for the array, a maximum size, or both!
204
+
205
+ ~~~ruby
206
+ minimum_schema = DuckHunt::Schemas::ArraySchema.define :min_size => 2 do |x|
207
+ x.integer
208
+ end
209
+
210
+ minimum_schema.validate?([1,2])
211
+ #=> true
212
+
213
+ minimum_schema.validate?([1])
214
+ #=> false
215
+
216
+ minimum_schema.errors
217
+ #=> {"base" => ["expected at least 2 item(s) but got 1 item(s)"]}
218
+ ~~~
219
+
220
+ ~~~ruby
221
+ max_schema = DuckHunt::Schemas::ArraySchema.define :max_size => 2 do |x|
222
+ x.integer
223
+ end
224
+
225
+ max_schema.validate?([1,2])
226
+ #=> true
227
+
228
+ max_schema.validate?([1,2,3])
229
+ #=> false
230
+
231
+ max_schema.errors
232
+ #=> {"base" => ["expected at most 2 item(s) but got 3 item(s)"]}
233
+ ~~~
234
+
235
+ ~~~ruby
236
+ max_schema = DuckHunt::Schemas::ArraySchema.define :min_size => 2 :max_size => 3 do |x|
237
+ x.integer
238
+ end
239
+
240
+ max_schema.validate?([1])
241
+ #=> false
242
+
243
+ max_schema.errors
244
+ #=> {"base" => ["expected at least 2 item(s) but got 1 item(s)"]}
245
+
246
+ max_schema.validate?([1,2])
247
+ #=> true
248
+
249
+ max_schema.validate?([1,2,3])
250
+ #=> true
251
+
252
+ max_schema.validate?([1,2,3,4])
253
+ #=> false
254
+
255
+ max_schema.errors
256
+ #=> {"base" => ["expected at most 3 item(s) but got 4 item(s)"]}
257
+ ~~~
258
+
259
+ ##### Tuple Arrays
260
+
261
+ A tuple array is an ordered array that can have mixed types. It expects a defined number of required items, and may have optional items at the end of the array. All items in the array must match the type defined for that index.
262
+
263
+ To define the required items for a tuple array, you call `items` in the `define` block:
264
+
265
+ ~~~ruby
266
+ tuple_schema = DuckHunt::Schemas::ArraySchema.define do |x|
267
+ x.items do |s|
268
+ s.integer
269
+ s.string
270
+ end
271
+ end
272
+
273
+ tuple_schema.validate?([1,"hello"])
274
+ #=> true
275
+
276
+ tuple_schema.validate?([1])
277
+ #=> false
278
+
279
+ tuple_schema.errors
280
+ #=> { "base" => "expected at least 2 item(s) but got 1 item(s)"}
281
+
282
+ tuple_schema.validate?([1,"hello", 3])
283
+ #=> false
284
+
285
+ tuple_schema.errors
286
+ #=> { "base" => "expected at most 2 item(s) but got 3 item(s)"}
287
+
288
+ tuple_schema.validate?([1,2])
289
+ #=> false
290
+
291
+ tuple_schema.errors
292
+ #=> { "1" => "wrong type" }
293
+ ~~~
294
+
295
+ Likewise, to define to optional itmes for a tuple array, you call `optional_items` in the `define` block. **Note that the object does not have to have *every* optional item.**
296
+
297
+ ~~~ruby
298
+ tuple_schema = DuckHunt::Schemas::ArraySchema.define do |x|
299
+ x.items do |s|
300
+ s.integer
301
+ s.string
302
+ end
303
+
304
+ x.optional_items do |y|
305
+ y.string
306
+ y.integer
307
+ end
308
+ end
309
+
310
+ tuple_schema.validate?([1,"hello"])
311
+ #=> true
312
+
313
+ tuple_schema.validate?([1,"hello", 3])
314
+ #=> false
315
+
316
+ tuple_schema.errors
317
+ #=> { "2" => "wrong type"}
318
+
319
+ tuple_schema.validate?([1,"hello", "world"])
320
+ #=> true
321
+
322
+ tuple_schema.validate?([1,"hello", "world", 3, 4])
323
+ #=> false
324
+
325
+ tuple_schema.errors
326
+ #=> { "base" => "expected at most 4 item(s) but got 5 item(s)"}
327
+ ~~~
328
+
329
+ ##### Allowing a nil object
330
+
331
+ If you don't care whether the object is `nil` or not, you can set `:allow_nil => true` in the `define` method:
332
+
333
+ ~~~ruby
334
+ nil_schema = DuckHunt::Schemas::Array.define, :allow_nil => false do |x|
335
+ x.integer
336
+ end
337
+
338
+ nil_schema.validate?([1,2,3])
339
+ #=> true
340
+
341
+ nil_schema.validate?(nil)
342
+ #=> true
343
+ ~~~
344
+
345
+ ### Properties
346
+
347
+ Properties are the datatypes you can validate against in your schemas. They cover the basic datatypes you'd see when converying JSON to a ruby object:
348
+
349
+ * Array
350
+ * Boolean
351
+ * Float
352
+ * Integer
353
+ * Nested Hash
354
+ * Nil
355
+ * String
356
+
357
+ #### Nested Arrays and Hashes
358
+
359
+ Sometimes you need nested objects, like nested hashes or multi-dimensional arrays. It's really easy to define these in Duck Hunt:
360
+
361
+ ~~~
362
+ nested_hash = DuckHunt::Schemas::HashSchema.define do |x|
363
+ x.nested_hash "name" do |s|
364
+ s.string "first_name"
365
+ s.string "last_name"
366
+ end
367
+ end
368
+
369
+ nested_hash.validate?({:name => {:first_name => "Jane", :last_name => "Doe"}})
370
+ #=> true
371
+
372
+ nested_hash.validate?({:name => "hello"})
373
+ #=> false
374
+
375
+ nested_hash.errors
376
+ #=> {"name"=>{"base"=>["wrong type"]}}
377
+
378
+ nested_hash.validate?({:name => {:first_name => "Jane", :last_name => 1}})
379
+ #=> false
380
+
381
+ nested_hash.errors
382
+ #=> {"name"=>{"last_name"=>["wrong type"]}}
383
+ ~~~
384
+
385
+ ~~~ruby
386
+ multi_array = DuckHunt::Schemas::ArraySchema.define do |x|
387
+ x.array do |y|
388
+ y.integer
389
+ end
390
+ end
391
+
392
+ multi_array.validate?([[1,2],[3,4]])
393
+ #=> true
394
+
395
+ multi_array.validate?([[1,2],"hello"])
396
+ #=> false
397
+
398
+ multi_array.errors
399
+ #=> {"1"=>{"base"=>["wrong type"]}}
400
+
401
+ multi_array.validate?([[1,2],["hello",4]])
402
+ #=> false
403
+
404
+ multi_array.errors
405
+ #=> {"1"=>{"0"=>["wrong type"]}}
406
+ ~~~
407
+
408
+
409
+ ### Validators
410
+
411
+ Validators can be attached to any property to check if the value follows certain behavior. Each validator has its own error message
412
+
413
+ #### Accepted Values
414
+
415
+ This property can only have the values in this list
416
+
417
+ ~~~ruby
418
+ schema = DuckHunt::Schemas::HashSchema.define do |x|
419
+ x.integer "cats" :accepted_values => [1,2,3]
420
+ end
421
+
422
+ schema.validates?({:cats => 4})
423
+ #=> false
424
+
425
+ schema.errors
426
+ #=> {"0" => "not an accepted value"}
427
+ ~~~
428
+
429
+ #### Accepted Values
430
+
431
+ This property cannot have any of the values in this list
432
+
433
+ ~~~ruby
434
+ schema = DuckHunt::Schemas::HashSchema.define do |x|
435
+ x.integer "cats" :rejected_values => [1,2,3]
436
+ end
437
+
438
+ schema.validates?({:cats => 1})
439
+ #=> false
440
+
441
+ schema.errors
442
+ #=> {"0" => "a rejected value"}
443
+ ~~~
444
+
445
+ ### Matches Regular Expression
446
+
447
+ This property is only valid if it matches the regular expression given
448
+
449
+ ~~~ruby
450
+ schema = DuckHunt::Schemas::HashSchema.define do |x|
451
+ x.string "name" :matches => /\w+\s\w+/
452
+ end
453
+
454
+ schema.validates?({ :name => "Bob" })
455
+ #=> false
456
+
457
+ schema.errors
458
+ #=> {"0" => "No matches for Regexp"}
459
+ ~~~
460
+
461
+ #### Divisible By
462
+
463
+ This property is only valid if it's divisble by the number provided
464
+
465
+ ~~~ruby
466
+ schema = DuckHunt::Schemas::HashSchema.define do |x|
467
+ x.integer "cats" :divisible_by => 3
468
+ end
469
+
470
+ schema.validates?({:cats => 4})
471
+ #=> false
472
+
473
+ schema.errors
474
+ #=> {"0" => "not divisible by `3`"}
475
+ ~~~
476
+
477
+ #### Not Divisible By
478
+
479
+ This property is only valid if it's not divisble by the number provided
480
+
481
+ ~~~ruby
482
+ schema = DuckHunt::Schemas::HashSchema.define do |x|
483
+ x.integer "cats" :not_divisible_by => 3
484
+ end
485
+
486
+ schema.validates?({:cats => 6})
487
+ #=> false
488
+
489
+ schema.errors
490
+ #=> {"0" => "divisible by `3`"}
491
+ ~~~
492
+
493
+ #### Standard comparisons: equal to, greater than (or equal to), less than (or equal to), not equal to
494
+
495
+ This property is only valid if it fits the comparison. The comparisons defined are:
496
+
497
+ * `:equal_to`
498
+ * `:not_equal_to`
499
+ * `:greater_than`
500
+ * `:greater_than_or_equal_to`
501
+ * `:less_than`
502
+ * `:less_than_or_equal_to`
503
+
504
+ ~~~ruby
505
+ schema = DuckHunt::Schemas::HashSchema.define do |x|
506
+ x.integer "cats" :greater_than => 4
507
+ end
508
+
509
+ schema.validates?({:cats => 4})
510
+ #=> false
511
+
512
+ schema.errors
513
+ #=> {"0" => "not greater than `4`"}
514
+ ~~~
515
+
516
+ ~~~ruby
517
+ schema = DuckHunt::Schemas::HashSchema.define do |x|
518
+ x.integer "name" :equal => "bob"
519
+ end
520
+
521
+ schema.validates?({:name => "Jim"})
522
+ #=> false
523
+
524
+ schema.errors
525
+ #=> {"0" => "not equal to `bob`"}
526
+ ~~~