"""Validators for the models.""" import copy import jsonschema from oto import response from marketing.constants import error as error_constants def cast(value, basetype, default_value=None): """Cast a value into a specific basetype. Args: value (mixed): the value to cast. basetype (mixed): the basetype for the value to be cast in. default_value (mixed): in case an exception is thrown while casting, return a default value. Returns: mixed: the casted value or the default value. """ try: return basetype(value) except Exception: return default_value def autocast(value, basetype, strip=True): """Automatically cast value from validator's schema. Args: value (mixed): the value to cast. basetype (str): the jsonschema basetype. strip (bool): automatically strip spaces from the strings. Returns: mixed: the automatically casted values. """ if basetype == 'string': value = cast(value or '', str, default_value='') if strip: return value.strip() return value elif basetype == 'number': if not isinstance(value, str): return cast(value, int, default_value=value) if value.isdigit(): return cast(value, int, default_value=value) return cast(value, float, default_value=value) # Excepted: if a basetype is not supported, it should be implemented. raise Exception('This basetype {} is not supported'.format(basetype)) def pluck_values(properties, data, excluded_fields=None, cast=True): """Pluck values from the data that belongs to the model. Args: properties (dict): the data validator's properties. data (dict): data that contains the values to validate. excluded_fields (list): optional list of fields to exclude. cast (bool): optional flag to trigger (or disable) casting while plucking values. Returns: dict: the clean dataset. """ excluded_fields = excluded_fields or [] data = data or {} values = {} for property_name, property_definition in properties.items(): if property_name in excluded_fields: continue basetype = property_definition.get('type') if property_name not in data: continue value = data.get(property_name) if cast: value = autocast(value, basetype) values.update({property_name: value}) return values def validate( validator, data, required_fields=None, excluded_fields=None, cast=True, new_fields=None): """Validate an incoming dataset. Args: validator (Draft3Validator): the base of the schema. data (dict): the dataset to validate. required_fields (list): optional list of required fields. excluded_fields (list): optional list of fields to exclude. cast (bool): optional flag to autocast field values. new_fields (list): additional new fields. Returns: Response: with the result of the operation and a clean dataset. """ validator = extend_validator( validator, required_fields=required_fields, new_fields=new_fields) data = pluck_values( validator.schema['properties'], data, excluded_fields=excluded_fields, cast=cast) errors = {} for error in validator.iter_errors(data): field_name = '.'.join(error.relative_path) errors.update({field_name: error.message}) if errors: return response.create_error_response( code=error_constants.ERROR_CODE_VALIDATION, message=errors) return response.Response(data) def extend_validator(validator, required_fields=None, new_fields=None): """Extend a Draft3Validator. Args: validator (Draft3Validator): the draft validator. required_fields (list): the list of required fields. new_fields (list): additional list of fields Returns: Draft3Validator: the new validator. """ required_fields = required_fields or [] if not required_fields and not new_fields: return validator schema = copy.deepcopy(validator.schema) if new_fields: new_fields = copy.deepcopy(new_fields) schema['properties'].update(new_fields) for field in required_fields: schema['properties'][field].update(required=True) return jsonschema.Draft3Validator(schema)