param_validator.js
  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 if (paramValue !== undefined && paramValue !== null) {
 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  });