from rest_framework.exceptions import APIException from datetime import datetime __all__ = ['validate'] _types_names = { bool: 'boolean', int: 'integer', float: 'float', str: 'string', dict: 'object', list: 'array', datetime: 'datetime', } class ValidationException(APIException): status_code = 400 default_detail = 'Bad request!' def validate(value, expected, path='input'): # Is the expected type respected? if 'type' in expected: expected_type = expected['type'] if not isinstance(value, expected_type): if expected_type in (bool, int, float, str, datetime, ): try: if expected_type == bool: value = value not in {0, 0.0, '', '0', 'false'} elif expected_type == datetime: value = value + '2000-01-01T00:00:00Z'[len(value):] value = datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') else: value = expected_type(value) except ValueError: raise ValidationException('%s should be a JSON %s, but could not be parsed as such' % (path, _types_names[expected_type], )) else: raise ValidationException('%s should be a JSON %s' % (path, _types_names[expected_type], )) else: expected_type = type(value) # Is the value in the expected range? if 'range' in expected: expected_range = expected['range'] if isinstance(expected_range, tuple): if expected_type in (int, float): tested_value = value tested_name = 'value' elif expected_type in (str, list): tested_value = len(value) tested_name = 'length' if tested_value < expected_range[0]: raise ValidationException('%s should have a minimum %s of %d' % (path, tested_name, expected_range[0], )) if len(expected_range) > 1 and tested_value > expected_range[1]: raise ValidationException('%s should have a maximum %s of %d' % (path, tested_name, expected_range[1], )) elif isinstance(expected_range, (list, set, )) and value not in expected_range: expected_values = expected_range if isinstance(expected_range, list) else expected_range expected_values = [str(value) for value in expected_values if isinstance(value, expected_type)] if len(expected_values) < 16: expected_values_str = '", "'.join(expected_values) expected_values_str = '"' + expected_values_str + '"' else: expected_values_str = '", "'.join(expected_values[:16]) expected_values_str = '"' + expected_values_str + '"...' raise ValidationException('%s should take one of the following values: %s' % (path, expected_values_str, )) # Do we have to translate through a dictionary? if 'translate' in expected: translate = expected['translate'] if callable(translate): value = translate(value) if value is None and expected.get('required', False): raise ValidationException('%s has been given an invalid value' % (path, )) return value try: value = expected['translate'][value] except KeyError: if expected.get('translate_fallback_keep', False): return value if expected.get('required', False): raise ValidationException('%s has been given an invalid value' % (path, )) else: return expected.get('default', value) # Are we handling an iterable? if expected_type in (list, dict): if 'items' in expected: expected_items = expected['items'] if expected_type == list: for i, element in enumerate(value): value[i] = validate(element, expected_items, '%s[%d]' % (path, i, )) elif expected_type == dict: if expected_items: for key in value: if key not in expected_items: raise ValidationException('%s should not have a "%s" key.' % (path, key, )) for expected_key, expected_value in expected_items.items(): if expected_key in value: value[expected_key] = validate(value[expected_key], expected_value, '%s["%s"]' % (path, expected_key, )) elif 'required' in expected_value and expected_value['required']: raise ValidationException('%s should have a "%s" key.' % (path, expected_key, )) elif 'default' in expected_value: value[expected_key] = expected_value['default'] # Let's return the proper value! return value