param_validator.js (9429B)
1 var AWS = require('./core'); 2 3 /** 4 * @api private 5 */ 6 AWS.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 if (this.validateType(value, context, ['string'])) { 151 this.validateEnum(shape, value, context); 152 this.validateRange(shape, value.length, context, 'string length'); 153 this.validatePattern(shape, value, context); 154 } 155 }, 156 157 validatePattern: function validatePattern(shape, value, context) { 158 if (this.validation['pattern'] && shape['pattern'] !== undefined) { 159 if (!(new RegExp(shape['pattern'])).test(value)) { 160 this.fail('PatternMatchError', 'Provided value "' + value + '" ' 161 + 'does not match regex pattern /' + shape['pattern'] + '/ for ' 162 + context); 163 } 164 } 165 }, 166 167 validateRange: function validateRange(shape, value, context, descriptor) { 168 if (this.validation['min']) { 169 if (shape['min'] !== undefined && value < shape['min']) { 170 this.fail('MinRangeError', 'Expected ' + descriptor + ' >= ' 171 + shape['min'] + ', but found ' + value + ' for ' + context); 172 } 173 } 174 if (this.validation['max']) { 175 if (shape['max'] !== undefined && value > shape['max']) { 176 this.fail('MaxRangeError', 'Expected ' + descriptor + ' <= ' 177 + shape['max'] + ', but found ' + value + ' for ' + context); 178 } 179 } 180 }, 181 182 validateEnum: function validateRange(shape, value, context) { 183 if (this.validation['enum'] && shape['enum'] !== undefined) { 184 // Fail if the string value is not present in the enum list 185 if (shape['enum'].indexOf(value) === -1) { 186 this.fail('EnumError', 'Found string value of ' + value + ', but ' 187 + 'expected ' + shape['enum'].join('|') + ' for ' + context); 188 } 189 } 190 }, 191 192 validateType: function validateType(value, context, acceptedTypes, type) { 193 // We will not log an error for null or undefined, but we will return 194 // false so that callers know that the expected type was not strictly met. 195 if (value === null || value === undefined) return false; 196 197 var foundInvalidType = false; 198 for (var i = 0; i < acceptedTypes.length; i++) { 199 if (typeof acceptedTypes[i] === 'string') { 200 if (typeof value === acceptedTypes[i]) return true; 201 } else if (acceptedTypes[i] instanceof RegExp) { 202 if ((value || '').toString().match(acceptedTypes[i])) return true; 203 } else { 204 if (value instanceof acceptedTypes[i]) return true; 205 if (AWS.util.isType(value, acceptedTypes[i])) return true; 206 if (!type && !foundInvalidType) acceptedTypes = acceptedTypes.slice(); 207 acceptedTypes[i] = AWS.util.typeName(acceptedTypes[i]); 208 } 209 foundInvalidType = true; 210 } 211 212 var acceptedType = type; 213 if (!acceptedType) { 214 acceptedType = acceptedTypes.join(', ').replace(/,([^,]+)$/, ', or$1'); 215 } 216 217 var vowel = acceptedType.match(/^[aeiou]/i) ? 'n' : ''; 218 this.fail('InvalidParameterType', 'Expected ' + context + ' to be a' + 219 vowel + ' ' + acceptedType); 220 return false; 221 }, 222 223 validateNumber: function validateNumber(shape, value, context) { 224 if (value === null || value === undefined) return; 225 if (typeof value === 'string') { 226 var castedValue = parseFloat(value); 227 if (castedValue.toString() === value) value = castedValue; 228 } 229 if (this.validateType(value, context, ['number'])) { 230 this.validateRange(shape, value, context, 'numeric value'); 231 } 232 }, 233 234 validatePayload: function validatePayload(value, context) { 235 if (value === null || value === undefined) return; 236 if (typeof value === 'string') return; 237 if (value && typeof value.byteLength === 'number') return; // typed arrays 238 if (AWS.util.isNode()) { // special check for buffer/stream in Node.js 239 var Stream = AWS.util.stream.Stream; 240 if (AWS.util.Buffer.isBuffer(value) || value instanceof Stream) return; 241 } 242 243 var types = ['Buffer', 'Stream', 'File', 'Blob', 'ArrayBuffer', 'DataView']; 244 if (value) { 245 for (var i = 0; i < types.length; i++) { 246 if (AWS.util.isType(value, types[i])) return; 247 if (AWS.util.typeName(value.constructor) === types[i]) return; 248 } 249 } 250 251 this.fail('InvalidParameterType', 'Expected ' + context + ' to be a ' + 252 'string, Buffer, Stream, Blob, or typed array object'); 253 } 254 });