diff --git a/fixtures/3/broken_service.json b/fixtures/3/broken_service.json new file mode 100644 index 0000000..504583c --- /dev/null +++ b/fixtures/3/broken_service.json @@ -0,0 +1,56 @@ +{ + "@context": "http://iiif.io/api/presentation/3/context.json", + "id": "https://example.org/iiif/book1/manifest", + "type": "Manifest", + "label": { "en": [ "Book 1" ] }, + "thumbnail": [ + { + "id": "https://example.org/iiif/book1/page1/full/80,100/0/default.jpg", + "type": "Image", + "format": "image/jpeg", + "service": + { + "id": "https://example.org/iiif/book1/page1", + "type": "ImageService3", + "profile": "level1" + } + + } + ], + "items": [ + { + "id": "https://example.org/iiif/book1/canvas/p1", + "type": "Canvas", + "height": 1000, + "width": 750, + "items": [ + { + "id": "https://example.org/iiif/book1/page/p1/1", + "type": "AnnotationPage", + "items": [ + { + "id": "https://example.org/iiif/book1/annotation/p0001-image", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://example.org/iiif/book1/page1/full/max/0/default.jpg", + "type": "Image", + "format": "image/jpeg", + "service": + { + "id": "https://example.org/iiif/book1/page1", + "type": "ImageService3", + "profile": "level2" + } + , + "height": 2000, + "width": 1500 + }, + "target": "https://example.org/iiif/book1/canvas/p1" + } + ] + } + ] + } + ] +} diff --git a/iiif-presentation-validator.py b/iiif-presentation-validator.py index f4102e3..dd1e61e 100755 --- a/iiif-presentation-validator.py +++ b/iiif-presentation-validator.py @@ -8,6 +8,7 @@ import os from gzip import GzipFile from io import BytesIO +from jsonschema.exceptions import ValidationError, SchemaError try: # python3 @@ -61,7 +62,17 @@ def check_manifest(self, data, version, url=None, warnings=[]): infojson = {} # Check if 3.0 if so run through schema rather than this version... if version == '3.0': - infojson = schemavalidator.validate(data, version, url) + try: + infojson = schemavalidator.validate(data, version, url) + for error in infojson['errorList']: + error.pop('error', None) + except ValidationError as e: + infojson = { + 'received': data, + 'okay': 0, + 'error': str(e), + 'url': url + } else: reader = ManifestReader(data, version=version) err = None diff --git a/requirements.txt b/requirements.txt index c2a8b3e..16c7501 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ bottle iiif_prezi jsonschema mock +jsonpath-rw diff --git a/schema/error_processor.py b/schema/error_processor.py new file mode 100755 index 0000000..df60944 --- /dev/null +++ b/schema/error_processor.py @@ -0,0 +1,358 @@ +#!/usr/local/bin/python3 + +from jsonschema import Draft7Validator, RefResolver +from jsonschema.exceptions import ValidationError, SchemaError, best_match, relevance +from jsonpath_rw import jsonpath, parse +import json +import sys +import re + +class IIIFErrorParser(object): + """ + This class tries to clean up json schema validation errors to remove + errors that are misleading. Particularly ones where part of the error is related + to a part of the schema with a different type. This occurs when you use `oneOf` and + the validation then doesn't know if its valid or not so gives misleading errors. To clean this up + this classes dismisses validation errors related to collections and annotation lists if the type is a Manifest. + + To initalise: + + errorParser = IIIFErrorParser(schema, iiif_json) + + where: + schema: the schema as a JSON object + iiif_json: the IIIF asset which failed validation + + then test if the error is related to the type of the IIIF asset: + + if errorParser.isValid(validationError.absolute_schema_path, validationError.absolute_path): + """ + + def __init__(self, schema, iiif_asset): + """ + Intialize the IIIFErrorParse. Parameters: + schema: the schema as a JSON object + iiif_json: the IIIF asset which failed validation + """ + self.schema = schema + self.iiif_asset = iiif_asset + self.resolver = RefResolver.from_schema(self.schema) + + def isValid(self, error_path, IIIFJsonPath): + """ + This checks wheather the passed error path is valid for this iiif_json + If the type doesn't match in the hirearchy then this error can + be dismissed as misleading. + + Arguments: + error_path (list of strings and ints): the path to the schema error + e.g. [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'type', u'pattern'] + from validation_error.absolute_schema_path + IIIFJsonPath (list of strings and ints): the path to the validation error + in the IIIF Json file. e.g. [u'items', 0, u'items', 0, u'items', 0, u'body'] + from validation_error.absolute_path + """ + return self.parse(error_path, self.schema, self.iiif_asset, IIIFJsonPath) + + def diagnoseWhichOneOf(self, error_path, IIIFJsonPath): + """ + Given a schema error that ends in oneOf the current json schema library + will check all possibilities in oneOf and return validation error messages for each one + This method will identify the real oneOf that causes the error by checking the type of + each oneOf possibility and returning only the one that matches. + + Arguments: + error_path: list of strings and ints which are the path to the error in the schema + generated from validation_error.absolute_schema_path + + IIIF Json Path: list of strings and ints which gives the path to the failing part of the + IIIF data. Generated from validation_error.absolute_path + """ + + # convert the IIIF path from a list to an actual JSON object containing only + # the JSON which has failed. + path = parse(self.pathToJsonPath(IIIFJsonPath)) + iiifJsonPart = path.find(self.iiif_asset)[0].value + + # Extract only the part of the schema that has failed and in practice will + # be a list of the oneOf possibilities. Also add all references in the schema + # so these resolve + schema_part = self.addReferences(self.getSchemaPortion(error_path)) + valid_errors = [] + oneOfIndex = 0 + # For each of the oneOf possibilities in the current part of the schema + for possibility in schema_part: + try: + # run through a validator passing the IIIF data snippet + # and the json schema snippet + validator = Draft7Validator(possibility) + results = validator.iter_errors(iiifJsonPart) + except SchemaError as err: + print('Problem with the supplied schema:\n') + print(err) + raise + + # One of the oneOf possibilities is a reference to anouther part of the schema + # this won't bother the validator but will complicate the diagnoise so replace + # it with the actual schema json (and copy all references) + if isinstance(possibility, dict) and "$ref" in possibility: + tmpClas = possibility['classes'] + tmpType = possibility['types'] + possibility = self.resolver.resolve(possibility['$ref'])[1] + possibility['classes'] = tmpClas + possibility['types'] = tmpType + + # This oneOf possiblity failed validation + if results: + addErrors = True + store_errs = [] + for err in results: + # For each of the reported errors check the types with the IIIF data to see if its relevant + # if one error in this oneOf possibility group is not relevant then none a relevant so discard + if not self.parse(list(err.absolute_schema_path), possibility, iiifJsonPart, list(err.absolute_path)): + addErrors = False + else: + # if this oneOf possiblity is still relevant add it to the list and check + # its not another oneOf error + if addErrors: + # if error is also a oneOf then diagnoise aggain + if err.absolute_schema_path[-1] == 'oneOf': + error_path.append(oneOfIndex) # this is is related to one of the original oneOfs at index oneOfIndex + error_path.append('oneOf') # but we found another oneOf test at this location + store_errs.append(self.diagnoseWhichOneOf(error_path, IIIFJsonPath)) # so recursivly discovery real error + #print ('would add: {} by addErrors is {}'.format(err.message, addErrors)) + store_errs.append(err) + # if All errors are relevant to the current type add them to the list + if addErrors: + valid_errors += store_errs + oneOfIndex += 1 + + if valid_errors: + # this may hide errors as we are only selecting the first one but better to get one to work on and then can re-run + # Also need to convert back the error paths to the full path + error_path.reverse() + valid_errors[0].absolute_schema_path.extendleft(error_path) + IIIFJsonPath.reverse() + valid_errors[0].absolute_path.extendleft(IIIFJsonPath) + return valid_errors[0] + else: + # Failed to find the source of the error so most likely its a problem with the type + # and it didn't match any of the possible oneOf types + + path = parse(self.pathToJsonPath(IIIFJsonPath)) + instance = path.find(self.iiif_asset)[0].value + IIIFJsonPath.append('type') + print (IIIFJsonPath) + return ValidationError(message='Failed to find out which oneOf test matched your data. This is likely due to an issue with the type and it not being valid value at this level. SchemaPath: {}'.format(self.pathToJsonPath(error_path)), + path=[], schema_path=error_path, schema=self.getSchemaPortion(error_path), instance=instance) + + + + def pathToJsonPath(self, pathAsList): + """ + Convert a json path as a list of keys and indexes to a json path + + Arguments: + pathAsList e.g. [u'items', 0, u'items', 0, u'items', 0, u'body'] + """ + jsonPath = "$" + for item in pathAsList: + if isinstance(item, int): + jsonPath += '[{}]'.format(item) + else: + jsonPath += '.{}'.format(item) + return jsonPath + + def getSchemaPortion(self, schemaPath): + """ + Given the path return the relevant part of the schema + Arguments: + schemaPath: e.g. [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'type', u'pattern'] + """ + schemaEl = self.schema + for pathPart in schemaPath: + if isinstance(schemaEl[pathPart], dict) and "$ref" in schemaEl[pathPart]: + schemaEl = self.resolver.resolve(schemaEl[pathPart]['$ref'])[1] + else: + schemaEl = schemaEl[pathPart] + + return schemaEl + + def addReferences(self, schemaPart): + """ + For the passed schemaPart add any references so that all #ref statements + resolve in the schemaPart cut down schema. Note this currently is hardcoded to + copy types and classes but could be more clever. + """ + definitions = {} + definitions['types'] = self.schema['types'] + definitions['classes'] = self.schema['classes'] + for item in schemaPart: + item.update(definitions) + + return schemaPart + + def parse(self, error_path, schemaEl, iiif_asset, IIIFJsonPath, parent=None, jsonPath="$"): + """ + Private method which recursivly travels the schema JSON to find + type checks and performs them until it finds a mismatch. If it finds + a mismatch it returns False. + + Parameters: + error_path (list of strings and ints): the path to the schema error + e.g. [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'type', u'pattern'] + from validation_error.absolute_schema_path + schemaEl: the current element we are testing in this iteration. Start with the root + parent: the last element tested + jsonPath: the path in the IIIF assset that relates to the current item in the schema we are looking at + IIIFJsonPath (list of strings and ints): the path to the validation error + in the IIIF Json file. e.g. [u'items', 0, u'items', 0, u'items', 0, u'body'] + from validation_error.absolute_path + + """ + if len(error_path) <= 0: + return True + + pathEl = error_path.pop(0) + # Check current type to see if its a match + if pathEl == 'type' and parent == 'properties': + if 'pattern' in schemaEl['type']: + value = schemaEl['type']['pattern'] + elif 'const' in schemaEl['type']: + value = schemaEl['type']['const'] + elif 'oneOf' in schemaEl['type']: + value = [] + for option in schemaEl['type']['oneOf']: + if 'pattern' in option: + value.append(option['pattern']) + else: + value.append(option['const']) + print ('Using values: {}'.format(value)) + if not self.isTypeMatch(jsonPath + '.type', iiif_asset, value, IIIFJsonPath): + return False + # Check child type to see if its a match + elif pathEl == 'properties' and 'type' in schemaEl['properties'] and 'pattern' in schemaEl['properties']['type']: + value = schemaEl['properties']['type']['pattern'] + if not self.isTypeMatch(jsonPath + '.type', iiif_asset, value, IIIFJsonPath): + return False + # This is the case where additionalProperties has falied but need to check + # if the type didn't match anyway + elif pathEl == 'additionalProperties' and 'type' in schemaEl['properties'] and 'pattern' in schemaEl['properties']['type']: + value = schemaEl['properties']['type']['pattern'] + if not self.isTypeMatch(jsonPath + '.type', iiif_asset, value, IIIFJsonPath): + return False + # if there is a property called items which is of type array add an item array + elif 'type' in schemaEl and schemaEl['type'] == 'array': + jsonPath += '.{}[_]'.format(parent) + #print (schemaEl) + #print (jsonPath) + # For all properties add json key but ignore items which are handled differently above + elif parent == 'properties' and pathEl != 'items' and "ref" in schemaEl[pathEl]: + # check type + jsonPath += '.{}'.format(pathEl) + #print (schemaEl) + + + if isinstance(schemaEl[pathEl], dict) and "$ref" in schemaEl[pathEl]: + + #print ('Found ref, trying to resolve: {}'.format(schemaEl[pathEl]['$ref'])) + return self.parse(error_path, self.resolver.resolve(schemaEl[pathEl]['$ref'])[1], iiif_asset, IIIFJsonPath, pathEl, jsonPath) + else: + return self.parse(error_path, schemaEl[pathEl], iiif_asset, IIIFJsonPath, pathEl, jsonPath) + + def isTypeMatch(self, iiifPath, iiif_asset, schemaType, IIIFJsonPath): + """ + Checks the required type in the schema with the actual type + in the iiif_asset to see if it matches. + Parameters: + iiifPath: the json path in the iiif_asset to the type to be checked. Due to + the way the schema works the index to arrays is left as _ e.g. + $.items[_].items[_].items[_]. The indexes in the array are contained + in IIIFJsonPath variable + schemaType: the type from the schema that should match the type in the iiif_asset + IIIFJsonPath: (Array of strings and int) path to the validation error in the iiif_asset + e.g. [u'items', 0, u'items', 0, u'items', 0, u'body']. The indexes + in this list are used to replace the _ indexes in the iiifPath + + Returns True if the schema type matches the iiif_asset type + """ + # get ints from IIIFJsonPath replace _ with numbers + if IIIFJsonPath: + indexes = [] + for item in IIIFJsonPath: + if isinstance(item, int): + indexes.append(item) + count = 0 + for index in find(iiifPath, '_'): + #print ('Replacing {} with {}'.format(iiifPath[index], indexes[count])) + iiifPath = iiifPath[:index] + str(indexes[count]) + iiifPath[index + 1:] + count += 1 + + #print ('JsonPath: {}'.format(iiifPath)) + path = parse(iiifPath) + typeValue = path.find(iiif_asset)[0].value + #print ('Found type {} and schemaType {}'.format(typeValue, schemaType)) + if isinstance(schemaType, list): + for typeOption in schemaType: + print ('Testing {} = {}'.format(typeOption, typeValue)) + if re.match(typeOption, typeValue): + return True + return False + else: + return re.match(schemaType, typeValue) + +def find(str, ch): + """ + Used to create an list with the indexes of a particular character. e.g.: + find('o_o_o','_') = [1,3] + """ + for i, ltr in enumerate(str): + if ltr == ch: + yield i + +if __name__ == '__main__': + if len(sys.argv) != 2: + print ('Usage:\n\t{} manifest'.format(sys.argv[0])) + exit(-1) + + with open(sys.argv[1]) as json_file: + print ('Loading: {}'.format(sys.argv[1])) + try: + iiif_json = json.load(json_file) + except ValueError as err: + print ('Failed to load JSON due to: {}'.format(err)) + exit(-1) + schema_file = 'schema/iiif_3_0.json' + with open(schema_file) as json_file: + print ('Loading: {}'.format(schema_file)) + try: + schema = json.load(json_file) + except ValueError as err: + print ('Failed to load JSON due to: {}'.format(err)) + exit(-1) + errorParser = IIIFErrorParser(schema, iiif_json) + + # annotationPage + path = [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'type', u'pattern'] + print("Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) + # Annotation + path = [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'type', u'pattern'] + print("Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) + # Additional props fail + path = [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'additionalProperties'] + print("Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) + # Collection + path = [u'allOf', 1, u'oneOf', 1, u'allOf', 1, u'properties', u'thumbnail', u'items', u'oneOf'] + print("Collection Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) + # Collection 2 + path = [u'allOf', 1, u'oneOf', 1, u'allOf', 1, u'properties', u'type', u'pattern'] + print("Collection 2 Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) + # Collection 3 + path = [u'allOf', 1, u'oneOf', 1, u'allOf', 1, u'properties', u'items', u'items', u'oneOf'] + print("Collection 3 Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) + # success service + path = [u'allOf', 1, u'oneOf', 0, u'allOf', 1, u'properties', u'thumbnail', u'items', u'oneOf'] + print("Success Service Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) + # Success service in canvas + path = [u'allOf', 1, u'oneOf', 0, u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'body', u'oneOf'] + print("Success Service Canvas Path: '{}' is valid: {}".format(path, errorParser.isValid(path, [u'items', 0, u'items', 0, u'items', 0, u'body']))) diff --git a/schema/schemavalidator.py b/schema/schemavalidator.py index 0e7aedd..1187efc 100755 --- a/schema/schemavalidator.py +++ b/schema/schemavalidator.py @@ -1,9 +1,11 @@ #!/usr/bin/python from jsonschema import Draft7Validator -from jsonschema.exceptions import ValidationError, SchemaError +from jsonschema.exceptions import ValidationError, SchemaError, best_match, relevance import json -import sys +from os import sys, path +sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) +from schema.error_processor import IIIFErrorParser def printPath(pathObj, fields): path = '' @@ -35,51 +37,60 @@ def validate(data, version, url): raise okay = 0 - errors = sorted(results, key=lambda e: e.path) + #print (best_match(results)) + errors = sorted(results, key=relevance) + #errors = [best_match(results)] error = '' errorsJson = [] if errors: print('Validation Failed') - errorCount = 1 if len(errors) == 1 and 'is not valid under any of the given schemas' in errors[0].message: errors = errors[0].context - for err in errors: - if 'is not valid under any of the given schemas' in err.message: - subErrorMessages = [] - for subErr in err.context: - if 'is not valid under any of the given schemas' not in subErr.message: - subErrorMessages.append(subErr.message) - errorsJson.append({ - 'title': 'Error {} of {}.\n Message: Failed to process submission due too many errors'.format(errorCount, len(errors)), - 'detail': 'This error is likely due to other listed errors. Fix those errors first.', - 'description': "{}".format(subErrorMessages), - 'path': '', - 'context': '' - }) - else: - detail = '' - if 'title' in err.schema: - detail = err.schema['title'] - description = '' - if 'description' in err.schema: - detail += ' ' + err.schema['description'] - context = err.instance - #print (json.dumps(err.instance, indent=4)) - if isinstance(context, dict): - for key in context: - if isinstance(context[key], list): - context[key] = '[ ... ]' - elif isinstance(context[key], dict): - context[key] = '{ ... }' - errorsJson.append({ - 'title': 'Error {} of {}.\n Message: {}'.format(errorCount, len(errors), err.message), - 'detail': detail, - 'description': description, - 'path': printPath(err.path, err.message), - 'context': context - - }) + + # check to see if errors are relveant to IIIF asset + errorParser = IIIFErrorParser(schema, json.loads(data)) + relevantErrors = [] + i = 0 + # Go through the list of errors and check to see if they are relevant + # If the schema has a oneOf clause it will return errors for each oneOf + # possibility. The isValid will check the type to ensure its relevant. e.g. + # if a oneOf possibility is of type Collection but we have passed a Manifest + # then its safe to ignore the validation error. + for err in errors: + if errorParser.isValid(list(err.absolute_schema_path), list(err.absolute_path)): + # if it is valid we want a good error message so diagnose which oneOf is + # relevant for the error we've found. + if err.absolute_schema_path[-1] == 'oneOf': + err = errorParser.diagnoseWhichOneOf(list(err.absolute_schema_path), list(err.absolute_path)) + relevantErrors.append(err) + i += 1 + errors = relevantErrors + errorCount = 1 + # Now create some useful messsages to pass on + for err in errors: + detail = '' + if 'title' in err.schema: + detail = err.schema['title'] + description = '' + if 'description' in err.schema: + detail += ' ' + err.schema['description'] + context = err.instance + if isinstance(context, dict): + for key in context: + if isinstance(context[key], list): + context[key] = '[ ... ]' + elif isinstance(context[key], dict): + context[key] = '{ ... }' + errorsJson.append({ + 'title': 'Error {} of {}.\n Message: {}'.format(errorCount, len(errors), err.message), + 'detail': detail, + 'description': description, + 'path': printPath(err.path, err.message), + 'context': context, + 'error': err + + }) #print (json.dumps(err.instance, indent=4)) errorCount += 1 @@ -106,6 +117,15 @@ def validate(data, version, url): 'url': url } +def json_path(absolute_path): + path = '$' + for elem in absolute_path: + if isinstance(elem, int): + path += '[' + str(elem) + ']' + else: + path += '.' + elem + return path + if __name__ == '__main__': if len(sys.argv) != 2: print ('Usage:\n\t{} manifest'.format(sys.argv[0])) @@ -120,4 +140,15 @@ def validate(data, version, url): result = validate(json.dumps(iiif_json), '3.0', sys.argv[1]) for error in result['errorList']: - print (error['title']) + print ("Message: {}".format(error['title'])) + print (" **") + # print (" Validator: {}".format(error['error'].validator)) + # print (" Relative Schema Path: {}".format(error['error'].relative_schema_path)) + # print (" Schema Path: {}".format(error['error'].absolute_schema_path)) + # print (" Relative_path: {}".format(error['error'].relative_path)) + # print (" Absolute_path: {}".format(error['error'].absolute_path)) + print (" Json_path: {}".format(json_path(error['error'].absolute_path))) + print (" Instance: {}".format(error['error'].instance)) + print (" Context: {}".format(error['error'].context)) + #print (" Full: {}".format(error['error'])) + diff --git a/setup.py b/setup.py index 86ea049..a98557d 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,8 @@ def run(self): install_requires=[ 'bottle>=0.12.9', 'iiif_prezi>=0.2.2', - 'jsonschema' + 'jsonschema', + 'jsonpath_rw' ], extras_require={ ':python_version>="3.0"': ["Pillow>=3.2.0"], diff --git a/tests/test_validator.py b/tests/test_validator.py index 55d2eb8..7f8d52d 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -11,7 +11,6 @@ # fall back to python2 from urllib2 import URLError import json - from os import sys, path sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) @@ -25,6 +24,7 @@ finally: fh.close() +from schema.error_processor import IIIFErrorParser def read_fixture(fixture): """Read data from text fixture.""" @@ -161,6 +161,105 @@ def test07_check_manifest3(self): self.assertEqual(j['okay'], 0) + def test08_errortrees(self): + with open('fixtures/3/broken_service.json') as json_file: + iiif_json = json.load(json_file) + + schema_file = 'schema/iiif_3_0.json' + with open(schema_file) as json_file: + schema = json.load(json_file) + + errorParser = IIIFErrorParser(schema, iiif_json) + + # annotationPage + path = [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'type', u'pattern'] + iiifPath = [u'items', 0, u'type'] + self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to annotation page incorrectly') + + # annotationPage + path = [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'required'] + iiifPath = [u'items', 0] + self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to annotation page incorrectly') + + # annotationPage + path = [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'type', u'pattern'] + iiifPath = [u'type'] + self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to annotation page incorrectly') + + # annotationPage + path = [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'additionalProperties'] + iiifPath = [] + self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to annotation page incorrectly') + + # Collection + path = [u'allOf', 1, u'oneOf', 1, u'allOf', 1, u'properties', u'thumbnail', u'items', u'oneOf'] + iiifPath = [u'thumbnail', 0] + self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to collection incorrectly') + + # Collection + path = [u'allOf', 1, u'oneOf', 1, u'allOf', 1, u'properties', u'type', u'pattern'] + iiifPath = [u'type'] + self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to collection incorrectly') + + # Collection + path = [u'allOf', 1, u'oneOf', 1, u'allOf', 1, u'properties', u'items', u'items', u'oneOf'] + iiifPath = [u'items', 0] + self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to collection incorrectly') + + # annotationPage + path = [u'allOf', 1, u'oneOf', 0, u'allOf', 1, u'properties', u'thumbnail', u'items', u'oneOf'] + iiifPath = [u'thumbnail', 0] + self.assertTrue(errorParser.isValid(path, iiifPath), 'Should have caught the service in thumbnail needs to be an array.') + + # annotationPage + path = [u'allOf', 1, u'oneOf', 0, u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'body', u'oneOf'] + iiifPath = [u'items', 0, u'items', 0, u'items', 0, u'body'] + self.assertTrue(errorParser.isValid(path, iiifPath), 'Should have caught the service in the canvas needs to be an array') + + with open('fixtures/3/broken_simple_image.json') as json_file: + iiif_json = json.load(json_file) + errorParser = IIIFErrorParser(schema, iiif_json) + # Provider as list example: + path = ['allOf', 1, 'oneOf', 0, 'allOf', 1, 'properties', 'provider', 'items', 'allOf', 1, 'properties', 'seeAlso', 'items', 'allOf', 0, 'required'] + iiifPath = ['provider', 0, 'seeAlso', 0] + self.assertTrue(errorParser.isValid(path, iiifPath)) + + def test_version3errors(self): + v = val_mod.Validator() + + filename = 'fixtures/3/broken_simple_image.json' + errorPaths = [ + '/provider[0]/logo[0]', + '/provider[0]/seeAlso[0]', + '/items[0]' + ] + response = self.helperRunValidation(v, filename) + self.helperTestValidationErrors(filename, response, errorPaths) + + filename = 'fixtures/3/broken_service.json' + errorPaths = [ + '/thumbnail[0]/service', + '/body[0]/items[0]/items[0]/items/items[0]/items[0]/items[0]/body/service' + ] + response = self.helperRunValidation(v, filename) + self.helperTestValidationErrors(filename, response, errorPaths) + + + def helperTestValidationErrors(self, filename, response, errorPaths): + self.assertEqual(response['okay'], 0, 'Expected {} to fail validation but it past.'.format(filename)) + self.assertEqual(len(response['errorList']), len(errorPaths), 'Expected {} validation errors but found {} for file {}'.format(len(errorPaths), len(response['errorList']), filename)) + + for error in response['errorList']: + foundPath = False + for path in errorPaths: + if error['path'].startswith(path): + foundPath=True + self.assertTrue(foundPath, 'Unexpected path: {} in file {}'.format(error['path'], filename)) + + def helperRunValidation(self, validator, iiifFile, version="3.0"): + with open(iiifFile, 'r') as fh: + data = fh.read() + return json.loads(validator.check_manifest(data, '3.0')) def printValidationerror(self, filename, errors): print ('Failed to validate: {}'.format(filename))