validate.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. # Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  2. #
  3. # Modifications made by Cloudera are:
  4. # Copyright (c) 2016 Cloudera, Inc. All rights reserved.
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License"). You
  7. # may not use this file except in compliance with the License. A copy of
  8. # the License is located at
  9. #
  10. # http://aws.amazon.com/apache2.0/
  11. #
  12. # or in the "license" file accompanying this file. This file is
  13. # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
  14. # ANY KIND, either express or implied. See the License for the specific
  15. # language governing permissions and limitations under the License.
  16. import datetime
  17. import decimal
  18. from altuscli.compat import six
  19. from altuscli.exceptions import ParamValidationError
  20. from altuscli.utils import parse_to_aware_datetime
  21. def validate_parameters(params, shape):
  22. """Validates input parameters against a schema.
  23. This is a convenience function that validates parameters against a schema.
  24. You can also instantiate and use the ParamValidator class directly if you
  25. want more control.
  26. If there are any validation errors then a ParamValidationError
  27. will be raised. If there are no validation errors than no exception
  28. is raised and a value of None is returned.
  29. """
  30. validator = ParamValidator()
  31. report = validator.validate(params, shape)
  32. if report.has_errors():
  33. raise ParamValidationError(report=report.generate_report())
  34. def type_check(valid_types):
  35. def _create_type_check_guard(func):
  36. def _on_passes_type_check(self, param, shape, errors, name):
  37. if _type_check(param, errors, name):
  38. return func(self, param, shape, errors, name)
  39. def _type_check(param, errors, name):
  40. if not isinstance(param, valid_types):
  41. valid_type_names = [six.text_type(t) for t in valid_types]
  42. errors.report(name, 'invalid type', param=param,
  43. valid_types=valid_type_names)
  44. return False
  45. return True
  46. return _on_passes_type_check
  47. return _create_type_check_guard
  48. def range_check(name, value, shape, error_type, errors):
  49. failed = False
  50. min_allowed = float('-inf')
  51. max_allowed = float('inf')
  52. if shape.minimum is not None:
  53. min_allowed = shape.minimum
  54. if value < min_allowed:
  55. failed = True
  56. if shape.maximum is not None:
  57. max_allowed = shape.maximum
  58. if value > max_allowed:
  59. failed = True
  60. if failed:
  61. errors.report(name, error_type, param=value,
  62. valid_range=[min_allowed, max_allowed])
  63. def length_check(name, value, shape, error_type, errors):
  64. failed = False
  65. min_allowed = float('-inf')
  66. max_allowed = float('inf')
  67. if shape.min_length is not None:
  68. min_allowed = shape.min_length
  69. if value < min_allowed:
  70. failed = True
  71. if shape.max_length is not None:
  72. max_allowed = shape.max_length
  73. if value > max_allowed:
  74. failed = True
  75. if failed:
  76. errors.report(name, error_type, param=value,
  77. valid_range=[min_allowed, max_allowed])
  78. class ValidationErrors(object):
  79. def __init__(self):
  80. self._errors = []
  81. def has_errors(self):
  82. if self._errors:
  83. return True
  84. return False
  85. def generate_report(self):
  86. error_messages = []
  87. for error in self._errors:
  88. error_messages.append(self._format_error(error))
  89. return '\n'.join(error_messages)
  90. def _format_error(self, error):
  91. error_type, name, additional = error
  92. name = self._get_name(name)
  93. if error_type == 'missing required field':
  94. return 'Missing required parameter in %s: "%s"' % (
  95. name, additional['required_name'])
  96. elif error_type == 'unknown field':
  97. return 'Unknown parameter in %s: "%s", must be one of: %s' % (
  98. name, additional['unknown_param'], ', '.join(additional['valid_names']))
  99. elif error_type == 'invalid type':
  100. return 'Invalid type for parameter %s, value: %s, type: %s, valid types: %s' \
  101. % (name, additional['param'],
  102. str(type(additional['param'])),
  103. ', '.join(additional['valid_types']))
  104. elif error_type == 'invalid enum':
  105. return ('Invalid value for parameter %s, value: %s, type: %s, valid '
  106. 'values: %s') \
  107. % (name, additional['param'],
  108. str(type(additional['param'])),
  109. ', '.join(additional['valid_values']))
  110. elif error_type == 'invalid range':
  111. min_allowed = additional['valid_range'][0]
  112. max_allowed = additional['valid_range'][1]
  113. return ('Invalid range for parameter %s, value: %s, valid range: '
  114. '%s-%s' % (name, additional['param'],
  115. min_allowed, max_allowed))
  116. elif error_type == 'invalid length':
  117. min_allowed = additional['valid_range'][0]
  118. max_allowed = additional['valid_range'][1]
  119. return ('Invalid length for parameter %s, value: %s, valid range: '
  120. '%s-%s' % (name, additional['param'],
  121. min_allowed, max_allowed))
  122. def _get_name(self, name):
  123. if not name:
  124. return 'input'
  125. elif name.startswith('.'):
  126. return name[1:]
  127. else:
  128. return name
  129. def report(self, name, reason, **kwargs):
  130. self._errors.append((reason, name, kwargs))
  131. class ParamValidator(object):
  132. def validate(self, params, shape):
  133. errors = ValidationErrors()
  134. self._validate(params, shape, errors, name='')
  135. return errors
  136. def _validate(self, params, shape, errors, name):
  137. getattr(self, '_validate_%s' % shape.type_name)(params, shape, errors, name)
  138. @type_check(valid_types=(dict,))
  139. def _validate_object(self, params, shape, errors, name):
  140. # Validate required fields.
  141. for required_member in shape.required_members:
  142. if required_member not in params:
  143. errors.report(name, 'missing required field',
  144. required_name=required_member, user_params=params)
  145. members = shape.members
  146. known_params = []
  147. # Validate known params.
  148. for param in params:
  149. if param not in members:
  150. errors.report(name, 'unknown field', unknown_param=param,
  151. valid_names=list(members))
  152. else:
  153. known_params.append(param)
  154. # Validate structure members.
  155. for param in known_params:
  156. self._validate(params[param], shape.members[param],
  157. errors, '%s.%s' % (name, param))
  158. @type_check(valid_types=(bool,))
  159. def _validate_boolean(self, param, shape, errors, name):
  160. pass
  161. @type_check(valid_types=six.integer_types)
  162. def _validate_integer(self, param, shape, errors, name):
  163. range_check(name, param, shape, 'invalid range', errors)
  164. @type_check(valid_types=(float, decimal.Decimal) + six.integer_types)
  165. def _validate_number(self, param, shape, errors, name):
  166. range_check(name, param, shape, 'invalid range', errors)
  167. @type_check(valid_types=six.string_types)
  168. def _validate_string(self, param, shape, errors, name):
  169. if len(shape.enum) > 0 and param not in shape.enum:
  170. errors.report(name, 'invalid enum', param=param,
  171. valid_values=shape.enum)
  172. length_check(name, len(param), shape, 'invalid length', errors)
  173. @type_check(valid_types=(list, tuple))
  174. def _validate_array(self, param, shape, errors, name):
  175. member_shape = shape.member
  176. length_check(name, len(param), shape, 'invalid length', errors)
  177. for i, item in enumerate(param):
  178. self._validate(item, member_shape, errors, '%s[%s]' % (name, i))
  179. def _validate_datetime(self, param, shape, errors, name):
  180. # We don't use @type_check because datetimes are a bit more flexible.
  181. # You can either provide a datetime object, or a string that parses
  182. # to a datetime.
  183. is_valid_type = self._type_check_datetime(param)
  184. if not is_valid_type:
  185. valid_type_names = [six.text_type(datetime), 'timestamp-string']
  186. errors.report(name, 'invalid type', param=param,
  187. valid_types=valid_type_names)
  188. def _type_check_datetime(self, value):
  189. try:
  190. parse_to_aware_datetime(value)
  191. return True
  192. except (TypeError, ValueError, AttributeError):
  193. # Yes, dateutil can sometimes raise an AttributeError when parsing
  194. # timestamps.
  195. return False
  196. class ParamValidationDecorator(object):
  197. def __init__(self, param_validator, serializer):
  198. self._param_validator = param_validator
  199. self._serializer = serializer
  200. def serialize_to_request(self, parameters, operation_model):
  201. input_shape = operation_model.input_shape
  202. if input_shape is not None:
  203. report = self._param_validator.validate(parameters,
  204. operation_model.input_shape)
  205. if report.has_errors():
  206. raise ParamValidationError(report=report.generate_report())
  207. return self._serializer.serialize_to_request(parameters, operation_model)