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
@@ -0,0 +1,135 @@
1
+ module DuckHunt
2
+ module Schemas
3
+ class HashSchema
4
+ # Valditation is performed in the following steps:
5
+ # 1. Check that the object being validated is of the correct type
6
+ # 2. Iterate through each defined property and check that the object's property value is `valid?`
7
+ # 3. If the schema validation has been set to "strict mode", check that the object doesn't have any properties that are not defined in the schema
8
+ include Schemas::SchemaDefinition
9
+ include Schemas::PropertyLookup
10
+
11
+ require 'set'
12
+
13
+ def initialize(var={})
14
+ DuckHunt::HashHelpers.stringify_keys!(var)
15
+ options = {"strict_mode" => true, "allow_nil" => false}.merge(var)
16
+ @strict_mode = options["strict_mode"]
17
+ @allow_nil = options["allow_nil"]
18
+ @properties = {}
19
+ #a key-value pair of all the required properties in the schema, references objects in `@properties`
20
+ @required_properties = {}
21
+ @errors = {}
22
+ end
23
+
24
+ def properties
25
+ return @properties.dup
26
+ end
27
+
28
+ def strict_mode
29
+ return @strict_mode
30
+ end
31
+
32
+ def strict_mode?
33
+ return strict_mode
34
+ end
35
+
36
+ def allow_nil
37
+ return @allow_nil
38
+ end
39
+
40
+ def allow_nil?
41
+ return @allow_nil
42
+ end
43
+
44
+ def validate?(object_being_validated)
45
+ @errors.clear #reset since we are revalidating
46
+ if object_being_validated.nil?
47
+ return true if allow_nil?
48
+ add_base_error_message(NIL_OBJECT_NOT_ALLOWED_MESSAGE)
49
+ return false
50
+ end
51
+ return false unless matches_type?(object_being_validated)
52
+ #now that we know the type matches, we can stringify the hash's keys
53
+ DuckHunt::HashHelpers.stringify_keys!(object_being_validated)
54
+ return false unless conforms_to_strict_mode_setting?(object_being_validated)
55
+ return false unless objects_properties_are_valid_and_has_all_required_properties?(object_being_validated)
56
+ return true
57
+ end
58
+
59
+ def errors
60
+ @properties.each{|name, property| @errors[name] = property.errors unless property.errors.empty? }
61
+ return DuckHunt::HashHelpers.stringify_keys(@errors)
62
+ end
63
+
64
+ protected
65
+
66
+ def add_property(property_constant, *args, &block)
67
+ name = args.shift.to_s
68
+ raise ArgumentError, "Property name cannot be blank" if name.nil? or name.empty?
69
+ raise PropertyAlreadyDefined, "`#{name}` has already been defined in this schema" if @properties.has_key?(name)
70
+ @properties[name] = property_constant.new(*args, &block)
71
+ if @properties[name].required?
72
+ @required_properties[name] = @properties[name]
73
+ end
74
+ end
75
+
76
+ def add_base_error_message(message)
77
+ @errors[:base] = [] if @errors[:base].nil?
78
+ @errors[:base] << message
79
+ end
80
+
81
+ def conforms_to_strict_mode_setting?(object_being_validated)
82
+ object_properties_set = object_being_validated.keys.to_set
83
+ schema_properties_set = properties.keys.to_set
84
+ # if we are in strict mode, we have to check that the object's properties are a subset
85
+ # of the schema (do not contain any extra properties).
86
+ if strict_mode?
87
+ if !(object_properties_set.subset? schema_properties_set)
88
+ add_base_error_message("has properties not defined in schema")
89
+ return false
90
+ end
91
+ end
92
+ #passes test, or is not in strict mode
93
+ return true
94
+ end
95
+
96
+ def objects_properties_are_valid_and_has_all_required_properties?(object_being_validated)
97
+ object_valid = missing_required_properties?(object_being_validated)
98
+ object_being_validated.each do |name, value|
99
+ next unless properties.has_key?(name.to_s)
100
+
101
+ # we want to validate every property, even if the object is invalid. That way, the
102
+ # errors are correctly populated for each property
103
+ property_is_valid = properties[name.to_s].valid?(value)
104
+
105
+ # switch the object's validity to false if a property is invalid. This will not
106
+ # revert back if a propety is true and the `object_valid == true` check prevents
107
+ # unnecessary re-assignments to false
108
+ if object_valid == true and property_is_valid == false
109
+ object_valid = false
110
+ end
111
+ end
112
+
113
+ return object_valid
114
+ end
115
+
116
+ def missing_required_properties?(object_being_validated)
117
+ missing_required_properties = @required_properties.keys - object_being_validated.keys
118
+ unless missing_required_properties.empty?
119
+ missing_required_properties.each{|name| properties[name.to_s].add_required_error }
120
+ return false
121
+ end
122
+ return true
123
+ end
124
+
125
+ #implemented by the class to do a top-level check that the object is even of the right type
126
+ def matches_type?(object_being_validated)
127
+ unless object_being_validated.is_a? Hash
128
+ add_base_error_message(DuckHunt::TYPE_MISMATCH_MESSAGE)
129
+ return false
130
+ end
131
+ return true
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,32 @@
1
+ module DuckHunt
2
+ module Schemas
3
+ module PropertyLookup
4
+ def method_missing(method_name, *args, &block)
5
+ #convert the method name into camel case and turn it into a symbol.
6
+ camelized_string = DuckHunt::StringHelpers.camelize(method_name.to_s)
7
+ property_symbol = camelized_string.to_sym
8
+ #check if this property has been defined.
9
+ if property_definition_exists?(property_symbol)
10
+ property_constant = get_property_constant(property_symbol)
11
+ add_property(property_constant, *args, &block)
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ protected
18
+
19
+ def property_definition_exists?(property)
20
+ DuckHunt::Properties.const_defined?(property)
21
+ end
22
+
23
+ def get_property_constant(property)
24
+ DuckHunt::Properties.const_get(property)
25
+ end
26
+
27
+ def add_property(property_constant, *args, &block)
28
+ raise NotImplementedError
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ module DuckHunt
2
+ module Schemas
3
+ module SchemaDefinition
4
+ # This module encompasses all of the logic required to define a schema, along with the necessary instance variables.
5
+ # Schema Definition requires the following:
6
+ # * using the block passed through the `define` method to define the Schema for this instance
7
+ # * Finding the correct Property subclass based on the method name in the `define` class (e.g: `schema.string "name"`)
8
+ # * Accepting the `strict!` method, which sets Schema validation to strict mode
9
+ # * Storing/Retrieving the Property instances defined as part of the schema
10
+
11
+ # Include class-level methods
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+
16
+ module ClassMethods
17
+ def define(*args, &block)
18
+ instance = self.new(*args)
19
+ instance.instance_eval(&block)
20
+ return instance
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # add support for camelization (to make property/validation lookup easier)
2
+ # keep these methods in their own modules so they don't conflict with other libraries that might
3
+ # be loaded (e.g: activesupport)
4
+ module DuckHunt
5
+ module StringHelpers
6
+ def self.camelize(string, first_letter = :upper)
7
+ case first_letter
8
+ when :upper
9
+ self.camelize_string(string, true)
10
+ when :lower
11
+ self.camelize_string(string, false)
12
+ end
13
+ end
14
+
15
+ protected
16
+
17
+ def self.camelize_string(lower_case_and_underscored_word, first_letter_in_uppercase = true)
18
+ if first_letter_in_uppercase
19
+ lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
20
+ else
21
+ lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ module DuckHunt
2
+ module Validators
3
+ autoload :Validator, File.dirname(__FILE__) + "/validators/validator.rb"
4
+ autoload :Matches, File.dirname(__FILE__) + "/validators/matches.rb"
5
+ autoload :GreaterThan, File.dirname(__FILE__) + "/validators/greater_than.rb"
6
+ autoload :GreaterThanOrEqualTo, File.dirname(__FILE__) + "/validators/greater_than_or_equal_to.rb"
7
+ autoload :LessThan, File.dirname(__FILE__) + "/validators/less_than.rb"
8
+ autoload :LessThanOrEqualTo, File.dirname(__FILE__) + "/validators/less_than_or_equal_to.rb"
9
+ autoload :EqualTo, File.dirname(__FILE__) + "/validators/equal_to.rb"
10
+ autoload :NotEqualTo, File.dirname(__FILE__) + "/validators/not_equal_to.rb"
11
+ autoload :DivisibleBy, File.dirname(__FILE__) + "/validators/divisible_by.rb"
12
+ autoload :NotDivisibleBy, File.dirname(__FILE__) + "/validators/not_divisible_by.rb"
13
+ autoload :AcceptedValues, File.dirname(__FILE__) + "/validators/accepted_values.rb"
14
+ autoload :RejectedValues, File.dirname(__FILE__) + "/validators/rejected_values.rb"
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ module DuckHunt
2
+ module Validators
3
+ class AcceptedValues < Validator
4
+ attr_reader :values
5
+ def initialize(*args)
6
+ @values = args[0]
7
+ raise ArgumentError, "an array must be provided" unless @values.is_a? Array
8
+ end
9
+
10
+ def valid?(value)
11
+ return @values.include? value
12
+ end
13
+
14
+ def error_message
15
+ return "not an accepted value"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module DuckHunt
2
+ module Validators
3
+ class DivisibleBy < Validator
4
+ attr_reader :value
5
+ def initialize(*args)
6
+ @value = args[0]
7
+ raise ArgumentError, "a value must be provided" if @value.nil?
8
+ end
9
+
10
+ def valid?(value)
11
+ return (value % @value).zero?
12
+ end
13
+
14
+ def error_message
15
+ return "not divisible by `#{@value}`"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module DuckHunt
2
+ module Validators
3
+ class EqualTo < Validator
4
+ attr_reader :value
5
+ def initialize(*args)
6
+ @value = args[0]
7
+ raise ArgumentError, "a value must be provided" if @value.nil?
8
+ end
9
+
10
+ def valid?(value)
11
+ return value == @value
12
+ end
13
+
14
+ def error_message
15
+ return "not equal to `#{@value}`"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module DuckHunt
2
+ module Validators
3
+ class GreaterThan < Validator
4
+ attr_reader :value
5
+ def initialize(*args)
6
+ @value = args[0]
7
+ raise ArgumentError, "a value must be provided" if @value.nil?
8
+ end
9
+
10
+ def valid?(value)
11
+ return value > @value
12
+ end
13
+
14
+ def error_message
15
+ return "less than `#{@value}`"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module DuckHunt
2
+ module Validators
3
+ class GreaterThanOrEqualTo < Validator
4
+ attr_reader :value
5
+ def initialize(*args)
6
+ @value = args[0]
7
+ raise ArgumentError, "a value must be provided" if @value.nil?
8
+ end
9
+
10
+ def valid?(value)
11
+ return value >= @value
12
+ end
13
+
14
+ def error_message
15
+ return "less than `#{@value}`"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module DuckHunt
2
+ module Validators
3
+ class LessThan < Validator
4
+ attr_reader :value
5
+ def initialize(*args)
6
+ @value = args[0]
7
+ raise ArgumentError, "a value must be provided" if @value.nil?
8
+ end
9
+
10
+ def valid?(value)
11
+ return value < @value
12
+ end
13
+
14
+ def error_message
15
+ return "greater than `#{@value}`"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module DuckHunt
2
+ module Validators
3
+ class LessThanOrEqualTo < Validator
4
+ attr_reader :value
5
+ def initialize(*args)
6
+ @value = args[0]
7
+ raise ArgumentError, "a value must be provided" if @value.nil?
8
+ end
9
+
10
+ def valid?(value)
11
+ return value <= @value
12
+ end
13
+
14
+ def error_message
15
+ return "greater than `#{@value}`"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ module DuckHunt
2
+ module Validators
3
+ class Matches < Validator
4
+ attr_reader :regex
5
+ def initialize(*args)
6
+ @regex = Regexp.new(args[0])
7
+ end
8
+
9
+ def valid?(value)
10
+ return @regex.match(value) != nil
11
+ end
12
+
13
+ def error_message
14
+ return "No matches for Regexp"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module DuckHunt
2
+ module Validators
3
+ class NotDivisibleBy < Validator
4
+ attr_reader :value
5
+ def initialize(*args)
6
+ @value = args[0]
7
+ raise ArgumentError, "a value must be provided" if @value.nil?
8
+ end
9
+
10
+ def valid?(value)
11
+ return !(value % @value).zero?
12
+ end
13
+
14
+ def error_message
15
+ return "divisible by `#{@value}`"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module DuckHunt
2
+ module Validators
3
+ class NotEqualTo < Validator
4
+ attr_reader :value
5
+ def initialize(*args)
6
+ @value = args[0]
7
+ raise ArgumentError, "a value must be provided" if @value.nil?
8
+ end
9
+
10
+ def valid?(value)
11
+ return value != @value
12
+ end
13
+
14
+ def error_message
15
+ return "equal to `#{@value}`"
16
+ end
17
+ end
18
+ end
19
+ end