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