summaryrefslogtreecommitdiff
path: root/pykwalify/rule.py
diff options
context:
space:
mode:
Diffstat (limited to 'pykwalify/rule.py')
-rw-r--r--pykwalify/rule.py1358
1 files changed, 1358 insertions, 0 deletions
diff --git a/pykwalify/rule.py b/pykwalify/rule.py
new file mode 100644
index 0000000..7ac2c9e
--- /dev/null
+++ b/pykwalify/rule.py
@@ -0,0 +1,1358 @@
+# -*- coding: utf-8 -*-
+
+""" pyKwalify - rule.py """
+
+# python stdlib
+import logging
+import re
+
+# pykwalify imports
+from pykwalify.compat import basestring
+from pykwalify.errors import SchemaConflict, RuleError
+from pykwalify.types import (
+ DEFAULT_TYPE,
+ is_bool,
+ is_builtin_type,
+ is_collection_type,
+ is_number,
+ is_string,
+ mapping_aliases,
+ sequence_aliases,
+ type_class,
+)
+
+log = logging.getLogger(__name__)
+
+
+class Rule(object):
+ """ Rule class that handles a rule constraint """
+
+ def __init__(self, schema=None, parent=None, strict_rule_validation=False):
+ self._allowempty_map = None
+ self._assertion = None
+ self._default = None
+ self._desc = None
+ self._enum = None
+ self._example = None
+ self._extensions = None
+ self._format = None
+ self._func = None
+ self._ident = None
+ self._include_name = None
+ self._length = None
+ self._map_regex_rule = None
+ self._mapping = None
+ # Possible values: [any, all, *]
+ self._matching = "any"
+ self._matching_rule = "any"
+ self._name = None
+ self._nullable = True
+ self._parent = parent
+ self._pattern = None
+ self._pattern_regexp = None
+ self._range = None
+ self._regex_mappings = None
+ self._required = False
+ self._schema = schema
+ self._schema_str = schema
+ self._sequence = None
+ self.strict_rule_validation = strict_rule_validation
+ self._type = None
+ self._type_class = None
+ self._unique = None
+ self._version = None
+
+ if isinstance(schema, dict):
+ self.init(schema, "")
+
+ @property
+ def allowempty_map(self):
+ return self._allowempty_map
+
+ @allowempty_map.setter
+ def allowempty_map(self, value):
+ self._allowempty_map = value
+
+ @property
+ def assertion(self):
+ return self._assertion
+
+ @assertion.setter
+ def assertion(self, value):
+ self._assertion = value
+
+ @property
+ def default(self):
+ return self._default
+
+ @default.setter
+ def default(self, value):
+ self._default = value
+
+ @property
+ def desc(self):
+ return self._desc
+
+ @desc.setter
+ def desc(self, value):
+ self._desc = value
+
+ @property
+ def enum(self):
+ return self._enum
+
+ @enum.setter
+ def enum(self, value):
+ self._enum = value
+
+ @property
+ def example(self):
+ return self._example
+
+ @example.setter
+ def example(self, value):
+ self._example = value
+
+ @property
+ def extensions(self):
+ return self._extensions
+
+ @extensions.setter
+ def extensions(self, value):
+ self._extensions = value
+
+ @property
+ def format(self):
+ return self._format
+
+ @format.setter
+ def format(self, value):
+ self._format = value
+
+ @property
+ def func(self):
+ return self._func
+
+ @func.setter
+ def func(self, value):
+ self._func = value
+
+ @property
+ def ident(self):
+ return self._ident
+
+ @ident.setter
+ def ident(self, value):
+ self._ident = value
+
+ @property
+ def include_name(self):
+ return self._include_name
+
+ @include_name.setter
+ def include_name(self, value):
+ self._include_name = value
+
+ @property
+ def length(self):
+ return self._length
+
+ @length.setter
+ def length(self, value):
+ self._length = value
+
+ @property
+ def map_regex_rule(self):
+ return self._map_regex_rule
+
+ @map_regex_rule.setter
+ def map_regex_rule(self, value):
+ self._map_regex_rule = value
+
+ @property
+ def mapping(self):
+ return self._mapping
+
+ @mapping.setter
+ def mapping(self, value):
+ self._mapping = value
+
+ @property
+ def matching(self):
+ return self._matching
+
+ @matching.setter
+ def matching(self, value):
+ self._matching = value
+
+ @property
+ def matching_rule(self):
+ return self._matching_rule
+
+ @matching_rule.setter
+ def matching_rule(self, value):
+ self._matching_rule = value
+
+ @property
+ def name(self):
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ self._name = value
+
+ @property
+ def nullable(self):
+ return self._nullable
+
+ @nullable.setter
+ def nullable(self, value):
+ self._nullable = value
+
+ @property
+ def parent(self):
+ return self._parent
+
+ @parent.setter
+ def parent(self, value):
+ self._parent = value
+
+ @property
+ def pattern(self):
+ return self._pattern
+
+ @pattern.setter
+ def pattern(self, value):
+ self._pattern = value
+
+ @property
+ def pattern_regexp(self):
+ return self._pattern_regexp
+
+ @pattern_regexp.setter
+ def pattern_regexp(self, value):
+ self._pattern_regexp = value
+
+ @property
+ def range(self):
+ return self._range
+
+ @range.setter
+ def range(self, value):
+ self._range = value
+
+ @property
+ def regex_mappings(self):
+ return self._regex_mappings
+
+ @regex_mappings.setter
+ def regex_mappings(self, value):
+ self._regex_mappings = value
+
+ @property
+ def required(self):
+ return self._required
+
+ @required.setter
+ def required(self, value):
+ self._required = value
+
+ @property
+ def schema(self):
+ return self._schema
+
+ @schema.setter
+ def schema(self, value):
+ self._schema = value
+
+ @property
+ def schema_str(self):
+ return self._schema_str
+
+ @schema_str.setter
+ def schema_str(self, value):
+ self._schema_str = value
+
+ @property
+ def sequence(self):
+ return self._sequence
+
+ @sequence.setter
+ def sequence(self, value):
+ self._sequence = value
+
+ @property
+ def type(self):
+ return self._type
+
+ @type.setter
+ def type(self, value):
+ self._type = value
+
+ @property
+ def type_class(self):
+ return self._type_class
+
+ @type_class.setter
+ def type_class(self, value):
+ self._type_class = value
+
+ @property
+ def unique(self):
+ return self._unique
+
+ @unique.setter
+ def unique(self, value):
+ self._unique = value
+
+ @property
+ def version(self):
+ return self._version
+
+ @version.setter
+ def version(self, value):
+ self._version = value
+
+ def __str__(self):
+ return "Rule: {0}".format(str(self.schema_str))
+
+ def keywords(self):
+ """
+ Returns a list of all keywords that this rule object has defined.
+ A keyword is considered defined if the value it returns != None.
+ """
+ defined_keywords = [
+ ('allowempty_map', 'allowempty_map'),
+ ('assertion', 'assertion'),
+ ('default', 'default'),
+ ('class', 'class'),
+ ('desc', 'desc'),
+ ('enum', 'enum'),
+ ('example', 'example'),
+ ('extensions', 'extensions'),
+ ('format', 'format'),
+ ('func', 'func'),
+ ('ident', 'ident'),
+ ('include_name', 'include'),
+ ('length', 'length'),
+ ('map_regex_rule', 'map_regex_rule'),
+ ('mapping', 'mapping'),
+ ('matching', 'matching'),
+ ('matching_rule', 'matching_rule'),
+ ('name', 'name'),
+ ('nullable', 'nullable')
+ ('parent', 'parent'),
+ ('pattern', 'pattern'),
+ ('pattern_regexp', 'pattern_regexp'),
+ ('range', 'range'),
+ ('regex_mappings', 'regex_mappings'),
+ ('required', 'required'),
+ ('schema', 'schema'),
+ ('schema_str', 'schema_str'),
+ ('sequence', 'sequence'),
+ ('type', 'type'),
+ ('type_class', 'type_class'),
+ ('unique', 'unique'),
+ ('version', 'version'),
+ ]
+ found_keywords = []
+
+ for var_name, keyword_name in defined_keywords:
+ if getattr(self, var_name, None):
+ found_keywords.append(keyword_name)
+
+ return found_keywords
+
+ 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 is_string(schema["type"]):
+ raise RuleError(
+ msg=u"Key 'type' in schema rule is not a string type (found %s)" % type(schema["type"]).__name__,
+ 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,
+ "class": lambda x, y, z: (),
+ "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,
+ "nul": self.init_nullable_value,
+ "nullable": self.init_nullable_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)
+
+ def init_format_value(self, v, rule, path):
+ log.debug(u"Init format value : %s", path)
+
+ if is_string(v):
+ 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_version(self, v, rule, path):
+ """
+ """
+ log.debug(u"Init version value : {0}".format(path))
+
+ self._version = str(v)
+
+ def init_example(self, v, rule, path):
+ log.debug(u'Init example value : {0}'.format(path))
+
+ if not is_string(v):
+ 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_length_value(self, v, rule, path):
+ log.debug(u'Init length value : {0}'.format(path))
+
+ supported_types = ["str", "int", "float", "number", "map", "seq"]
+
+ if not isinstance(v, dict):
+ raise RuleError(
+ msg=u"Length value is not a dict type: '{0}'".format(v),
+ error_key=u"length.not_map",
+ path=path,
+ )
+
+ if self.type not in supported_types:
+ raise RuleError(
+ msg=u"Length value type: '{0}' is not a supported type".format(self.type),
+ error_key=u"length.not_supported_type",
+ path=path,
+ )
+
+ # dict that should contain min, max, min-ex, max-ex keys
+ self.length = v
+
+ # This should validate that only min, max, min-ex, max-ex exists in the dict
+ for k, v in self.length.items():
+ if k not in ["max", "min", "max-ex", "min-ex"]:
+ raise RuleError(
+ msg=u"Unknown key: '{0}' found in length keyword".format(k),
+ error_key=u"length.unknown_key",
+ path=path,
+ )
+
+ if "max" in self.length and "max-ex" in self.length:
+ raise RuleError(
+ msg=u"'max' and 'max-ex' can't be used in the same length rule",
+ error_key=u"length.max_duplicate_keywords",
+ path=path,
+ )
+
+ if "min" in self.length and "min-ex" in self.length:
+ raise RuleError(
+ msg=u"'min' and 'min-ex' can't be used in the same length rule",
+ error_key=u"length.min_duplicate_keywords",
+ path=path,
+ )
+
+ max = self.length.get("max")
+ min = self.length.get("min")
+ max_ex = self.length.get("max-ex")
+ min_ex = self.length.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"length.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"length.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"length.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"length.min_ex.not_number",
+ path=path,
+ )
+
+ # only numbers allow negative lengths
+ # string, map and seq require non negative lengtsh
+ 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"length.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"length.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"length.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"length.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"length.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"length.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"length.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"length.max-ex_le_min-ex",
+ path=path,
+ )
+
+ def init_func(self, v, rule, path):
+ """
+ """
+ if not is_string(v):
+ 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_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_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_allow_empty_map(self, v, rule, path):
+ """
+ """
+ log.debug(u"Init allow empty value: %s", path)
+ log.debug(u"Type: %s : %s", v, rule)
+
+ self.allowempty_map = 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_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_name_value(self, v, rule, path):
+ """
+ """
+ log.debug(u"Init name value : %s", path)
+
+ if not is_string(v):
+ 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_nullable_value(self, v, rule, path):
+ """
+ """
+ log.debug(u"Init nullable value : %s", path)
+
+ if not isinstance(v, bool):
+ raise RuleError(
+ msg=u"Value: '{0}' for nullable keyword must be a boolean".format(v),
+ error_key=u"nullable.not_bool",
+ path=path,
+ )
+
+ self.nullable = v
+
+ def init_desc_value(self, v, rule, path):
+ """
+ """
+ log.debug(u"Init descr value : %s", path)
+
+ if not is_string(v):
+ 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 is_bool(v):
+ 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_pattern_value(self, v, rule, path):
+ """
+ """
+ log.debug(u"Init pattern value : %s", path)
+
+ if not is_string(v):
+ 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_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_assert_value(self, v, rule, path):
+ """
+ """
+ log.debug(u"Init assert value : %s", path)
+
+ if not is_string(v):
+ 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_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_ident_value(self, v, rule, path):
+ """
+ """
+ log.debug(u"Init ident value : %s", path)
+
+ if v is None or not is_bool(v):
+ 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_unique_value(self, v, rule, path):
+ """
+ """
+ log.debug(u"Init unique value : %s", path)
+
+ if not is_bool(v):
+ 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_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_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 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 check_type_keywords(self, schema, rule, path):
+ """
+ All supported keywords:
+ - allowempty_map
+ - assertion
+ - class
+ - date
+ - default
+ - desc
+ - enum
+ - example
+ - extensions
+ - func
+ - ident
+ - include_name
+ - map_regex_rule
+ - mapping
+ - matching
+ - matching_rule
+ - name
+ - nullable
+ - 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', 'nullable', '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', 'class'],
+ '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 check_conflicts(self, schema, rule, path):
+ """
+ """
+ log.debug(u"Checking for conflicts : %s", path)
+
+ if self.type == "seq":
+ if all(sa not in schema for sa in sequence_aliases):
+ raise SchemaConflict(
+ msg="Type is sequence but no sequence alias found on same level",
+ error_key=u"seq.no_sequence",
+ path=path,
+ )
+
+ if self.enum is not None:
+ raise SchemaConflict(
+ msg="Sequence and enum can't be on the same level in the schema",
+ error_key=u"seq.conflict.enum",
+ path=path,
+ )
+
+ if self.pattern is not None:
+ raise SchemaConflict(
+ msg="Sequence and pattern can't be on the same level in the schema",
+ error_key=u"seq.conflict.pattern",
+ path=path,
+ )
+
+ if self.mapping is not None:
+ raise SchemaConflict(
+ msg="Sequence and mapping can't be on the same level in the schema",
+ error_key=u"seq.conflict.mapping",
+ path=path,
+ )
+ elif self.type == "map":
+ if all(ma not in schema for ma in mapping_aliases) and not self.allowempty_map:
+ raise SchemaConflict(
+ msg="Type is mapping but no mapping alias found on same level",
+ error_key=u"map.no_mapping",
+ path=path,
+ )
+
+ if self.enum is not None:
+ raise SchemaConflict(
+ msg="Mapping and enum can't be on the same level in the schema",
+ error_key=u"map.conflict.enum",
+ path=path,
+ )
+
+ if self.sequence is not None:
+ raise SchemaConflict(
+ msg="Mapping and sequence can't be on the same level in the schema",
+ error_key=u"map.conflict.sequence",
+ path=path,
+ )
+ else:
+ if self.sequence is not None:
+ raise SchemaConflict(
+ msg="Scalar and sequence can't be on the same level in the schema",
+ error_key=u"scalar.conflict.sequence",
+ path=path,
+ )
+
+ if self.mapping is not None:
+ raise SchemaConflict(
+ msg="Scalar and mapping can't be on the same level in the schema",
+ error_key=u"scalar.conflict.mapping",
+ path=path,
+ )
+
+ if self.enum is not None and self.range is not None:
+ raise SchemaConflict(
+ msg="Enum and range can't be on the same level in the schema",
+ error_key=u"enum.conflict.range",
+ path=path,
+ )