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.
- data/LICENSE +20 -0
- data/README.md +526 -0
- data/Rakefile +15 -0
- data/lib/duck-hunt.rb +17 -0
- data/lib/duck-hunt/hash_helpers.rb +28 -0
- data/lib/duck-hunt/properties.rb +13 -0
- data/lib/duck-hunt/properties/array.rb +81 -0
- data/lib/duck-hunt/properties/boolean.rb +10 -0
- data/lib/duck-hunt/properties/float.rb +10 -0
- data/lib/duck-hunt/properties/integer.rb +9 -0
- data/lib/duck-hunt/properties/nested_hash.rb +61 -0
- data/lib/duck-hunt/properties/nil.rb +15 -0
- data/lib/duck-hunt/properties/property.rb +85 -0
- data/lib/duck-hunt/properties/string.rb +10 -0
- data/lib/duck-hunt/properties/validator_lookup.rb +27 -0
- data/lib/duck-hunt/schemas.rb +8 -0
- data/lib/duck-hunt/schemas/array_schema.rb +254 -0
- data/lib/duck-hunt/schemas/hash_schema.rb +135 -0
- data/lib/duck-hunt/schemas/property_lookup.rb +32 -0
- data/lib/duck-hunt/schemas/schema_definition.rb +25 -0
- data/lib/duck-hunt/string_helpers.rb +25 -0
- data/lib/duck-hunt/validators.rb +16 -0
- data/lib/duck-hunt/validators/accepted_values.rb +19 -0
- data/lib/duck-hunt/validators/divisible_by.rb +19 -0
- data/lib/duck-hunt/validators/equal_to.rb +19 -0
- data/lib/duck-hunt/validators/greater_than.rb +19 -0
- data/lib/duck-hunt/validators/greater_than_or_equal_to.rb +19 -0
- data/lib/duck-hunt/validators/less_than.rb +19 -0
- data/lib/duck-hunt/validators/less_than_or_equal_to.rb +19 -0
- data/lib/duck-hunt/validators/matches.rb +18 -0
- data/lib/duck-hunt/validators/not_divisible_by.rb +19 -0
- data/lib/duck-hunt/validators/not_equal_to.rb +19 -0
- data/lib/duck-hunt/validators/rejected_values.rb +19 -0
- data/lib/duck-hunt/validators/validator.rb +16 -0
- data/lib/duck-hunt/version.rb +3 -0
- data/test/properties/array_test.rb +837 -0
- data/test/properties/boolean_test.rb +37 -0
- data/test/properties/float_test.rb +49 -0
- data/test/properties/integer_test.rb +48 -0
- data/test/properties/nested_hash_test.rb +465 -0
- data/test/properties/nil_test.rb +30 -0
- data/test/properties/property_test.rb +193 -0
- data/test/properties/string_test.rb +24 -0
- data/test/properties/validator_lookup_test.rb +25 -0
- data/test/schemas/array_schema_test.rb +797 -0
- data/test/schemas/hash_schema_test.rb +264 -0
- data/test/schemas/property_lookup_test.rb +41 -0
- data/test/schemas/schema_definition_test.rb +51 -0
- data/test/test_helper.rb +29 -0
- data/test/test_helper/test_classes.rb +74 -0
- data/test/validators/accepted_values_test.rb +46 -0
- data/test/validators/divisible_by_test.rb +38 -0
- data/test/validators/equal_to_test.rb +38 -0
- data/test/validators/greater_than_or_equal_to_test.rb +39 -0
- data/test/validators/greater_than_test.rb +39 -0
- data/test/validators/less_than_or_equal_to_test.rb +40 -0
- data/test/validators/less_than_test.rb +39 -0
- data/test/validators/matches_test.rb +43 -0
- data/test/validators/not_divisible_by_test.rb +38 -0
- data/test/validators/not_equal_to_test.rb +38 -0
- data/test/validators/rejected_values_test.rb +46 -0
- data/test/validators/validator_test.rb +23 -0
- 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
|