UNPKG

10 kBJavaScriptView Raw
1var AWS = require('./core');
2
3/**
4 * @api private
5 */
6AWS.ParamValidator = AWS.util.inherit({
7 /**
8 * Create a new validator object.
9 *
10 * @param validation [Boolean|map] whether input parameters should be
11 * validated against the operation description before sending the
12 * request. Pass a map to enable any of the following specific
13 * validation features:
14 *
15 * * **min** [Boolean] — Validates that a value meets the min
16 * constraint. This is enabled by default when paramValidation is set
17 * to `true`.
18 * * **max** [Boolean] — Validates that a value meets the max
19 * constraint.
20 * * **pattern** [Boolean] — Validates that a string value matches a
21 * regular expression.
22 * * **enum** [Boolean] — Validates that a string value matches one
23 * of the allowable enum values.
24 */
25 constructor: function ParamValidator(validation) {
26 if (validation === true || validation === undefined) {
27 validation = {'min': true};
28 }
29 this.validation = validation;
30 },
31
32 validate: function validate(shape, params, context) {
33 this.errors = [];
34 this.validateMember(shape, params || {}, context || 'params');
35
36 if (this.errors.length > 1) {
37 var msg = this.errors.join('\n* ');
38 msg = 'There were ' + this.errors.length +
39 ' validation errors:\n* ' + msg;
40 throw AWS.util.error(new Error(msg),
41 {code: 'MultipleValidationErrors', errors: this.errors});
42 } else if (this.errors.length === 1) {
43 throw this.errors[0];
44 } else {
45 return true;
46 }
47 },
48
49 fail: function fail(code, message) {
50 this.errors.push(AWS.util.error(new Error(message), {code: code}));
51 },
52
53 validateStructure: function validateStructure(shape, params, context) {
54 this.validateType(params, context, ['object'], 'structure');
55
56 var paramName;
57 for (var i = 0; shape.required && i < shape.required.length; i++) {
58 paramName = shape.required[i];
59 var value = params[paramName];
60 if (value === undefined || value === null) {
61 this.fail('MissingRequiredParameter',
62 'Missing required key \'' + paramName + '\' in ' + context);
63 }
64 }
65
66 // validate hash members
67 for (paramName in params) {
68 if (!Object.prototype.hasOwnProperty.call(params, paramName)) continue;
69
70 var paramValue = params[paramName],
71 memberShape = shape.members[paramName];
72
73 if (memberShape !== undefined) {
74 var memberContext = [context, paramName].join('.');
75 this.validateMember(memberShape, paramValue, memberContext);
76 } else {
77 this.fail('UnexpectedParameter',
78 'Unexpected key \'' + paramName + '\' found in ' + context);
79 }
80 }
81
82 return true;
83 },
84
85 validateMember: function validateMember(shape, param, context) {
86 switch (shape.type) {
87 case 'structure':
88 return this.validateStructure(shape, param, context);
89 case 'list':
90 return this.validateList(shape, param, context);
91 case 'map':
92 return this.validateMap(shape, param, context);
93 default:
94 return this.validateScalar(shape, param, context);
95 }
96 },
97
98 validateList: function validateList(shape, params, context) {
99 if (this.validateType(params, context, [Array])) {
100 this.validateRange(shape, params.length, context, 'list member count');
101 // validate array members
102 for (var i = 0; i < params.length; i++) {
103 this.validateMember(shape.member, params[i], context + '[' + i + ']');
104 }
105 }
106 },
107
108 validateMap: function validateMap(shape, params, context) {
109 if (this.validateType(params, context, ['object'], 'map')) {
110 // Build up a count of map members to validate range traits.
111 var mapCount = 0;
112 for (var param in params) {
113 if (!Object.prototype.hasOwnProperty.call(params, param)) continue;
114 // Validate any map key trait constraints
115 this.validateMember(shape.key, param,
116 context + '[key=\'' + param + '\']');
117 this.validateMember(shape.value, params[param],
118 context + '[\'' + param + '\']');
119 mapCount++;
120 }
121 this.validateRange(shape, mapCount, context, 'map member count');
122 }
123 },
124
125 validateScalar: function validateScalar(shape, value, context) {
126 switch (shape.type) {
127 case null:
128 case undefined:
129 case 'string':
130 return this.validateString(shape, value, context);
131 case 'base64':
132 case 'binary':
133 return this.validatePayload(value, context);
134 case 'integer':
135 case 'float':
136 return this.validateNumber(shape, value, context);
137 case 'boolean':
138 return this.validateType(value, context, ['boolean']);
139 case 'timestamp':
140 return this.validateType(value, context, [Date,
141 /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/, 'number'],
142 'Date object, ISO-8601 string, or a UNIX timestamp');
143 default:
144 return this.fail('UnkownType', 'Unhandled type ' +
145 shape.type + ' for ' + context);
146 }
147 },
148
149 validateString: function validateString(shape, value, context) {
150 var validTypes = ['string'];
151 if (shape.isJsonValue) {
152 validTypes = validTypes.concat(['number', 'object', 'boolean']);
153 }
154 if (value !== null && this.validateType(value, context, validTypes)) {
155 this.validateEnum(shape, value, context);
156 this.validateRange(shape, value.length, context, 'string length');
157 this.validatePattern(shape, value, context);
158 this.validateUri(shape, value, context);
159 }
160 },
161
162 validateUri: function validateUri(shape, value, context) {
163 if (shape['location'] === 'uri') {
164 if (value.length === 0) {
165 this.fail('UriParameterError', 'Expected uri parameter to have length >= 1,'
166 + ' but found "' + value +'" for ' + context);
167 }
168 }
169 },
170
171 validatePattern: function validatePattern(shape, value, context) {
172 if (this.validation['pattern'] && shape['pattern'] !== undefined) {
173 if (!(new RegExp(shape['pattern'])).test(value)) {
174 this.fail('PatternMatchError', 'Provided value "' + value + '" '
175 + 'does not match regex pattern /' + shape['pattern'] + '/ for '
176 + context);
177 }
178 }
179 },
180
181 validateRange: function validateRange(shape, value, context, descriptor) {
182 if (this.validation['min']) {
183 if (shape['min'] !== undefined && value < shape['min']) {
184 this.fail('MinRangeError', 'Expected ' + descriptor + ' >= '
185 + shape['min'] + ', but found ' + value + ' for ' + context);
186 }
187 }
188 if (this.validation['max']) {
189 if (shape['max'] !== undefined && value > shape['max']) {
190 this.fail('MaxRangeError', 'Expected ' + descriptor + ' <= '
191 + shape['max'] + ', but found ' + value + ' for ' + context);
192 }
193 }
194 },
195
196 validateEnum: function validateRange(shape, value, context) {
197 if (this.validation['enum'] && shape['enum'] !== undefined) {
198 // Fail if the string value is not present in the enum list
199 if (shape['enum'].indexOf(value) === -1) {
200 this.fail('EnumError', 'Found string value of ' + value + ', but '
201 + 'expected ' + shape['enum'].join('|') + ' for ' + context);
202 }
203 }
204 },
205
206 validateType: function validateType(value, context, acceptedTypes, type) {
207 // We will not log an error for null or undefined, but we will return
208 // false so that callers know that the expected type was not strictly met.
209 if (value === null || value === undefined) return false;
210
211 var foundInvalidType = false;
212 for (var i = 0; i < acceptedTypes.length; i++) {
213 if (typeof acceptedTypes[i] === 'string') {
214 if (typeof value === acceptedTypes[i]) return true;
215 } else if (acceptedTypes[i] instanceof RegExp) {
216 if ((value || '').toString().match(acceptedTypes[i])) return true;
217 } else {
218 if (value instanceof acceptedTypes[i]) return true;
219 if (AWS.util.isType(value, acceptedTypes[i])) return true;
220 if (!type && !foundInvalidType) acceptedTypes = acceptedTypes.slice();
221 acceptedTypes[i] = AWS.util.typeName(acceptedTypes[i]);
222 }
223 foundInvalidType = true;
224 }
225
226 var acceptedType = type;
227 if (!acceptedType) {
228 acceptedType = acceptedTypes.join(', ').replace(/,([^,]+)$/, ', or$1');
229 }
230
231 var vowel = acceptedType.match(/^[aeiou]/i) ? 'n' : '';
232 this.fail('InvalidParameterType', 'Expected ' + context + ' to be a' +
233 vowel + ' ' + acceptedType);
234 return false;
235 },
236
237 validateNumber: function validateNumber(shape, value, context) {
238 if (value === null || value === undefined) return;
239 if (typeof value === 'string') {
240 var castedValue = parseFloat(value);
241 if (castedValue.toString() === value) value = castedValue;
242 }
243 if (this.validateType(value, context, ['number'])) {
244 this.validateRange(shape, value, context, 'numeric value');
245 }
246 },
247
248 validatePayload: function validatePayload(value, context) {
249 if (value === null || value === undefined) return;
250 if (typeof value === 'string') return;
251 if (value && typeof value.byteLength === 'number') return; // typed arrays
252 if (AWS.util.isNode()) { // special check for buffer/stream in Node.js
253 var Stream = AWS.util.stream.Stream;
254 if (AWS.util.Buffer.isBuffer(value) || value instanceof Stream) return;
255 } else {
256 if (typeof Blob !== void 0 && value instanceof Blob) return;
257 }
258
259 var types = ['Buffer', 'Stream', 'File', 'Blob', 'ArrayBuffer', 'DataView'];
260 if (value) {
261 for (var i = 0; i < types.length; i++) {
262 if (AWS.util.isType(value, types[i])) return;
263 if (AWS.util.typeName(value.constructor) === types[i]) return;
264 }
265 }
266
267 this.fail('InvalidParameterType', 'Expected ' + context + ' to be a ' +
268 'string, Buffer, Stream, Blob, or typed array object');
269 }
270});