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