def init_default_value(self, v, rule, path): """ """ log.debug(u"Init default value : %s", path) self.default = v if is_collection_type(self.type): raise RuleError( msg=u"Value: {0} for keyword 'default' is not a scalar type". format(v), error_key=u"default.not_scalar", path=path, ) if self.type == "map" or self.type == "seq": raise RuleError( msg=u"Value: {0} for keyword 'default' is not a scalar type". format(v), error_key=u"default.not_scalar", path=path, ) if not isinstance(v, self.type_class): raise RuleError( msg=u"Types do not match: '{0}' --> '{1}'".format( v, self.type_class), error_key=u"default.type.unmatch", path=path, )
def init_unique_value(self, v, rule, path): """ """ log.debug(u"Init unique value : %s", path) if not isinstance(v, bool): raise RuleError( msg=u"Value: '{0}' for 'unique' keyword is not boolean".format( v), error_key=u"unique.not_bool", path=path, ) self.unique = v if is_collection_type(self.type): raise RuleError( msg= u"Type of the value: '{0}' for 'unique' keyword is not a scalar type" .format(self.type), error_key=u"unique.not_scalar", path=path, ) if path == "": raise RuleError( msg=u"Keyword 'unique' can't be on root level of schema", error_key=u"unique.not_on_root_level", path=path, )
def init_pattern_value(self, v, rule, path): """ """ log.debug(u"Init pattern value : %s", path) if not isinstance(v, str): raise RuleError( msg=u"Value of pattern keyword: '{0}' is not a string".format( v), error_key=u"pattern.not_string", path=path, ) self.pattern = v if self.schema_str["type"] == "map": raise RuleError( msg=u"Keyword pattern is not allowed inside map", error_key=u"pattern.not_allowed_in_map", path=path, ) # TODO: Some form of validation of the regexp? it exists in the source try: self.pattern_regexp = re.compile(self.pattern) except Exception: raise RuleError( msg=u"Syntax error when compiling regex pattern: {0}".format( self.pattern_regexp), error_key=u"pattern.syntax_error", path=path, )
def init_sequence_value(self, v, rule, path): """ """ log.debug(u"Init sequence value : %s", path) if v is not None and not isinstance(v, list): raise RuleError( msg=u"Sequence keyword is not a list", error_key=u"sequence.not_seq", path=path, ) self.sequence = v if self.sequence is None or len(self.sequence) == 0: raise RuleError( msg=u"Sequence contains 0 elements", error_key=u"sequence.no_elements", path=path, ) tmp_seq = [] for i, e in enumerate(self.sequence): elem = e or {} rule = Rule(None, self) rule.init(elem, u"{0}/sequence/{1}".format(path, i)) tmp_seq.append(rule) self.sequence = tmp_seq return rule
def init_func(self, v, rule, path): """ """ if not isinstance(v, str): raise RuleError( msg=u"Value: {0} for func keyword must be a string".format(v), error_key=u"func.notstring", path=path, ) self.func = v
def init_example(self, v, rule, path): log.debug(u'Init example value : {0}'.format(path)) if not isinstance(v, basestring): raise RuleError( msg=u"Value: {0} for keyword example must be a string".format( v), error_key=u"example.not_string", path=path, ) self.desc = v
def init_desc_value(self, v, rule, path): """ """ log.debug(u"Init descr value : %s", path) if not isinstance(v, basestring): raise RuleError( msg=u"Value: {0} for keyword desc must be a string".format(v), error_key=u"desc.not_string", path=path, ) self.desc = v
def init_required_value(self, v, rule, path): """ """ log.debug(u"Init required value : %s", path) if not isinstance(v, bool): raise RuleError( msg=u"Value: '{0}' for required keyword must be a boolean". format(v), error_key=u"required.not_bool", path=path, ) self.required = v
def init_format_value(self, v, rule, path): log.debug(u"Init format value : %s", path) if isinstance(v, basestring): self._format = [v] elif isinstance(v, list): valid = True for date_format in v: if not isinstance(date_format, basestring): valid = False if valid: self._format = v else: raise RuleError( msg=u"All values in format list must be strings", error_key=u"format.not_string", path=path, ) else: raise RuleError( msg= u"Value of format keyword: '{}' must be a string or list or string values" .format(v), error_key=u"format.not_string", path=path, ) valid_types = ("date", ) # Format is only supported when used with "type=date" if self._type not in valid_types: raise RuleError( msg= "Keyword format is only allowed when used with the following types: {0}" .format(valid_types), error_key=u"format.not_used_with_correct_type", path=path, )
def init_enum_value(self, v, rule, path): """ """ log.debug(u"Init enum value : %s", path) if not isinstance(v, list): raise RuleError( msg=u"Enum is not a sequence", error_key=u"enum.not_seq", path=path, ) self.enum = v if is_collection_type(self.type): raise RuleError( msg=u"Enum is not a scalar", error_key=u"enum.not_scalar", path=path, ) lookup = set() for item in v: if not isinstance(item, self.type_class): raise RuleError( msg= u"Item: '{0}' in enum is not of correct class type: '{1}'". format(item, self.type_class), error_key=u"enum.type.unmatch", path=path, ) if item in lookup: raise RuleError( msg=u"Duplicate items: '{0}' found in enum".format(item), error_key=u"enum.duplicate_items", path=path, ) lookup.add(item)
def init_name_value(self, v, rule, path): """ """ log.debug(u"Init name value : %s", path) if not isinstance(v, basestring): raise RuleError( msg=u"Value: {0} for keyword name must be a string".format(v), error_key=u"name.not_string", path=path, ) self.name = v
def init_extensions(self, v, rule, path): """ """ if not isinstance(v, list): raise RuleError( msg=u"Extension definition should be a list", error_key=u"extension.not_list", path=path, ) # TODO: Add limitation that this keyword can only be used at the top level of the file self.extensions = v
def init_assert_value(self, v, rule, path): """ """ log.debug(u"Init assert value : %s", path) if not isinstance(v, str): raise RuleError( msg=u"Value: '{0}' for keyword 'assert' is not a string". format(v), error_key=u"assert.not_str", path=path, ) self.assertion = v if any(k in self.assertion for k in (';', 'import', '__import__')): raise RuleError( msg= u"Value: '{assertion}' contain invalid content that is not allowed to be present in assertion keyword" .format(assertion=self.assertion), error_key=u"assert.unsupported_content", path=path, )
def init_ident_value(self, v, rule, path): """ """ log.debug(u"Init ident value : %s", path) if v is None or not isinstance(v, bool): raise RuleError( msg=u"Value: '{0}' of 'ident' is not a boolean value".format( v), error_key=u"ident.not_bool", path=path, ) self.ident = bool(v) self.required = True if is_collection_type(self.type): raise RuleError( msg=u"Value: '{0}' of 'ident' is not a scalar value".format(v), error_key=u"ident.not_scalar", path=path, ) if path == "": raise RuleError( msg=u"Keyword 'ident' can't be on root level of schema", error_key=u"ident.not_on_root_level", path=path, ) if self.parent is None or not self.parent.type == "map": raise RuleError( msg=u"Keword 'ident' can't be inside 'map'", error_key=u"ident.not_in_map", path=path, )
def init_matching(self, v, rule, path): """ """ log.debug(u"Init matching rule : %s", path) valid_values = ["any", "all", "*"] if str(v) not in valid_values: raise RuleError( msg=u"matching value: {0} is not one of {1}".format( str(v), valid_values), error_key=u"matching_rule.invalid", path=path, ) self.matching = str(v)
def init_matching_rule(self, v, rule, path): """ """ log.debug(u"Init matching-rule: %s", path) log.debug(u"%s %s", v, rule) # Verify that the provided rule is part of one of the allowed one allowed = ["any", "all"] # ["none", "one"] Is currently awaiting proper implementation if v not in allowed: raise RuleError( msg= u"Specified rule in key: {0} is not part of allowed rule set : {1}" .format(v, allowed), error_key=u"matching_rule.not_allowed", path=path, ) else: self.matching_rule = v
def init_type_value(self, v, rule, path): """ """ log.debug(u"Init type value : %s", path) log.debug(u"Type: %s %s", v, rule) if v is None: v = DEFAULT_TYPE self.type = v self.type_class = type_class(v) if not is_builtin_type(self.type): raise RuleError( msg=u"Type: {0} is not any of the known types".format( self.type), error_key=u"type.unknown", path=path, )
def init_range_value(self, v, rule, path): """ """ log.debug(u"Init range value : %s", path) supported_types = ["str", "int", "float", "number", "map", "seq"] if not isinstance(v, dict): raise RuleError( msg=u"Range value is not a dict type: '{0}'".format(v), error_key=u"range.not_map", path=path, ) if self.type not in supported_types: raise RuleError( msg=u"Range value type: '{0}' is not a supported type".format( self.type), error_key=u"range.not_supported_type", path=path, ) # dict that should contain min, max, min-ex, max-ex keys self.range = v # This should validate that only min, max, min-ex, max-ex exists in the dict for k, v in self.range.items(): if k not in ["max", "min", "max-ex", "min-ex"]: raise RuleError( msg=u"Unknown key: '{0}' found in range keyword".format(k), error_key=u"range.unknown_key", path=path, ) if "max" in self.range and "max-ex" in self.range: raise RuleError( msg=u"'max' and 'max-ex' can't be used in the same range rule", error_key=u"range.max_duplicate_keywords", path=path, ) if "min" in self.range and "min-ex" in self.range: raise RuleError( msg=u"'min' and 'min-ex' can't be used in the same range rule", error_key=u"range.min_duplicate_keywords", path=path, ) max = self.range.get("max") min = self.range.get("min") max_ex = self.range.get("max-ex") min_ex = self.range.get("min-ex") if max is not None and not is_number(max) or is_bool(max): raise RuleError( msg=u"Value: '{0}' for 'max' keyword is not a number".format( v), error_key=u"range.max.not_number", path=path, ) if min is not None and not is_number(min) or is_bool(min): raise RuleError( msg=u"Value: '{0}' for 'min' keyword is not a number".format( v), error_key=u"range.min.not_number", path=path, ) if max_ex is not None and not is_number(max_ex) or is_bool(max_ex): raise RuleError( msg=u"Value: '{0}' for 'max-ex' keyword is not a number". format(v), error_key=u"range.max_ex.not_number", path=path, ) if min_ex is not None and not is_number(min_ex) or is_bool(min_ex): raise RuleError( msg=u"Value: '{0}' for 'min-ex' keyword is not a number". format(v), error_key=u"range.min_ex.not_number", path=path, ) # only numbers allow negative ranges # string, map and seq require non negative ranges if self.type not in ["int", "float", "number"]: if min is not None and min < 0: raise RuleError( msg=u"Value for 'min' can't be negative in case of type {0}." .format(self.type), error_key=u"range.min_negative", path=path, ) elif min_ex is not None and min_ex < 0: raise RuleError( msg= u"Value for 'min-ex' can't be negative in case of type {0}." .format(self.type), error_key=u"range.min-ex_negative", path=path, ) if max is not None and max < 0: raise RuleError( msg=u"Value for 'max' can't be negative in case of type {0}." .format(self.type), error_key=u"range.max_negative", path=path, ) elif max_ex is not None and max_ex < 0: raise RuleError( msg= u"Value for 'max-ex' can't be negative in case of type {0}." .format(self.type), error_key=u"range.max-ex_negative", path=path, ) if max is not None: if min is not None and max < min: raise RuleError( msg= u"Value for 'max' can't be less then value for 'min'. {0} < {1}" .format(max, min), error_key=u"range.max_lt_min", path=path, ) elif min_ex is not None and max <= min_ex: raise RuleError( msg= u"Value for 'max' can't be less then value for 'min-ex'. {0} <= {1}" .format(max, min_ex), error_key=u"range.max_le_min-ex", path=path, ) elif max_ex is not None: if min is not None and max_ex < min: raise RuleError( msg= u"Value for 'max-ex' can't be less then value for 'min'. {0} < {1}" .format(max_ex, min), error_key=u"range.max-ex_le_min", path=path, ) elif min_ex is not None and max_ex <= min_ex: raise RuleError( msg= u"Value for 'max-ex' can't be less then value for 'min-ex'. {0} <= {1}" .format(max_ex, min_ex), error_key=u"range.max-ex_le_min-ex", path=path, )
def init_mapping_value(self, v, rule, path): """ """ # Check for duplicate use of 'map' and 'mapping' if self.mapping: raise RuleError( msg= u"Keywords 'map' and 'mapping' can't be used on the same level", error_key=u"mapping.duplicate_keywords", path=path, ) log.debug(u"Init mapping value : %s", path) if v is not None and not isinstance(v, dict): raise RuleError( msg=u"Value for keyword 'map/mapping' is not a dict", error_key=u"mapping.not_dict", path=path, ) if v is None or len(v) == 0: raise RuleError( msg=u"Mapping do not contain any elements", error_key=u"mapping.no_elements", path=path, ) self.mapping = {} self.regex_mappings = [] for k, v in v.items(): if v is None: v = {} # Check if this is a regex rule. Handle specially if k.startswith("regex;") or k.startswith("re;"): log.debug(u"Found regex map rule") regex = k.split(";", 1) if len(regex) != 2: raise RuleError( msg=u"Value: '{0}' for keyword regex is malformed". format(k), error_key=u"mapping.regex.malformed", path=path, ) else: regex = regex[1] try: re.compile(regex) except Exception as e: log.debug(e) raise RuleError( msg=u"Unable to compile regex '{0}'".format(regex), error_key=u"mapping.regex.compile_error", path=path, ) regex_rule = Rule(None, self) regex_rule.init( v, u"{0}/mapping;regex/{1}".format(path, regex[1:-1])) regex_rule.map_regex_rule = regex[1:-1] self.regex_mappings.append(regex_rule) self.mapping[k] = regex_rule else: rule = Rule(None, self) rule.init(v, u"{0}/mapping/{1}".format(path, k)) self.mapping[k] = rule return rule
def check_type_keywords(self, schema, rule, path): """ All supported keywords: - allowempty_map - assertion - date - default - desc - enum - example - extensions - func - ident - include_name - map_regex_rule - mapping - matching - matching_rule - name - pattern - pattern_regexp - range - regex_mappings - required - schema - sequence - type - type_class - unique - version """ if not self.strict_rule_validation: return global_keywords = [ 'type', 'desc', 'example', 'extensions', 'name', 'version', 'func', 'include' ] all_allowed_keywords = { 'str': global_keywords + [ 'default', 'pattern', 'range', 'enum', 'required', 'unique', 'req' ], 'int': global_keywords + ['default', 'range', 'enum', 'required', 'unique'], 'float': global_keywords + ['default', 'enum', 'range', 'required'], 'number': global_keywords + ['default', 'enum'], 'bool': global_keywords + ['default', 'enum'], 'map': global_keywords + [ 'allowempty_map', 'mapping', 'map', 'allowempty', 'required', 'matching-rule', 'range' ], 'seq': global_keywords + ['sequence', 'seq', 'required', 'range', 'matching'], 'sequence': global_keywords + ['sequence', 'seq', 'required'], 'mapping': global_keywords + ['mapping', 'seq', 'required'], 'timestamp': global_keywords + ['default', 'enum'], 'date': global_keywords + ['default', 'enum'], 'symbol': global_keywords + ['default', 'enum'], 'scalar': global_keywords + ['default', 'enum'], 'text': global_keywords + ['default', 'enum', 'pattern'], 'any': global_keywords + ['default', 'enum'], 'enum': global_keywords + ['default', 'enum'], 'none': global_keywords + ['default', 'enum', 'required'], } rule_type = schema.get('type') if not rule_type: # Special cases for the "shortcut methods" if 'sequence' in schema or 'seq' in schema: rule_type = 'sequence' elif 'mapping' in schema or 'map' in schema: rule_type = 'mapping' allowed_keywords = all_allowed_keywords.get(rule_type) if not allowed_keywords and 'sequence' not in schema and 'mapping' not in schema and 'seq' not in schema and 'map' not in schema: raise RuleError( 'No allowed keywords found for type: {0}'.format(rule_type)) for k, v in schema.items(): if k not in allowed_keywords: raise RuleError( 'Keyword "{0}" is not supported for type: "{1}" '.format( k, rule_type))
def init(self, schema, path): """ """ log.debug(u"Init schema: %s", schema) include = schema.get("include") # Check if this item is a include, overwrite schema with include schema and continue to parse if include: log.debug(u"Found include tag...") self.include_name = include return t = None rule = self if schema is not None: if "type" not in schema: # Mapping and sequence do not need explicit type defenitions if any(sa in schema for sa in sequence_aliases): t = "seq" self.init_type_value(t, rule, path) elif any(ma in schema for ma in mapping_aliases): t = "map" self.init_type_value(t, rule, path) else: t = DEFAULT_TYPE self.type = t else: if not isinstance(schema["type"], str): raise RuleError( msg=u"Key 'type' in schema rule is not a string type", error_key=u"type.not_string", path=path, ) self.type = schema["type"] self.schema_str = schema if not t: t = schema["type"] self.init_type_value(t, rule, path) func_mapping = { "allowempty": self.init_allow_empty_map, "assert": self.init_assert_value, "default": self.init_default_value, "desc": self.init_desc_value, "enum": self.init_enum_value, "example": self.init_example, "extensions": self.init_extensions, "format": self.init_format_value, "func": self.init_func, "ident": self.init_ident_value, "length": self.init_length_value, "map": self.init_mapping_value, "mapping": self.init_mapping_value, "matching": self.init_matching, "matching-rule": self.init_matching_rule, "name": self.init_name_value, "pattern": self.init_pattern_value, "range": self.init_range_value, "req": self.init_required_value, "required": self.init_required_value, "seq": self.init_sequence_value, "sequence": self.init_sequence_value, "type": lambda x, y, z: (), "unique": self.init_unique_value, "version": self.init_version, } for k, v in schema.items(): if k in func_mapping: func_mapping[k](v, rule, path) elif k.startswith("schema;"): # Schema tag is only allowed on top level of data log.debug(u"Found schema tag...") raise RuleError( msg=u"Schema is only allowed on top level of schema file", error_key=u"schema.not.toplevel", path=path, ) else: raise RuleError( msg=u"Unknown key: {0} found".format(k), error_key=u"key.unknown", path=path, ) self.check_conflicts(schema, rule, path) self.check_type_keywords(schema, rule, path)