diff --git a/README.md b/README.md index c77eab3..6045db6 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,22 @@ Note: It is recomended allways to use a virtual-enviroment when using pyKwalify +# How to run tests + +Install test requirements with + +``` +pip install -r test-requirements.txt +``` + +Run tests with + +``` +nosetests +``` + + + # Implemented validation rules ``` @@ -85,6 +101,7 @@ type: - timestamp [NYI] - seq - map + - none - scalar (all but seq and map) - any (means any implemented type of data) @@ -96,11 +113,10 @@ enum: pattern: Specifies regular expression pattern of value. (Uses re.match() ) - pattern rule works in map to validate keys, it is usefull when allowempty is set to True. Pattern also works on all scalar types. - This will be matched against all keys in a map. + Pattern no longer works in map. Use regex; as keys in "mapping:" -regex;: +regex;: This is only implemented in map where a key inside the mapping keyword can implement this regex; pattern and all keys will be matched against the pattern. If a match is found then it will parsed the subrules on that key. A single key can be matched against multiple regex rules and the normal map rules. @@ -137,6 +153,40 @@ matching-rule: +## Partial schemas + +It is possible to create small partial schemas that can be included in other schemas. This feature do not use any built-in YAML or JSON linking. + +To define a partial schema use the keyword "schema;:". must be globally unique for the loaded schema partials. If collisions is detected then error will be raised. + +To use a partial schema use the keyword "include: :". This will work at any place you can specify the keyword "type". Include directive do not currently work inside a partial schema. + +It is possible to define any number of partial schemas in any schema file as long as they are defined at top level of the schema. + +For example, this schema contains one partial and the regular schema. + +```yaml +schema;fooone: + type: map + mapping: + foo: + type: str + + +type: seq +sequence: + - include: fooone + +``` + +And it can be used to validate the following data + +```yaml +- foo: "opa" +``` + + + ## License MIT [See LICENSE file] diff --git a/ReleaseNotes.rst b/ReleaseNotes.rst index f4496ff..acc041a 100644 --- a/ReleaseNotes.rst +++ b/ReleaseNotes.rst @@ -2,6 +2,16 @@ Release Notes ============= +v14.06.1 +======== + + - New feature "partial schema". Define a small schema with a ID that can be reused at other places in the schema. See readme for details. + - New directive "include" that is used to include a partial schema at the specefied location. + - Cli and Core() now can handle multiple schema files. + - Directive "pattern" can no longer be used with map to validate all keys against that regex. Use "regex;" inside "mapping:" + - 'none' can now be used as a type + - Many more tests added + v14.06 ====== diff --git a/pykwalify/__init__.py b/pykwalify/__init__.py index c4dbd06..c28b2b6 100644 --- a/pykwalify/__init__.py +++ b/pykwalify/__init__.py @@ -4,7 +4,7 @@ __author__ = 'Grokzen ' #__version__ = '.'.join(map(str, __version_info__)) -__foobar__ = "0.1.2" +__foobar__ = "14.06.1" # Set to True to have revision from Version Control System in version string __devel__ = True @@ -30,3 +30,10 @@ def init_logging(): "formatters": {"simple": {"format": " {}".format(msg)}}} logging.config.dictConfig(logging_conf) + + +partial_schemas = {} + + +def add_partial_schema(schema_id, rule_object): + partial_schemas[schema_id] = rule_object diff --git a/pykwalify/cli.py b/pykwalify/cli.py index 6635b52..f7410b0 100644 --- a/pykwalify/cli.py +++ b/pykwalify/cli.py @@ -31,7 +31,7 @@ def main(): ##### __docopt__ = """ -usage: pykwalify -d DATAFILE -s SCHEMAFILE [-q] [-v ...] +usage: pykwalify -d DATAFILE -s SCHEMAFILE ... [-q] [-v ...] pykwalify -h | --help pykwalify -V | --version @@ -92,5 +92,5 @@ def main(): ##### 3. parse cli arguments ##### - c = Core(source_file=args["--data-file"], schema_file=args["--schema-file"]) + c = Core(source_file=args["--data-file"], schema_files=args["--schema-file"]) c.validate() diff --git a/pykwalify/core.py b/pykwalify/core.py index f763184..ca0da58 100644 --- a/pykwalify/core.py +++ b/pykwalify/core.py @@ -14,6 +14,7 @@ Log = logging.getLogger(__name__) # pyKwalify imports +import pykwalify from pykwalify.rule import Rule from pykwalify.types import isScalar, tt from pykwalify.errors import CoreError, SchemaError @@ -25,9 +26,9 @@ class Core(object): """ Core class of pyKwalify """ - def __init__(self, source_file=None, schema_file=None, source_data=None, schema_data=None): + def __init__(self, source_file=None, schema_files=[], source_data=None, schema_data=None): Log.debug("source_file: {}".format(source_file)) - Log.debug("schema_file: {}".format(schema_file)) + Log.debug("schema_file: {}".format(schema_files)) Log.debug("source_data: {}".format(source_data)) Log.debug("schema_data: {}".format(schema_data)) @@ -48,17 +49,35 @@ def __init__(self, source_file=None, schema_file=None, source_data=None, schema_ else: raise CoreError("Unable to load source_file. Unknown file format of specified file path: {}".format(source_file)) - if schema_file is not None: - if not os.path.exists(schema_file): - raise CoreError("Provided source_file do not exists on disk") + if not isinstance(schema_files, list): + raise CoreError("schema_files must be of list type") + + # Merge all schema files into one signel file for easy parsing + if len(schema_files) > 0: + schema_data = {} + for f in schema_files: + if not os.path.exists(f): + raise CoreError("Provided source_file do not exists on disk") + + with open(f, "r") as stream: + if f.endswith(".json"): + data = json.load(stream) + if not data: + raise CoreError("No data loaded from file : {}".format(f)) + elif f.endswith(".yaml") or f.endswith(".yml"): + data = yaml.load(stream) + if not data: + raise CoreError("No data loaded from file : {}".format(f)) + else: + raise CoreError("Unable to load file : {} : Unknown file format. Supported file endings is [.json, .yaml, .yml]") + + for key in data.keys(): + if key in schema_data.keys(): + raise CoreError("Parsed key : {} : two times in schema files...".format(key)) + + schema_data = dict(schema_data, **data) - with open(schema_file, "r") as stream: - if schema_file.endswith(".json"): - self.schema = json.load(stream) - elif schema_file.endswith(".yaml"): - self.schema = yaml.load(stream) - else: - raise CoreError("Unable to load source_file. Unknown file format of specified file path: {}".format(schema_file)) + self.schema = schema_data # Nothing was loaded so try the source_data variable if self.source is None: @@ -101,10 +120,26 @@ def _start_validate(self, value=None): errors = [] done = [] + s = {} + + # Look for schema; tags so they can be parsed before the root rule is parsed + for k, v in self.schema.items(): + if k.startswith("schema;"): + Log.debug("Found partial schema; : {}".format(v)) + r = Rule(schema=v) + Log.debug(" Partial schema : {}".format(r)) + pykwalify.partial_schemas[k.split(";", 1)[1]] = r + else: + # readd all items that is not schema; so they can be parsed + s[k] = v + + self.schema = s + Log.debug("Building root rule object") root_rule = Rule(schema=self.schema) self.root_rule = root_rule Log.debug("Done building root rule") + Log.debug("Root rule: {}".format(self.root_rule)) self._validate(value, root_rule, path, errors, done) @@ -122,7 +157,9 @@ def _validate(self, value, rule, path, errors, done): Log.debug(" ? ValidateRule: {}".format(rule)) n = len(errors) - if rule._sequence is not None: + if rule._include_name is not None: + self._validate_include(value, rule, path, errors, done=None) + elif rule._sequence is not None: self._validate_sequence(value, rule, path, errors, done=None) elif rule._mapping is not None or rule._allowempty_map: self._validate_mapping(value, rule, path, errors, done=None) @@ -132,6 +169,19 @@ def _validate(self, value, rule, path, errors, done): if len(errors) != n: return + def _validate_include(self, value, rule, path, errors=[], done=None): + if rule._include_name is None: + errors.append("Include name not valid : {} : {}".format(path, value)) + return + + include_name = rule._include_name + partial_schema_rule = pykwalify.partial_schemas.get(include_name, None) + if not partial_schema_rule: + errors.append("No partial schema found for name : {} : Existing partial schemas: {}".format(include_name, ", ".join(sorted(pykwalify.partial_schemas.keys())))) + return + + self._validate(value, partial_schema_rule, path, errors, done) + def _validate_sequence(self, value, rule, path, errors=[], done=None): Log.debug("Core Validate sequence") Log.debug(" * Data: {}".format(value)) @@ -215,6 +265,10 @@ def _validate_mapping(self, value, rule, path, errors=[], done=None): Log.debug(" + Value is None, returning...") return + if not isinstance(value, dict): + errors.append("mapping.value.notdict : {} : {}".format(value, path)) + return + m = rule._mapping Log.debug(" + RuleMapping: {}".format(m)) @@ -233,13 +287,7 @@ def _validate_mapping(self, value, rule, path, errors=[], done=None): regex_mappings = [(regex_rule, re.match(regex_rule._map_regex_rule, str(k))) for regex_rule in rule._regex_mappings] Log.debug(" + Mapping Regex matches: {}".format(regex_mappings)) - if rule._pattern: - # This is the global regex pattern specefied at the same level as mapping: and type: map keys - res = re.match(rule._pattern, str(k)) - Log.debug("Matching regexPattern: {} with value: {}".format(rule._pattern, k)) - if res is None: # Not matching - errors.append("pattern.unmatch : {} --> {} : {}".format(rule._pattern, k, path)) - elif any(regex_mappings): + if any(regex_mappings): # Found atleast one that matches a mapping regex for mm in regex_mappings: if mm[1]: @@ -279,6 +327,8 @@ def _validate_scalar(self, value, rule, path, errors=[], done=None): if rule._default and value is None: value = rule._default + self._validate_scalar_type(value, rule._type, errors, path) + if value is None: return @@ -357,7 +407,6 @@ def _validate_scalar(self, value, rule, path, errors=[], done=None): if l.get("min-ex", None) is not None and l["min-ex"] >= L: errors.append("length.tooshort-ex : {} >= {} : {}".format(l["min-ex"], L, path)) - self._validate_scalar_type(value, rule._type, errors, path) def _validate_scalar_type(self, value, t, errors, path): Log.debug("Core scalar: validating scalar type") diff --git a/pykwalify/rule.py b/pykwalify/rule.py index d15eecb..385bef3 100644 --- a/pykwalify/rule.py +++ b/pykwalify/rule.py @@ -42,6 +42,7 @@ def __init__(self, schema=None, parent=None): self._matching_rule = None self._map_regex_rule = None self._regex_mappings = None + self._include_name = None self._parent = parent self._schema = schema @@ -56,9 +57,15 @@ def __str__(self): def init(self, schema, path): Log.debug("Init schema: {}".format(schema)) - if schema is not None: - # assert isinstance(schema, dict), "schema is not a dict : {}".format(path) + include = schema.get("include", None) + + # Check if this item is a include, overwrite schema with include schema and continue to parse + if include: + Log.debug("Found include tag...") + self._include_name = include + return + if schema is not None: if "type" not in schema: raise RuleError("key 'type' not found in schema rule : {}".format(path)) else: @@ -96,6 +103,9 @@ def init(self, schema, path): for k, v in schema.items(): if k in func_mapping: func_mapping[k](v, rule, path) + elif k.startswith("schema;"): + Log.debug("Found schema tag...") + raise RuleError("Schema is only allowed on top level of schema file...") else: raise RuleError("Unknown key: {} found : {}".format(k, path)) @@ -160,6 +170,9 @@ def initPatternValue(self, v, rule, path): self._pattern = v + if self._schema_str["type"] == "map": + raise RuleError("map.pattern : pattern not allowed inside map : {} : {}".format(v, path)) + # TODO: Some form of validation of the regexp? it exists in the source try: diff --git a/pykwalify/types.py b/pykwalify/types.py index a3f8c73..87148c7 100644 --- a/pykwalify/types.py +++ b/pykwalify/types.py @@ -24,7 +24,8 @@ "scalar": None, "text": None, "any": object, - "enum": str + "enum": str, + "none": None } @@ -88,6 +89,10 @@ def isEnum(obj): return isinstance(obj, str) +def isNone(obj): + return obj is None + + tt = {"str": isString, "int": isInt, "bool": isBool, @@ -96,4 +101,5 @@ def isEnum(obj): "text": isText, "any": isAny, "enum": isEnum, + "none": isNone } diff --git a/setup.py b/setup.py index 919ad00..99832f7 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ settings.update( name="pykwalify", - version="14.06", + version="14.06.1", description='Python lib/cli for JSON/YAML schema validation', long_description='Python lib/cli for JSON/YAML schema validation', author="Grokzen", diff --git a/tests/files/27a.yaml b/tests/files/27a.yaml deleted file mode 100644 index 5f6e70a..0000000 --- a/tests/files/27a.yaml +++ /dev/null @@ -1 +0,0 @@ -na1me: foobar \ No newline at end of file diff --git a/tests/files/27b.yaml b/tests/files/27b.yaml deleted file mode 100644 index d629738..0000000 --- a/tests/files/27b.yaml +++ /dev/null @@ -1,6 +0,0 @@ -type: map -pattern: "^[a-z]+$" -allowempty: True -mapping: - name: - type: str diff --git a/tests/files/28a.yaml b/tests/files/28a.yaml deleted file mode 100644 index 5f6e70a..0000000 --- a/tests/files/28a.yaml +++ /dev/null @@ -1 +0,0 @@ -na1me: foobar \ No newline at end of file diff --git a/tests/files/28b.yaml b/tests/files/28b.yaml deleted file mode 100644 index 0c2e051..0000000 --- a/tests/files/28b.yaml +++ /dev/null @@ -1,6 +0,0 @@ -type: map -pattern: "^[a-z0-9]+$" -allowempty: True -mapping: - name: - type: str diff --git a/tests/files/29a.yaml b/tests/files/29a.yaml deleted file mode 100644 index 9587acf..0000000 --- a/tests/files/29a.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- mic: - name: input - bits: 16 -- media: - name: output - bits: 32 diff --git a/tests/files/29b.yaml b/tests/files/29b.yaml deleted file mode 100644 index 0525396..0000000 --- a/tests/files/29b.yaml +++ /dev/null @@ -1,9 +0,0 @@ -type: seq -sequence: - - type: map - pattern: ".+" - mapping: - name: - type: str - bits: - type: str diff --git a/tests/files/32a.yaml b/tests/files/32a.yaml new file mode 100644 index 0000000..6d9dc13 --- /dev/null +++ b/tests/files/32a.yaml @@ -0,0 +1,14 @@ +schema;fooone: + type: map + mapping: + foo: + type: str + +schema;footwo: + type: map + mapping: + foo: + type: bool + +schema;str: + type: str diff --git a/tests/files/32b.yaml b/tests/files/32b.yaml new file mode 100644 index 0000000..13e396c --- /dev/null +++ b/tests/files/32b.yaml @@ -0,0 +1 @@ +include: fooone diff --git a/tests/files/32c.yaml b/tests/files/32c.yaml new file mode 100644 index 0000000..4a80875 --- /dev/null +++ b/tests/files/32c.yaml @@ -0,0 +1,12 @@ + + +# 4 +# foo: "bar" + +# 3 +foo: "opa" + +# 2 +# - foo: "opa" + +# 1 diff --git a/tests/files/33a.yaml b/tests/files/33a.yaml new file mode 100644 index 0000000..ea433b7 --- /dev/null +++ b/tests/files/33a.yaml @@ -0,0 +1,3 @@ +type: seq +sequence: + - include: fooone diff --git a/tests/files/33b.yaml b/tests/files/33b.yaml new file mode 100644 index 0000000..ef84657 --- /dev/null +++ b/tests/files/33b.yaml @@ -0,0 +1,11 @@ +schema;fooone: + type: map + mapping: + foo: + type: str + +schema;footwo: + type: map + mapping: + foo: + type: bool \ No newline at end of file diff --git a/tests/files/33c.yaml b/tests/files/33c.yaml new file mode 100644 index 0000000..4444bad --- /dev/null +++ b/tests/files/33c.yaml @@ -0,0 +1 @@ +- foo: "opa" \ No newline at end of file diff --git a/tests/files/34a.yaml b/tests/files/34a.yaml new file mode 100644 index 0000000..462d391 --- /dev/null +++ b/tests/files/34a.yaml @@ -0,0 +1,3 @@ +type: seq +sequence: + - include: fooonez diff --git a/tests/files/34b.yaml b/tests/files/34b.yaml new file mode 100644 index 0000000..b93127a --- /dev/null +++ b/tests/files/34b.yaml @@ -0,0 +1,11 @@ +schema;fooone: + type: map + mapping: + foo: + include: footwo + +schema;footwo: + type: map + mapping: + foo: + type: bool diff --git a/tests/files/34c.yaml b/tests/files/34c.yaml new file mode 100644 index 0000000..4444bad --- /dev/null +++ b/tests/files/34c.yaml @@ -0,0 +1 @@ +- foo: "opa" \ No newline at end of file diff --git a/tests/files/35a.yaml b/tests/files/35a.yaml new file mode 100644 index 0000000..ea433b7 --- /dev/null +++ b/tests/files/35a.yaml @@ -0,0 +1,3 @@ +type: seq +sequence: + - include: fooone diff --git a/tests/files/35b.yaml b/tests/files/35b.yaml new file mode 100644 index 0000000..238abb9 --- /dev/null +++ b/tests/files/35b.yaml @@ -0,0 +1,16 @@ +schema;footwo: + type: map + mapping: + bar: + include: foothree + +schema;fooone: + type: map + mapping: + foo: + include: footwo + +schema;foothree: + type: seq + sequence: + - type: bool diff --git a/tests/files/35c.yaml b/tests/files/35c.yaml new file mode 100644 index 0000000..c67a590 --- /dev/null +++ b/tests/files/35c.yaml @@ -0,0 +1,3 @@ +- foo: + bar: + - true diff --git a/tests/files/36a.yaml b/tests/files/36a.yaml new file mode 100644 index 0000000..fba304b --- /dev/null +++ b/tests/files/36a.yaml @@ -0,0 +1,5 @@ +streams: + - name: + sampleRateMultiple: 1 + - name: media + sampleRateMultiple: 2 \ No newline at end of file diff --git a/tests/files/36b.yaml b/tests/files/36b.yaml new file mode 100644 index 0000000..54b4bfb --- /dev/null +++ b/tests/files/36b.yaml @@ -0,0 +1,16 @@ +type: map +mapping: + streams: + type: seq + required: True + sequence: + - type: map + mapping: + name: + type: str + length: + min: 1 + required: True + sampleRateMultiple: + type: int + required: True \ No newline at end of file diff --git a/tests/files/37a.yaml b/tests/files/37a.yaml new file mode 100644 index 0000000..a2a51ee --- /dev/null +++ b/tests/files/37a.yaml @@ -0,0 +1,3 @@ +streams: + - name: + sampleRateMultiple: 1 diff --git a/tests/files/37b.yaml b/tests/files/37b.yaml new file mode 100644 index 0000000..b5eb823 --- /dev/null +++ b/tests/files/37b.yaml @@ -0,0 +1,14 @@ +type: map +mapping: + streams: + type: seq + required: True + sequence: + - type: map + mapping: + name: + type: none + required: True + sampleRateMultiple: + type: int + required: True \ No newline at end of file diff --git a/tests/testcore.py b/tests/testcore.py index 225b200..bbd5fd8 100644 --- a/tests/testcore.py +++ b/tests/testcore.py @@ -10,12 +10,16 @@ from testfixtures import compare # pyKwalify imports +import pykwalify from pykwalify.core import Core from pykwalify.errors import PyKwalifyExit, UnknownError, FileNotAccessible, OptionError, NotImplemented, ParseFailure, SchemaError, CoreError, RuleError class TestCore(unittest.TestCase): + def setUp(self): + pykwalify.partial_schemas = {} + def f(self, *args): return os.path.join(os.path.dirname(os.path.realpath(__file__)), "files", *args) @@ -49,6 +53,46 @@ def testCoreDataMode(self): with self.assertRaises(SchemaError): Core(source_data=dict, schema_data={"type": "any"}).validate() + def test_multi_file_support(self): + """ + This should test that multiple files is supported correctly + """ + pass_tests = [ + # Test that include directive can be used at top level of the schema + ([self.f("33a.yaml"), self.f("33b.yaml")], self.f("33c.yaml"), {'sequence': [{'include': 'fooone'}], 'type': 'seq'}), + # # This test that include directive works inside sequence + # ([self.f("33a.yaml"), self.f("33b.yaml")], self.f("33c.yaml"), {'sequence': [{'include': 'fooone'}], 'type': 'seq'}), + # This test recursive schemas + ([self.f("35a.yaml"), self.f("35b.yaml")], self.f("35c.yaml"), {'sequence': [{'include': 'fooone'}], 'type': 'seq'}) + ] + + failing_tests = [ + # Test include inside partial schema + ([self.f("34a.yaml"), self.f("34b.yaml")], self.f("34c.yaml"), SchemaError, ['No partial schema found for name : fooonez : Existing partial schemas: fooone, foothree, footwo']) + ] + + for passing_test in pass_tests: + try: + c = Core(source_file=passing_test[1], schema_files=passing_test[0]) + c.validate() + compare(c.validation_errors, [], prefix="No validation errors should exist...") + except Exception as e: + print("ERROR RUNNING FILE: {} : {}".format(passing_test[0], passing_test[1])) + raise e + + # This serve as an extra schema validation that tests more complex structures then testrule.py do + compare(c.root_rule._schema_str, passing_test[2], prefix="Parsed rules is not correct, something have changed...") + + for failing_test in failing_tests: + with self.assertRaises(failing_test[2], msg="Test files: {} : {}".format(", ".join(failing_test[0]), failing_test[1])): + c = Core(schema_files=failing_test[0], source_file=failing_test[1]) + c.validate() + + if not c.validation_errors: + raise AssertionError("No validation_errors was raised...") + + compare(sorted(c.validation_errors), sorted(failing_test[3]), prefix="Wrong validation errors when parsing files : {} : {}".format(failing_test[0], failing_test[1])) + def testCore(self): # These tests should pass with no exception raised pass_tests = [ @@ -83,13 +127,11 @@ def testCore(self): # ("26a.yaml", "26b.yaml", {'type': 'any'}), # - ("28a.yaml", "28b.yaml", {'allowempty': True, 'mapping': {'name': {'type': 'str'}}, 'pattern': '^[a-z0-9]+$', 'type': 'map'}), - # - ("29a.yaml", "29b.yaml", {'sequence': [{'mapping': {'bits': {'type': 'str'}, 'name': {'type': 'str'}}, 'pattern': '.+', 'type': 'map'}], 'type': 'seq'}), - # ("30a.yaml", "30b.yaml", {'sequence': [{'mapping': {'foobar': {'mapping': {'opa': {'type': 'bool'}}, 'type': 'map'}, 'media': {'type': 'int'}, 'regex;[mi.+]': {'sequence': [{'type': 'str'}], 'type': 'seq'}, 'regex;[mo.+]': {'sequence': [{'type': 'bool'}], 'type': 'seq'}}, 'matching-rule': 'any', 'type': 'map'}], 'type': 'seq'}), # This test that a regex that will compile ("31a.yaml", "31b.yaml", {'mapping': {'regex;mi.+': {'sequence': [{'type': 'str'}], 'type': 'seq'}}, 'matching-rule': 'any', 'type': 'map'}), + # Test that type can be set to 'None' and it will validate ok + ("37a.yaml", "37b.yaml", {'mapping': {'streams': {'required': True, 'sequence': [{'mapping': {'name': {'required': True, 'type': 'none'}, 'sampleRateMultiple': {'required': True, 'type': 'int'}}, 'type': 'map'}], 'type': 'seq'}}, 'type': 'map'}), ] # These tests are designed to fail with some exception raised @@ -130,21 +172,25 @@ def testCore(self): ("22a.yaml", "22b.yaml", SchemaError, ["Value: abc is not of type 'number' : /2"]), # This test the text validation rule with wrong data ("24a.yaml", "24b.yaml", SchemaError, ["Value: True is not of type 'text' : /3"]), - # This tests pattern matching on keys in a map - ("27a.yaml", "27b.yaml", SchemaError, ['pattern.unmatch : ^[a-z]+$ --> na1me : ']), + # This test that typechecking works when value in map is None + ("36a.yaml", "36b.yaml", SchemaError, ["Value: None is not of type 'str' : /streams/0/name"]) ] for passing_test in pass_tests: - c = Core(source_file=self.f(passing_test[0]), schema_file=self.f(passing_test[1])) - c.validate() - compare(c.validation_errors, [], prefix="No validation errors should exist...") + try: + c = Core(source_file=self.f(passing_test[0]), schema_files=[self.f(passing_test[1])]) + c.validate() + compare(c.validation_errors, [], prefix="No validation errors should exist...") + except Exception as e: + print("ERROR RUNNING FILE: {} : {}".format(passing_test[0], passing_test[1])) + raise e # This serve as an extra schema validation that tests more complex structures then testrule.py do compare(c.root_rule._schema_str, passing_test[2], prefix="Parsed rules is not correct, something have changed...") for failing_test in fail_tests: with self.assertRaises(failing_test[2], msg="Test file: {} : {}".format(failing_test[0], failing_test[1])): - c = Core(source_file=self.f(failing_test[0]), schema_file=self.f(failing_test[1])) + c = Core(source_file=self.f(failing_test[0]), schema_files=[self.f(failing_test[1])]) c.validate() compare(sorted(c.validation_errors), sorted(failing_test[3]), prefix="Wrong validation errors when parsing files : {} : {}".format(failing_test[0], failing_test[1])) diff --git a/tests/testrule.py b/tests/testrule.py index 14dfb5e..8d10207 100644 --- a/tests/testrule.py +++ b/tests/testrule.py @@ -5,12 +5,16 @@ import unittest # pyKwalify imports +import pykwalify from pykwalify.rule import Rule -from pykwalify.errors import RuleError +from pykwalify.errors import RuleError, SchemaError class TestRule(unittest.TestCase): + def setUp(self): + pykwalify.partial_schemas = {} + def testRuleClass(self): # this tests seq type with a internal type of str r = Rule(schema={"type": "seq", "sequence": [{"type": "str"}]}) @@ -102,3 +106,15 @@ def testRuleClass(self): # This will test that a invalid regex will throw error when parsing rules with self.assertRaises(RuleError): Rule(schema={"type": "map", "matching-rule": "any", "mapping": {"regex;(+": {"type": "seq", "sequence": [{"type": "str"}]}}}) + + # Test that pattern keyword is not allowed when using a map + with self.assertRaisesRegexp(RuleError, ".+map\.pattern.+"): + Rule(schema={"type": "map", "pattern": "^[a-z]+$", "allowempty": True, "mapping": {"name": {"type": "str"}}}) + + # Test that when only having a schema; rule it should throw error + with self.assertRaises(RuleError): + r = Rule(schema={"schema;fooone": {"type": "map", "mapping": {"foo": {"type": "str"}}}}) + + # Test that when using both schema; and include tag that it throw an error because schema; tags should be parsed via Core() + with self.assertRaises(RuleError): + r = Rule(schema={"schema;str": {"type": "map", "mapping": {"foo": {"type": "str"}}}, "type": "map", "mapping": {"foo": {"include": "str"}}})