duck-hunt 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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
+ ~~~