/
validation.rb
234 lines (203 loc) · 8.17 KB
/
validation.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
require 'yaml'
module FakeDynamo
module Validation
def validate!(&block)
@api_errors = []
yield
unless @api_errors.empty?
plural = @api_errors.size == 1 ? '' : 's'
message = "#{@api_errors.size} error#{plural} detected: #{@api_errors.join('; ')}"
raise ValidationException, message
end
end
def add_errors(message)
@api_errors << message
end
def validate_payload(operation, data)
validate! do
validate_request_size(data)
validate_operation(operation)
validate_input(operation, data)
end
end
def validate_operation(operation)
raise UnknownOperationException, "Unknown operation: #{operation}" unless available_operations.include? operation
end
def validate_input(operation, data)
api_input_spec(operation).each do |attribute, spec|
validate_spec(attribute, data[attribute], spec, [])
end
end
def validate_spec(attribute, data, spec, parents)
if not data
if spec.include?(:required)
add_errors("value null at '#{param(attribute, parents)}' failed to satisfy the constraint: Member must not be null")
end
return
end
spec.each do |constrain|
case constrain
when :string
add_errors("The parameter '#{param(attribute, parents)}' must be a string") unless data.kind_of? String
when :blob
add_errors("The parameter '#{param(attribute, parents)}' must be a binary") unless data.kind_of? String
when :long
add_errors("The parameter '#{param(attribute, parents)}' must be a long") unless data.kind_of? Integer
when :integer
add_errors("The parameter '#{param(attribute, parents)}' must be a integer") unless data.kind_of? Integer
when :boolean
add_errors("The parameter '#{param(attribute, parents)}' must be a boolean") unless (data.kind_of? TrueClass or data.kind_of? FalseClass)
when Hash
new_parents = parents + [attribute]
case constrain.keys.first
when :pattern
pattern = constrain[:pattern]
unless data =~ pattern
add_errors("The parameter '#{param(attribute, parents)}' should match the pattern #{pattern}")
end
when :within
range = constrain[:within]
unless range.include?(Numeric === data ? data : data.size)
add_errors("The parameter '#{param(attribute, parents)}' value '#{data}' should be within #{range}")
end
when :enum
enum = constrain[:enum]
unless enum.include? data
add_errors("Value '#{data}' at '#{param(attribute, parents)}' failed to satisfy the constraint: Member must satisfy enum values set: #{enum}")
end
when :structure
structure = constrain[:structure]
structure.each do |attribute, spec|
validate_spec(attribute, data[attribute], spec, parents + ["member"])
end
when :map
map = constrain[:map]
raise ValidationException, "#{param(attribute, parents)} must be a Hash" unless data.kind_of? Hash
data.each do |key, value|
validate_spec(key, key, map[:key], new_parents)
validate_spec(key, value, map[:value], new_parents)
end
when :list
raise ValidationException, "#{param(attribute, parents)} must be a Array" unless data.kind_of? Array
data.each_with_index do |element, i|
validate_spec(element, element, constrain[:list], new_parents + [(i+1).to_s])
end
else
raise "Unhandled constraint #{constrain}"
end
when :required
# handled earlier
else
raise "Unhandled constraint #{constrain}"
end
end
end
def param(attribute, parents)
(parents + [attribute]).join('.')
end
def api_input_spec(operation)
api_config[:operations].find { |spec| spec[:name] == operation }[:inputs]
end
def available_operations
@available_operations ||= api_config[:operations].map { |spec| spec[:name] }
end
def api_config
@api_config ||= YAML.load_file(api_config_path)
end
def api_config_path
File.join File.expand_path(File.dirname(__FILE__)), 'api.yml'
end
def validate_type(value, attribute)
if attribute.kind_of?(Attribute)
expected_type = value.keys.first
if expected_type != attribute.type
raise ValidationException, "Type mismatch for key #{attribute.name}"
end
else
raise 'Unknown attribute'
end
end
def validate_key_schema(data, key_schema)
key = data[key_schema.hash_key.name] or raise ValidationException, "Missing the key #{key_schema.hash_key.name} in the item"
validate_type(key, key_schema.hash_key)
if key_schema.range_key
range_key = data[key_schema.range_key.name] or raise ValidationException, "Missing the key #{key_schema.range_key.name} in the item"
validate_type(range_key, key_schema.range_key)
end
end
def validate_key_data(data, key_schema)
hash_key = data[key_schema.hash_key.name] or key_schema_mismatch
validate_type(hash_key, key_schema.hash_key)
if key_schema.range_key
range_key = data[key_schema.range_key.name] or key_schema_mismatch
validate_type(range_key, key_schema.range_key)
key_schema_mismatch if data.size != 2
else
key_schema_mismatch if data.size != 1
end
end
def key_schema_mismatch
raise ValidationException, "The provided key element does not match the schema"
end
def validate_request_size(data)
if data.to_s.bytesize > 1 * 1024 * 1024
raise ValidationException, "Request size can't exceed 1 mb"
end
end
def validate_range_key(key_schema)
unless key_schema.range_key
raise ValidationException, 'Table KeySchema does not have a range key'
end
end
def validate_hash_key(index, table)
if index.hash_key != table.hash_key
raise ValidationException, "Index KeySchema does not have the same leading hash key as table KeySchema for index"
end
end
def validate_projection(projection)
if projection.type == 'INCLUDE'
unless projection.non_key_attributes
raise ValidationException, "ProjectionType is #{projection.type}, but NonKeyAttributes is not specified"
end
else
if projection.non_key_attributes
raise ValidationException, "ProjectionType is #{projection.type}, but NonKeyAttributes is specified"
end
end
end
def validate_index_names(indexes)
names = indexes.map(&:name)
if names.uniq.size != names.size
raise ValidationException, "Duplicate index name: #{names.find { |n| names.count(n) > 1 }}"
end
end
def validate_hash_condition(condition, schema)
unless condition
raise ValidationException, "Query condition missed key schema element #{schema.hash_key.name}"
end
if condition['ComparisonOperator'] != 'EQ'
raise ValidationException, "Query key condition not supported"
end
end
def validate_range_condition(condition, schema)
unless condition.key?(schema.range_key.name)
raise ValidationException, "Query condition missed key schema element #{schema.range_key.name}"
end
end
def validate_conditions(conditions)
if conditions.any? { |_, v| !v['AttributeValueList'] }
raise ValidationException, "One or more parameter values were invalid: Invalid number of argument(s) for the ComparisonOperator"
end
end
def validate_table_update(data)
if !data['GlobalSecondaryIndexUpdates'] && !data['ProvisionedThroughput']
raise ValidationException, "At least one of ProvisionedThroughput or GlobalSecondaryIndexUpdates is required"
end
end
def validate_consistent_read_and_global_secondary_index(data, index)
if data['ConsistentRead'] && index.kind_of?(GlobalSecondaryIndex)
raise ValidationException, "Consistent reads are not supported on global indexes"
end
end
end
end