param_validator.js 9.9 KB

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