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
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'lib'
5
+ t.libs << 'test'
6
+ t.pattern = 'test/**/*_test.rb'
7
+ end
8
+
9
+ desc "Run tests"
10
+ task :default => :test
11
+
12
+
13
+ task :console do
14
+ exec "irb -r duck-hunt -I ./lib"
15
+ end
data/lib/duck-hunt.rb ADDED
@@ -0,0 +1,17 @@
1
+ module DuckHunt
2
+ class MethodNotDefined < StandardError; end
3
+ class AbstractClass < StandardError; end
4
+ class PropertyAlreadyDefined < StandardError; end
5
+ class ValidatorAlreadyDefined < StandardError; end
6
+ class InvalidSchema < StandardError; end
7
+
8
+ TYPE_MISMATCH_MESSAGE = "wrong type"
9
+ REQUIRED_MESSAGE = "required"
10
+ NIL_OBJECT_NOT_ALLOWED_MESSAGE = "nil object not allowed"
11
+
12
+ autoload :StringHelpers, File.dirname(__FILE__) + '/duck-hunt/string_helpers.rb'
13
+ autoload :HashHelpers, File.dirname(__FILE__) + '/duck-hunt/hash_helpers.rb'
14
+ autoload :Schemas, File.dirname(__FILE__) + '/duck-hunt/schemas.rb'
15
+ autoload :Properties, File.dirname(__FILE__) + '/duck-hunt/properties.rb'
16
+ autoload :Validators, File.dirname(__FILE__) + '/duck-hunt/validators.rb'
17
+ end
@@ -0,0 +1,28 @@
1
+ # Support `stringify_keys` and `symbolize_keys`
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 HashHelpers
6
+ def self.stringify_keys!(hash)
7
+ hash.keys.each do |key|
8
+ hash[key.to_s] = hash.delete(key)
9
+ end
10
+ return hash
11
+ end
12
+
13
+ def self.stringify_keys(hash)
14
+ return stringify_keys!(hash.dup)
15
+ end
16
+
17
+ def self.symbolize_keys!(hash)
18
+ hash.keys.each do |key|
19
+ hash[(key.to_sym rescue key) || key] = hash.delete(key)
20
+ end
21
+ return hash
22
+ end
23
+
24
+ def self.symbolize_keys(hash)
25
+ return symbolize_keys!(hash.dup)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ module DuckHunt
2
+ module Properties
3
+ autoload :ValidatorLookup, File.dirname(__FILE__) + "/properties/validator_lookup.rb"
4
+ autoload :Property, File.dirname(__FILE__) + "/properties/property.rb"
5
+ autoload :Integer, File.dirname(__FILE__) + "/properties/integer.rb"
6
+ autoload :Float, File.dirname(__FILE__) + "/properties/float.rb"
7
+ autoload :NestedHash, File.dirname(__FILE__) + "/properties/nested_hash.rb"
8
+ autoload :Array, File.dirname(__FILE__) + "/properties/array.rb"
9
+ autoload :String, File.dirname(__FILE__) + "/properties/string.rb"
10
+ autoload :Boolean, File.dirname(__FILE__) + "/properties/boolean.rb"
11
+ autoload :Nil, File.dirname(__FILE__) + "/properties/nil.rb"
12
+ end
13
+ end
@@ -0,0 +1,81 @@
1
+ module DuckHunt
2
+ module Properties
3
+ class Array
4
+ # Used when an array is a property of a schema.
5
+ # Allows us to "nest" schemas (eg: an array in a array, an array of arrays)
6
+ attr_reader :required
7
+
8
+ def initialize(options= {}, &block)
9
+ raise ArgumentError, "a block must be given to define the array" unless block_given?
10
+ DuckHunt::HashHelpers.stringify_keys!(options)
11
+ options = {"required" => true}.merge(options)
12
+ @required = options["required"]
13
+ @required_but_not_present = false
14
+ @schema = DuckHunt::Schemas::ArraySchema.define options, &block
15
+ end
16
+
17
+ def required?
18
+ return self.required
19
+ end
20
+
21
+ def single_type_property
22
+ return @schema.single_type_property
23
+ end
24
+
25
+ def tuple_properties
26
+ return @schema.tuple_properties
27
+ end
28
+
29
+ def optional_tuple_properties
30
+ return @schema.optional_tuple_properties
31
+ end
32
+
33
+ def validates_uniqueness
34
+ return @schema.validates_uniqueness
35
+ end
36
+
37
+ def validates_uniqueness?
38
+ return @schema.validates_uniqueness?
39
+ end
40
+
41
+ def allow_nil
42
+ return @schema.allow_nil
43
+ end
44
+
45
+ def allow_nil?
46
+ return @schema.allow_nil?
47
+ end
48
+
49
+ def min_size
50
+ return @schema.min_size
51
+ end
52
+
53
+ def max_size
54
+ return @schema.max_size
55
+ end
56
+
57
+ def errors
58
+ return @schema.errors
59
+ end
60
+
61
+ def valid?(value)
62
+ @required_but_not_present = false
63
+ return @schema.validate?(value)
64
+ end
65
+
66
+ def add_required_error
67
+ @required_but_not_present = true
68
+ end
69
+
70
+ def errors
71
+ errors = @schema.errors
72
+ if @required_but_not_present
73
+ errors[:base] = [] if errors[:base].nil?
74
+ errors[:base] << DuckHunt::REQUIRED_MESSAGE
75
+ end
76
+
77
+ return errors
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,10 @@
1
+ module DuckHunt
2
+ module Properties
3
+ class Boolean < Property
4
+ def matches_type?(value)
5
+ return true if value.is_a? ::TrueClass or value.is_a? ::FalseClass
6
+ return false
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module DuckHunt
2
+ module Properties
3
+ class Float < Property
4
+ def matches_type?(value)
5
+ return true if value.is_a? ::Float or value.is_a? ::BigDecimal
6
+ return false
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module DuckHunt
2
+ module Properties
3
+ class Integer < Property
4
+ def matches_type?(value)
5
+ return value.integer? rescue return false
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,61 @@
1
+ module DuckHunt
2
+ module Properties
3
+ class NestedHash
4
+ # Used when a hash is a property of a schema.
5
+ # Allows us to "nest" schemas (eg: an array in a hash, an array of hashes)
6
+ attr_reader :required
7
+
8
+ def initialize(options= {}, &block)
9
+ raise ArgumentError, "a block must be given to define the hash" unless block_given?
10
+ DuckHunt::HashHelpers.stringify_keys!(options)
11
+ options = {"required" => true}.merge(options)
12
+ @required = options["required"]
13
+ @required_but_not_present = false
14
+ @schema = DuckHunt::Schemas::HashSchema.define options, &block
15
+ end
16
+
17
+ def required?
18
+ return self.required
19
+ end
20
+
21
+ def properties
22
+ return @schema.properties
23
+ end
24
+
25
+ def strict_mode
26
+ return @schema.strict_mode
27
+ end
28
+
29
+ def strict_mode?
30
+ return strict_mode
31
+ end
32
+
33
+ def allow_nil
34
+ return @schema.allow_nil
35
+ end
36
+
37
+ def allow_nil?
38
+ return @schema.allow_nil
39
+ end
40
+
41
+ def valid?(value)
42
+ @required_but_not_present = false
43
+ return @schema.validate?(value)
44
+ end
45
+
46
+ def add_required_error
47
+ @required_but_not_present = true
48
+ end
49
+
50
+ def errors
51
+ errors = @schema.errors
52
+ if @required_but_not_present
53
+ errors[:base] = [] if errors[:base].nil?
54
+ errors[:base] << DuckHunt::REQUIRED_MESSAGE
55
+ end
56
+
57
+ return errors
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,15 @@
1
+ module DuckHunt
2
+ module Properties
3
+ class Nil < Property
4
+ def initialize(options= {})
5
+ super(options)
6
+ @allow_nil = true
7
+ end
8
+
9
+ def matches_type?(value)
10
+ return true if value.nil?
11
+ return false
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,85 @@
1
+ module DuckHunt
2
+ module Properties
3
+ class Property
4
+ # the base class for the individual properties that make up a schema
5
+ # a property is valid if:
6
+ # 1. The value is required and has not been provided
7
+ # 2. The value provided `matches_type?`
8
+ # 3. Each of the attached validators returns `true` when `valid?` is called
9
+ include DuckHunt::Properties::ValidatorLookup
10
+ attr_reader :required, :allow_nil
11
+
12
+ def initialize(options= {})
13
+ DuckHunt::HashHelpers.stringify_keys!(options)
14
+ options = {"required" => true, "allow_nil" => false}.merge(options)
15
+ @required = options["required"]
16
+ @allow_nil = options["allow_nil"]
17
+ @validators = {}
18
+ @errors = []
19
+
20
+ options.delete("required")
21
+ options.delete("allow_nil")
22
+ options.each{ |key, value| find_and_create_validator(key, value) }
23
+ end
24
+
25
+ def required?
26
+ return self.required
27
+ end
28
+
29
+ def allow_nil?
30
+ return @allow_nil
31
+ end
32
+
33
+ def valid?(value)
34
+ @errors.clear
35
+ if value.nil?
36
+ return true if allow_nil?
37
+ add_error(NIL_OBJECT_NOT_ALLOWED_MESSAGE)
38
+ return false
39
+ end
40
+
41
+ if matches_type?(value)
42
+ check_validators(value)
43
+ else
44
+ add_type_mismatch_error(value)
45
+ end
46
+ return @errors.size == 0
47
+ end
48
+
49
+ def add_required_error
50
+ add_error(DuckHunt::REQUIRED_MESSAGE)
51
+ end
52
+
53
+ def errors
54
+ @errors.dup
55
+ end
56
+
57
+ protected
58
+
59
+ def add_type_mismatch_error(value)
60
+ add_error(DuckHunt::TYPE_MISMATCH_MESSAGE)
61
+ end
62
+
63
+ def check_validators(value)
64
+ @validators.values.each do |validator|
65
+ unless validator.valid?(value)
66
+ add_error(validator.error_message)
67
+ end
68
+ end
69
+ end
70
+
71
+ def add_error(error_message)
72
+ @errors << error_message unless @errors.include?(error_message)
73
+ end
74
+
75
+ def add_validator(validator_symbol, validator_constant, *args)
76
+ raise ValidatorAlreadyDefined, "`#{validator_symbol}` has already been defined in this schema" if @validators.has_key?(validator_symbol)
77
+ @validators[validator_symbol] = validator_constant.new(*args)
78
+ end
79
+
80
+ def matches_type?(value)
81
+ raise NotImplementedError
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,10 @@
1
+ module DuckHunt
2
+ module Properties
3
+ class String < Property
4
+ def matches_type?(value)
5
+ return true if value.is_a? ::String
6
+ return false
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,27 @@
1
+ module DuckHunt
2
+ module Properties
3
+ module ValidatorLookup
4
+ protected
5
+ def find_and_create_validator(validator_name, *args)
6
+ #convert the validator name into camel case and turn it into a symbol.
7
+ camelized_string = DuckHunt::StringHelpers.camelize(validator_name.to_s)
8
+ validator_symbol = camelized_string.to_sym
9
+
10
+ validator_constant = get_validator_constant(validator_symbol)
11
+ add_validator(validator_symbol, validator_constant, *args)
12
+ end
13
+
14
+ def validator_definition_exists?(validator)
15
+ DuckHunt::Validators.const_defined?(validator)
16
+ end
17
+
18
+ def get_validator_constant(validator)
19
+ DuckHunt::Validators.const_get(validator)
20
+ end
21
+
22
+ def add_validator(validator_symbol, validator_constant, *args)
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ module DuckHunt
2
+ module Schemas
3
+ autoload :SchemaDefinition, File.dirname(__FILE__) + "/schemas/schema_definition.rb"
4
+ autoload :PropertyLookup, File.dirname(__FILE__) + "/schemas/property_lookup.rb"
5
+ autoload :HashSchema, File.dirname(__FILE__) + "/schemas/hash_schema.rb"
6
+ autoload :ArraySchema, File.dirname(__FILE__) + "/schemas/array_schema.rb"
7
+ end
8
+ end
@@ -0,0 +1,254 @@
1
+ module DuckHunt
2
+ module Schemas
3
+ class ArraySchema
4
+ # There are two ways to define the schema of an array:
5
+ # * All items in the array have the same definition (integer, specific hash, etc.)
6
+ # * The order of the items in the array matters
7
+ # * each item in the array can have a different definition (e.g.: [1,"hello", { :hash=>"here" }])
8
+ # * The array definition can also include an optional set of items.
9
+ #
10
+ # These two types cannot be mixed
11
+ #
12
+ # The Array Schema also accepts some options during definition:
13
+ # * validates_uniqueness: if true, the array is invalid if it has duplicates (false by default)
14
+ # * min_size: the array must be at least this large (nil by default)
15
+ # * max_size: the array cannot be any larger (nil by default)
16
+ #
17
+ # Validation is performed in the following steps:
18
+ # 1. If the object is nil and nil objects are allowed, then return true
19
+ # 2. Check that the object is an array
20
+ # 3. If the `validates_uniqueness` flag is set, check that the array's entries are unique.
21
+ # 4. If the schema is a single-type:
22
+ # 1. Check that it falls within the specified min/max range
23
+ # 2. Check that every entry is valid according to the defined single type property
24
+ # 5. If the Schema is a tuple:
25
+ # 1. Check that the total number of entries is within the following range:
26
+ # (tuple_properties) <= (number of enties) <= (tuple_properties + optional_tuple_properties)
27
+ # 2. Check that each entry is valid according to the defined type in the tuple properties
28
+ include Schemas::SchemaDefinition
29
+ include Schemas::PropertyLookup
30
+
31
+ DUPLICATE_ITEMS_NOT_ALLOWED_MESSAGE = "duplicate items are not allowed"
32
+
33
+ def initialize(var={})
34
+ DuckHunt::HashHelpers.stringify_keys!(var)
35
+ options = {"validates_uniqueness" => false, "min_size" => nil, "max_size" => nil, "allow_nil" => false}.merge(var)
36
+ @single_type_property = nil
37
+ @tuple_properties = nil
38
+ @optional_tuple_properties = nil
39
+ @validates_uniqueness = options["validates_uniqueness"]
40
+ @min_size = options["min_size"]
41
+ @max_size = options["max_size"]
42
+ @allow_nil = options["allow_nil"]
43
+ @errors = {}
44
+ end
45
+
46
+ def single_type_property
47
+ @single_type_property
48
+ end
49
+
50
+ def tuple_properties
51
+ return nil if @tuple_properties.nil?
52
+ @tuple_properties.properties
53
+ end
54
+
55
+ def optional_tuple_properties
56
+ return nil if @optional_tuple_properties.nil?
57
+ @optional_tuple_properties.properties
58
+ end
59
+
60
+ def validates_uniqueness
61
+ return @validates_uniqueness
62
+ end
63
+
64
+ def validates_uniqueness?
65
+ return @validates_uniqueness
66
+ end
67
+
68
+ def allow_nil
69
+ return @allow_nil
70
+ end
71
+
72
+ def allow_nil?
73
+ return @allow_nil
74
+ end
75
+
76
+ def min_size
77
+ return @min_size
78
+ end
79
+
80
+ def max_size
81
+ return @max_size
82
+ end
83
+
84
+ def errors
85
+ return DuckHunt::HashHelpers.stringify_keys(@errors)
86
+ end
87
+
88
+ def items(&block)
89
+ raise InvalidSchema, "cannot mix single-type and tuple definitions" unless @single_type_property.nil?
90
+ @tuple_properties = TupleProperties.new(&block)
91
+ end
92
+
93
+ def optional_items(&block)
94
+ raise InvalidSchema, "cannot mix single-type and tuple definitions" unless @single_type_property.nil?
95
+ @optional_tuple_properties = TupleProperties.new(&block)
96
+ end
97
+
98
+ def validate?(object)
99
+ @errors.clear
100
+ if object.nil?
101
+ return true if allow_nil?
102
+ add_base_error_message(NIL_OBJECT_NOT_ALLOWED_MESSAGE)
103
+ return false
104
+ end
105
+
106
+ return false unless matches_type?(object)
107
+
108
+ if validates_uniqueness? and not has_unique_entries?(object)
109
+ add_base_error_message(DUPLICATE_ITEMS_NOT_ALLOWED_MESSAGE)
110
+ return false
111
+ end
112
+
113
+ return validate_single_type?(object) if single_type_schema?
114
+ return validate_tuple?(object) if tuple_schema?
115
+ end
116
+
117
+ protected
118
+
119
+ def add_property(property_constant, *args, &block)
120
+ raise InvalidSchema, "cannot mix single-type and tuple definitions" unless @tuple_properties.nil? and @optional_tuple_properties.nil?
121
+ raise InvalidSchema, "single type property has already been defined for this schema" unless @single_type_property.nil?
122
+ @single_type_property = property_constant.new(*args, &block)
123
+ end
124
+
125
+
126
+ def matches_type?(object_being_validated)
127
+ unless object_being_validated.is_a? Array
128
+ add_base_error_message(DuckHunt::TYPE_MISMATCH_MESSAGE)
129
+ return false
130
+ end
131
+ return true
132
+ end
133
+
134
+ def add_base_error_message(message)
135
+ @errors[:base] = [] if @errors[:base].nil?
136
+ @errors[:base] << message
137
+ end
138
+
139
+ def single_type_schema?
140
+ return !@single_type_property.nil?
141
+ end
142
+
143
+ def tuple_schema?
144
+ @tuple_properties.nil? == false or @optional_tuple_properties.nil? == false
145
+ end
146
+
147
+ def has_unique_entries?(object)
148
+ return object.uniq.size == object.size
149
+ end
150
+
151
+ def validate_single_type?(object)
152
+ object_size = object.size
153
+ object_valid = true
154
+
155
+ if !min_size.nil? and object_size < min_size
156
+ add_base_error_message(generate_under_minimum_message(min_size, object_size))
157
+ return false
158
+ end
159
+
160
+ if !max_size.nil? and object_size > max_size
161
+ add_base_error_message(generate_over_maximum_message(max_size, object_size))
162
+ return false
163
+ end
164
+
165
+ object.each_with_index{ |item, index|
166
+ unless single_type_property.valid?(item)
167
+ @errors[index.to_s] = single_type_property.errors
168
+ object_valid = false
169
+ end
170
+ }
171
+
172
+ return object_valid
173
+ end
174
+
175
+ def validate_tuple?(object)
176
+ object_size = object.size
177
+ object_valid = true
178
+
179
+ if object_size < minimum_tuple_size
180
+ add_base_error_message(generate_under_minimum_message(minimum_tuple_size, object_size))
181
+ return false
182
+ end
183
+
184
+ if object_size > maximum_tuple_size
185
+ add_base_error_message(generate_over_maximum_message(maximum_tuple_size, object_size))
186
+ return false
187
+ end
188
+
189
+ unless tuple_properties.nil?
190
+ # check that each required entry has the correct type in the tuple
191
+ tuple_properties.each_with_index{ |property, index|
192
+ unless property.valid?(object[index])
193
+ @errors[index.to_s] = property.errors
194
+ object_valid = false
195
+ end
196
+ }
197
+ end
198
+
199
+ if !optional_tuple_properties.nil? and object.size > 0
200
+ # check that each optional entry has the correct type in the tuple
201
+ optional_tuple_properties.each_with_index{ |property, index|
202
+ object_index = minimum_tuple_size + index
203
+ if object_index > (object.size - 1)
204
+ break #stop looking at optional properties if we've reached the end of the array
205
+ end
206
+ unless property.valid?(object[object_index])
207
+ @errors[object_index.to_s] = property.errors
208
+ object_valid = false
209
+ end
210
+ }
211
+ end
212
+
213
+ return object_valid
214
+ end
215
+
216
+ def minimum_tuple_size
217
+ return tuple_properties.nil? ? 0 : tuple_properties.size
218
+ end
219
+
220
+ def maximum_tuple_size
221
+ optional_tuple_size = optional_tuple_properties.nil? ? 0 : optional_tuple_properties.size
222
+ return minimum_tuple_size + optional_tuple_size
223
+ end
224
+
225
+ def generate_under_minimum_message(minimum_size, object_size)
226
+ return "expected at least #{minimum_size} item(s) but got #{object_size} item(s)"
227
+ end
228
+
229
+ def generate_over_maximum_message(maximum_size, object_size)
230
+ return "expected at most #{maximum_size} item(s) but got #{object_size} item(s)"
231
+ end
232
+
233
+ class TupleProperties
234
+ include Schemas::PropertyLookup
235
+
236
+ def initialize(*var)
237
+ raise ArgumentError, "a block of properties must be given to define the tuple items" unless block_given?
238
+ @properties = []
239
+ yield self
240
+ end
241
+
242
+ def properties
243
+ @properties.dup
244
+ end
245
+
246
+ protected
247
+
248
+ def add_property(property_constant, *args, &block)
249
+ @properties << property_constant.new(*args, &block)
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end