diff --git a/presentation_validator/unique_ids.py b/presentation_validator/unique_ids.py new file mode 100644 index 0000000..c08f486 --- /dev/null +++ b/presentation_validator/unique_ids.py @@ -0,0 +1,79 @@ +import sys +import json +from typing import List, Set, Tuple +from presentation_validator.model import ErrorDetail +from presentation_validator.v3.schemavalidator import create_snippet + +IGNORE: Set[str] = { + "target", + "lookAt", + "range", + "structures", + "first", + "last", + "start", + "source", + "body", + "scope", + "thumbnail", +} +MAX_DEPTH: int = 1000 + + +class MaxDepthExceeded(Exception): + pass + + +def check(manifest) -> List[ErrorDetail]: + """ + Checks that all values associated with the key 'id' are globally unique. + + Args: + manifest: the root JSON dict + + Returns: + A generator of ErrorDetail objects for each duplicate ID found. + The generator is empty if no duplicates are found. + + Raises: + MaxDepthExceeded: If MAX_DEPTH is exceeded in the search. + """ + seen_ids = [] + # stores tuples of (search depth, path, node) + stack: List[Tuple[int, str, dict]] = [(0, "", manifest)] + while stack: + depth, path, node = stack.pop() + if depth > MAX_DEPTH: + raise MaxDepthExceeded(f"Max search depth {MAX_DEPTH} exceeded at {node}") + for key, value in filter(lambda x: x[0] not in IGNORE, node.items()): + if key == "id" or key == "@id": + if value in seen_ids: + yield ErrorDetail( + f"Duplicate id found", + "The id field must be unique", + f"Duplicate id: {value}", + path + "/" + key, + create_snippet(node), + None, + ) + seen_ids.append(value) + elif isinstance(value, list): + for i, item in enumerate(value): + # only dicts can contain IDs + if isinstance(item, dict): + stack.append((depth + 1, f"{path}/{key}[{i}]", item)) + + +def main(): + # pass in manifest by command line argument + # load json from file + with open(sys.argv[1], "r") as f: + manifest = json.load(f) + + errors = check(manifest) + for err in errors: + print(err) + + +if __name__ == "__main__": + main() diff --git a/presentation_validator/v4/unique_ids.py b/presentation_validator/v4/unique_ids.py deleted file mode 100644 index 7126769..0000000 --- a/presentation_validator/v4/unique_ids.py +++ /dev/null @@ -1,61 +0,0 @@ -import sys -import json -from presentation_validator.model import ErrorDetail -from presentation_validator.v3.schemavalidator import create_snippet - -ignore = ["target", "lookAt", "range","structures","first","last","start","source","body","scope"] -# create a method where you pass in a manifest and it checks to see if the id is unique -# if it is not unique, then it should raise a validation error -def check(manifest): - - duplicates = [] - ids = [] - checkNode(manifest, ids, duplicates) - - if len(duplicates) > 0: - return duplicates - else: - return None - -def checkNode(node, ids=[], duplicates=[], path = ""): - if type(node) != dict: - return - - for key, value in node.items(): - if key == 'id': - if type(value) != str: - raise ValueError(f"Id must be a string: {value}") - if value in ids: - duplicates.append(ErrorDetail( - f"Duplicate id found", - "The id field must be unique", - f"Duplicate id: {value}", - path + "/" + key, - create_snippet(node), - None - )) - ids.append(value) - else: - # Don't look further in fields that point to other resources - if key in ignore: - continue - - if type(value) == list: - count = 0 - for item in value: - checkNode(item, ids, duplicates, path + "/" + key + "[" + str(count) + "]") - count += 1 - - elif type(value) != str: - checkNode(value, ids, duplicates, path + "/" + key) - -def main(): - # pass in manifest by command line argument - # load json from file - with open(sys.argv[1], 'r') as f: - manifest = json.load(f) - - check(manifest) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/presentation_validator/v4/validation4.py b/presentation_validator/v4/validation4.py index 9426895..5910f64 100644 --- a/presentation_validator/v4/validation4.py +++ b/presentation_validator/v4/validation4.py @@ -4,7 +4,6 @@ import sys from presentation_validator.model import ValidationResult from presentation_validator.v3.schemavalidator import convertValidationError -from presentation_validator.v4.unique_ids import check from jsonschema import Draft202012Validator from jsonschema.exceptions import relevance @@ -51,18 +50,11 @@ def validate(instance): # Now create some useful messsages to pass on for err in errors: result.errorList.append(convertValidationError(err, errorCount, len(errors))) - + errorCount += 1 else: result.passed = True - duplicate_ids = check(instance) - if duplicate_ids: - result.passed = False - - # Add all of the examples of duplicated ids - result.errorList.extend(duplicate_ids) - return result def main(): diff --git a/presentation_validator/validator.py b/presentation_validator/validator.py index 62d456c..3ebb888 100644 --- a/presentation_validator/validator.py +++ b/presentation_validator/validator.py @@ -4,6 +4,7 @@ from presentation_validator.v3 import schemavalidator from presentation_validator.v4 import validation4 from presentation_validator.enum import IIIFVersion +from presentation_validator import unique_ids import requests from urllib.parse import urlparse @@ -102,6 +103,9 @@ def check_manifest( result.error = str(err) result.url = url + # Check for duplicate ID's in the manifest + result.errorList.extend(unique_ids.check(manifest)) + return result def fetch_manifest(url, accept, version):