From ac36c92d77f26322abe14e1b7b0dbc702cd0b992 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Fri, 18 Nov 2022 13:37:26 +0000 Subject: [PATCH 01/23] Adding AnnotationPageRef --- schema/iiif_3_0.json | 64 ++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index c6fea39..e8d3222 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -579,6 +579,20 @@ } ] }, + "annotationPageRef": { + "allOf": [ + { "$ref": "#/types/reference" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^AnnotationPage$" + } + } + } + ] + }, "canvas": { "allOf": [ { "$ref": "#/types/class" }, @@ -666,30 +680,34 @@ ] }, "annotationPage": { - "type": "object", - "properties": { - "@context": {}, - "id": { "$ref": "#/types/id" }, - "type": { - "type": "string", - "pattern": "^AnnotationPage$", - "default": "AnnotationPage" - }, - "label": {"$ref": "#/types/lngString" }, - "service": { "$ref": "#/classes/service" }, - "thumbnail": { - "type": "array", - "items": { "$ref": "#/classes/resource" } + "oneOf": [ + { + "type": "object", + "properties": { + "@context": {}, + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^AnnotationPage$", + "default": "AnnotationPage" + }, + "label": {"$ref": "#/types/lngString" }, + "service": { "$ref": "#/classes/service" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/annotation" + } + } + }, + "required": ["id", "type", "items"], + "additionalProperties": false }, - "items": { - "type": "array", - "items": { - "$ref": "#/classes/annotation" - } - } - }, - "required": ["id", "type"], - "additionalProperties": false + { "$ref": "#/classes/annotationPageRef" } }, "annotation": { "allOf": [ From 3db9b4311493486de68bfb717dad69ffb305ec4b Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Fri, 18 Nov 2022 13:38:46 +0000 Subject: [PATCH 02/23] Fixing json --- schema/iiif_3_0.json | 1 + 1 file changed, 1 insertion(+) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index e8d3222..1b3e03e 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -708,6 +708,7 @@ "additionalProperties": false }, { "$ref": "#/classes/annotationPageRef" } + ] }, "annotation": { "allOf": [ From a89ae52e2c762e04fd155072aa7567da5e84f0d7 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Fri, 9 Dec 2022 13:33:57 +0000 Subject: [PATCH 03/23] Adding rendering where it is allowed --- schema/iiif_3_0.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index 1b3e03e..931f676 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -168,6 +168,7 @@ "width": { "$ref": "#/types/dimension" }, "duration": { "$ref": "#/types/duration" }, "language": { "type": "string"}, + "rendering": { "$ref": "#/types/external" }, "service": { "$ref": "#/classes/service" }, "format": { "$ref": "#/types/format" }, "label": {"$ref": "#/types/lngString" }, @@ -415,6 +416,7 @@ "metadata": { "$ref": "#/classes/metadata" }, "summary": { "$ref": "#/types/lngString" }, "requiredStatement": { "$ref": "#/types/keyValueString" }, + "rendering": { "$ref": "#/types/external" }, "rights": { "$ref": "#/classes/rights" }, "navDate": { "$ref": "#/classes/navDate" }, "navPlace": { "$ref": "#/classes/navPlace" }, @@ -610,6 +612,7 @@ "metadata": { "$ref": "#/classes/metadata" }, "summary": { "$ref": "#/types/lngString" }, "requiredStatement": { "$ref": "#/types/keyValueString" }, + "rendering": { "$ref": "#/types/external" }, "rights": { "$ref": "#/classes/rights" }, "navDate": { "$ref": "#/classes/navDate" }, "navPlace": { "$ref": "#/classes/navPlace" }, @@ -660,6 +663,7 @@ "pattern": "^AnnotationCollection$", "default": "AnnotationCollection" }, + "rendering": { "$ref": "#/types/external" }, "partOf": { "$ref": "#/classes/partOf" }, "next": { "$ref": "#/classes/annotationPage" }, "first": { "$ref": "#/classes/annotationPage" }, @@ -693,6 +697,7 @@ }, "label": {"$ref": "#/types/lngString" }, "service": { "$ref": "#/classes/service" }, + "rendering": { "$ref": "#/types/external" }, "thumbnail": { "type": "array", "items": { "$ref": "#/classes/resource" } @@ -722,6 +727,7 @@ "default": "Annotation" }, "service": { "$ref": "#/classes/service" }, + "rendering": { "$ref": "#/types/external" }, "thumbnail": { "type": "array", "items": { "$ref": "#/classes/resource" } @@ -858,6 +864,7 @@ "pattern": "^Range$", "default": "Range" }, + "rendering": { "$ref": "#/types/external" }, "supplementary": { "$ref": "#/classes/annotationCollection" }, "service": { "$ref": "#/classes/service" }, "annotations": { From 85b433ed81e315298b205cdf162062ad5a4f17be Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Fri, 9 Dec 2022 13:20:14 +0000 Subject: [PATCH 04/23] Adding specific support for accompanyingCanvas and placeholderCanvas --- fixtures/3/accompanyingCanvas.json | 81 +++++++++++++++++++ fixtures/3/placeholderCanvas.json | 68 ++++++++++++++++ schema/iiif_3_0.json | 126 +++++++++++++++++++++++++++++ tests/test_validator.py | 4 +- 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 fixtures/3/accompanyingCanvas.json create mode 100644 fixtures/3/placeholderCanvas.json diff --git a/fixtures/3/accompanyingCanvas.json b/fixtures/3/accompanyingCanvas.json new file mode 100644 index 0000000..641259e --- /dev/null +++ b/fixtures/3/accompanyingCanvas.json @@ -0,0 +1,81 @@ +{ + "@context": "http://iiif.io/api/presentation/3/context.json", + "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/manifest.json", + "type": "Manifest", + "label": { + "en": [ + "Partial audio recording of Gustav Mahler's _Symphony No. 3_" + ] + }, + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/p1", + "type": "Canvas", + "label": { + "en": [ + "Gustav Mahler, Symphony No. 3, CD 1" + ] + }, + "duration": 1985.024, + "accompanyingCanvas": { + "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/accompanying", + "type": "Canvas", + "label": { + "en": [ + "First page of score for Gustav Mahler, Symphony No. 3" + ] + }, + "height": 998, + "width": 772, + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/accompanying/annotation/page", + "type": "AnnotationPage", + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/accompanying/annotation/image", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://iiif.io/api/image/3.0/example/reference/4b45bba3ea612ee46f5371ce84dbcd89-mahler-0/full/,998/0/default.jpg", + "type": "Image", + "format": "image/jpeg", + "height": 998, + "width": 772, + "service": [ + { + "id": "https://iiif.io/api/image/3.0/example/reference/4b45bba3ea612ee46f5371ce84dbcd89-mahler-0", + "type": "ImageService3", + "profile": "level1" + } + ] + }, + "target": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/accompanying" + } + ] + } + ] + }, + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/page/p1", + "type": "AnnotationPage", + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/page/annotation/segment1-audio", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://fixtures.iiif.io/audio/indiana/mahler-symphony-3/CD1/medium/128Kbps.mp4", + "type": "Sound", + "duration": 1985.024, + "format": "video/mp4" + }, + "target": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/page/p1" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/fixtures/3/placeholderCanvas.json b/fixtures/3/placeholderCanvas.json new file mode 100644 index 0000000..6cf6568 --- /dev/null +++ b/fixtures/3/placeholderCanvas.json @@ -0,0 +1,68 @@ +{ + "@context": "http://iiif.io/api/presentation/3/context.json", + "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/manifest.json", + "type": "Manifest", + "label": { + "en": [ + "Video recording of Donizetti's _The Elixer of Love_" + ] + }, + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti", + "type": "Canvas", + "duration": 7278.466, + "width": 640, + "height": 360, + "placeholderCanvas": { + "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti/placeholder", + "type": "Canvas", + "width": 640, + "height": 360, + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti/placeholder/1", + "type": "AnnotationPage", + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti/placeholder/1-image", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://fixtures.iiif.io/video/indiana/donizetti-elixir/act1-thumbnail.png", + "type": "Image", + "format": "image/png", + "width": 640, + "height": 360 + }, + "target": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti/placeholder" + } + ] + } + ] + }, + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/donizetti/1", + "type": "AnnotationPage", + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/donizetti/1-video", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://fixtures.iiif.io/video/indiana/donizetti-elixir/vae0637_accessH264_low.mp4", + "type": "Video", + "duration": 7278.466, + "width": 640, + "height": 360, + "format": "video/mp4" + }, + "target": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index 931f676..f62543f 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -424,6 +424,8 @@ "seeAlso": { "$ref": "#/classes/seeAlso" }, "services": { "$ref": "#/classes/service" }, "service": { "$ref": "#/classes/service" }, + "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, + "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, "thumbnail": { "type": "array", "items": { "$ref": "#/classes/resource" } @@ -489,6 +491,8 @@ "service": { "$ref": "#/classes/service" }, "services": { "$ref": "#/classes/service" }, "viewingDirection": { "$ref": "#/classes/viewingDirection" }, + "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, + "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, "rights": { "$ref": "#/classes/rights" }, "start": {}, "navDate": { "$ref": "#/classes/navDate" }, @@ -619,6 +623,8 @@ "provider": { "$ref": "#/classes/provider" }, "seeAlso": { "$ref": "#/classes/seeAlso" }, "service": { "$ref": "#/classes/service" }, + "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, + "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, "thumbnail": { "type": "array", "items": { "$ref": "#/classes/resource" } @@ -652,6 +658,124 @@ } ] }, + "placeholderCanvas": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Canvas$", + "default": "Canvas" + }, + "height": { "$ref": "#/types/dimension" }, + "width": { "$ref": "#/types/dimension" }, + "duration": { "$ref": "#/types/duration" }, + "metadata": { "$ref": "#/classes/metadata" }, + "summary": { "$ref": "#/types/lngString" }, + "requiredStatement": { "$ref": "#/types/keyValueString" }, + "rendering": { "$ref": "#/types/external" }, + "rights": { "$ref": "#/classes/rights" }, + "navDate": { "$ref": "#/classes/navDate" }, + "navPlace": { "$ref": "#/classes/navPlace" }, + "provider": { "$ref": "#/classes/provider" }, + "seeAlso": { "$ref": "#/classes/seeAlso" }, + "service": { "$ref": "#/classes/service" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "homepage": { "$ref": "#/classes/homepage" }, + "behavior": { "$ref": "#/classes/behavior" }, + "partOf": { "$ref": "#/classes/partOf" }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/anonPage" + } + }, + "annotations": { + "type": "array", + "items": { + "$ref": "#/classes/annotationPage" + } + } + }, + "required": ["items"], + "anyOf":[ + { "required": ["width"] }, + { "required": ["height"] }, + { "required": ["duration"] } + ], + "dependencies": { + "width": ["height"], + "height": ["width"] + }, + "not": { "required": [ "placeholderCanvas", "accompanyingCanvas" ] } + } + ] + }, + "accompanyingCanvas": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^Canvas$", + "default": "Canvas" + }, + "height": { "$ref": "#/types/dimension" }, + "width": { "$ref": "#/types/dimension" }, + "duration": { "$ref": "#/types/duration" }, + "metadata": { "$ref": "#/classes/metadata" }, + "summary": { "$ref": "#/types/lngString" }, + "requiredStatement": { "$ref": "#/types/keyValueString" }, + "rendering": { "$ref": "#/types/external" }, + "rights": { "$ref": "#/classes/rights" }, + "navDate": { "$ref": "#/classes/navDate" }, + "navPlace": { "$ref": "#/classes/navPlace" }, + "provider": { "$ref": "#/classes/provider" }, + "seeAlso": { "$ref": "#/classes/seeAlso" }, + "service": { "$ref": "#/classes/service" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "homepage": { "$ref": "#/classes/homepage" }, + "behavior": { "$ref": "#/classes/behavior" }, + "partOf": { "$ref": "#/classes/partOf" }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/anonPage" + } + }, + "annotations": { + "type": "array", + "items": { + "$ref": "#/classes/annotationPage" + } + } + }, + "required": ["items"], + "anyOf":[ + { "required": ["width"] }, + { "required": ["height"] }, + { "required": ["duration"] } + ], + "dependencies": { + "width": ["height"], + "height": ["width"] + }, + "not": { "required": [ "placeholderCanvas", "accompanyingCanvas" ] } + } + ] + }, + + "annotationCollection": { "allOf": [ { "$ref": "#/types/class" }, @@ -867,6 +991,8 @@ "rendering": { "$ref": "#/types/external" }, "supplementary": { "$ref": "#/classes/annotationCollection" }, "service": { "$ref": "#/classes/service" }, + "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, + "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, "annotations": { "type": "array", "items": { diff --git a/tests/test_validator.py b/tests/test_validator.py index c3bba34..44feb14 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -146,7 +146,9 @@ def test07_check_manifest3(self): 'fixtures/3/publicdomain.json', 'fixtures/3/navPlace.json', 'fixtures/3/anno_source.json', - 'fixtures/3/range_range.json' + 'fixtures/3/range_range.json', + 'fixtures/3/accompanyingCanvas.json', + 'fixtures/3/placeholderCanvas.json' ]: with open(good, 'r') as fh: print ('Testing: {}'.format(good)) From 3d1894a89a58f5ebfa7639630b43f4512469c6b0 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Fri, 9 Dec 2022 13:25:12 +0000 Subject: [PATCH 05/23] Fixing missing anno page ref --- schema/iiif_3_0.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index f62543f..2313a9e 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -692,7 +692,7 @@ "items": { "type": "array", "items": { - "$ref": "#/classes/anonPage" + "$ref": "#/classes/annotationPage" } }, "annotations": { @@ -750,7 +750,7 @@ "items": { "type": "array", "items": { - "$ref": "#/classes/anonPage" + "$ref": "#/classes/annotationPage" } }, "annotations": { From 87ed1483e5786a9defa1a5dc17d88dcd52155419 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Fri, 9 Dec 2022 13:49:42 +0000 Subject: [PATCH 06/23] Only run tests on push --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c14b408..b2fa01d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ name: Run-tests # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch -on: [push, pull_request] +on: [push] jobs: build: From ee463e42fa9f806abcb6e2327b54d5a1af19af67 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Sat, 10 Dec 2022 03:21:02 +0000 Subject: [PATCH 07/23] Moving away from oneOf to if/then/else --- fixtures/3/full_example.json | 2 +- schema/iiif_3_0.json | 298 ++++++++++++++++++++--------------- schema/schemavalidator.py | 53 +++++-- tests/test_validator.py | 74 +-------- 4 files changed, 219 insertions(+), 208 deletions(-) diff --git a/fixtures/3/full_example.json b/fixtures/3/full_example.json index 9d4b692..07a9b9e 100644 --- a/fixtures/3/full_example.json +++ b/fixtures/3/full_example.json @@ -145,7 +145,7 @@ { "@id": "https://example.org/iiif/auth/token", "@type": "AuthTokenService1", - "profile": "http://iiif.io/api/auth/1/token" + "profile": "http://iiif.io/api/auth/1/token" } ] } diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index 2313a9e..ef10dfe 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -83,26 +83,21 @@ } }, "reference": { - "allOf": [ - { - "type": "object", - "additionalProperties": true, - "properties": { - "id": { "$ref": "#/types/id" }, - "label": {"$ref": "#/types/lngString" }, - "type": { - "type": "string", - "pattern": "^Manifest$|^AnnotationPage$|^Collection$|^AnnotationCollection$|^Canvas$|^Range$" - }, - "thumbnail": { - "type": "array", - "items": { "$ref": "#/classes/resource" } - } - }, - "required": ["id", "type", "label"], - "not": { "required": [ "items" ] } + "type": "object", + "additionalProperties": true, + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^Manifest$|^AnnotationPage$|^Collection$|^AnnotationCollection$|^Canvas$|^Range$" + }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } } - ] + }, + "required": ["id", "type"], + "not": { "required": [ "items" ] } } }, @@ -142,6 +137,8 @@ } }, "choice": { + "title": "Choice", + "description": "must have a type:'Choice' and an items array", "type": "object", "properties":{ "type": { @@ -155,53 +152,60 @@ "required": ["type", "items"] }, "resource": { - "oneOf": [ - { - "title": "Annotation bodies MUST have an id and type property.", - "type": "object", - "properties": { - "id": { "$ref": "#/types/id" }, - "type": { - "type": "string" - }, - "height": { "$ref": "#/types/dimension" }, - "width": { "$ref": "#/types/dimension" }, - "duration": { "$ref": "#/types/duration" }, - "language": { "type": "string"}, - "rendering": { "$ref": "#/types/external" }, - "service": { "$ref": "#/classes/service" }, - "format": { "$ref": "#/types/format" }, - "label": {"$ref": "#/types/lngString" }, - "thumbnail": { - "type": "array", - "items": { "$ref": "#/classes/resource" } - }, - "annotations": { - "type": "array", - "items": { - "$ref": "#/classes/annotationPage" - } - } + "title": "ContentResource", + "if": { + "type": "object", + "properties": { + "type": { "const": "TextualBody" } + } + }, + "then": { + "title": "Annotation bodies which are TextualBody MUST have an type and value property.", + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^TextualBody$", + "default": "TextualBody" }, - "required": ["id", "type"] + "value": { "type": "string" }, + "format": { "$ref": "#/types/format" }, + "language": { "type": "string"} }, - { - "title": "Annotation bodies which are TextualBody MUST have an type and value property.", - "type": "object", - "properties": { - "id": { "$ref": "#/types/id" }, - "type": { - "type": "string", - "pattern": "^TextualBody$", - "default": "TextualBody" - }, - "value": { "type": "string" }, - "format": { "$ref": "#/types/format" }, - "language": { "type": "string"} + "required": ["value", "type"] + }, + "else": { + "title": "ContentResource: (not a TextualBody): ", + "description": "Annotation bodies MUST have an id and type property.", + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string" }, - "required": ["value", "type"] - } - ] + "height": { "$ref": "#/types/dimension" }, + "width": { "$ref": "#/types/dimension" }, + "duration": { "$ref": "#/types/duration" }, + "language": { "type": "string"}, + "rendering": { "$ref": "#/types/external" }, + "service": { "$ref": "#/classes/service" }, + "format": { "$ref": "#/types/format" }, + "label": {"$ref": "#/types/lngString" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "annotations": { + "type": "array", + "items": { + "$ref": "#/classes/annotationPage" + } + } + }, + "required": ["id", "type"] + } + }, "imgSvr": { "allOf": [ @@ -216,6 +220,8 @@ ] }, "service": { + "title": "Service", + "description": "must be an array and have an id and type or @id and @type", "type": "array", "items": { "oneOf": [ @@ -436,11 +442,13 @@ "items": { "type": "array", "items": { - "anyOf": [ - { "$ref": "#/classes/manifestRef" }, - { "$ref": "#/classes/collectionRef" }, - { "$ref": "#/classes/collection" } - ] + "title":"Collection items", + "description": " can only by Manifest references or Collections", + "anyOf": [ + { "$ref": "#/classes/manifestRef" }, + { "$ref": "#/classes/collectionRef" }, + { "$ref": "#/classes/collection" } + ] } }, "annotations": { @@ -538,8 +546,10 @@ "type": { "type": "string", "pattern": "^Manifest$" - } - } + }, + "label": {"$ref": "#/types/lngString" } + }, + "required": ["label"] } ] }, @@ -552,8 +562,10 @@ "type": { "type": "string", "pattern": "^Collection$" - } - } + }, + "label": {"$ref": "#/types/lngString" } + }, + "required": ["label"] } ] }, @@ -566,7 +578,8 @@ "type": { "type": "string", "pattern": "^Range$" - } + }, + "label": {"$ref": "#/types/lngString" } } } ] @@ -580,7 +593,8 @@ "type": { "type": "string", "pattern": "^Canvas$" - } + }, + "label": {"$ref": "#/types/lngString" } } } ] @@ -594,7 +608,8 @@ "type": { "type": "string", "pattern": "^AnnotationPage$" - } + }, + "label": {"$ref": "#/types/lngString" } } } ] @@ -603,6 +618,8 @@ "allOf": [ { "$ref": "#/types/class" }, { + "title":"Canvas", + "description": "require items, (height and width) or (duration) or (height, width and duration)", "type": "object", "properties": { "type": { @@ -808,36 +825,40 @@ ] }, "annotationPage": { - "oneOf": [ - { - "type": "object", - "properties": { - "@context": {}, - "id": { "$ref": "#/types/id" }, - "type": { - "type": "string", - "pattern": "^AnnotationPage$", - "default": "AnnotationPage" - }, - "label": {"$ref": "#/types/lngString" }, - "service": { "$ref": "#/classes/service" }, - "rendering": { "$ref": "#/types/external" }, - "thumbnail": { - "type": "array", - "items": { "$ref": "#/classes/resource" } - }, + "if": { + "type": "object", + "required": ["items"] + }, + "then": { + "type": "object", + "properties": { + "@context": {}, + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^AnnotationPage$", + "default": "AnnotationPage" + }, + "label": {"$ref": "#/types/lngString" }, + "service": { "$ref": "#/classes/service" }, + "rendering": { "$ref": "#/types/external" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "items": { + "type": "array", "items": { - "type": "array", - "items": { - "$ref": "#/classes/annotation" - } + "$ref": "#/classes/annotation" } - }, - "required": ["id", "type", "items"], - "additionalProperties": false + } }, - { "$ref": "#/classes/annotationPageRef" } - ] + "required": ["id", "type", "items"], + "additionalProperties": false + }, + "else": { + "$ref": "#/classes/annotationPageRef" + } }, "annotation": { "allOf": [ @@ -868,34 +889,29 @@ ] }, "body": { - "anyOf": [ - { - "type": "object", - "$ref": "#/classes/resource" + "if": { + "type": "object", + "properties": { + "type": { "const": "Choice" } + } + }, + "then": { + "$ref": "#/classes/choice" + }, + "else": { + "if": { + "type": "object" }, - { - "type": "object", - "allOf":[ - { "$ref": "#/classes/choice" }, - { - "properties": { - "items": { - "type": "array", - "items": {"$ref": "#/classes/resource"} - } - }, - "required": ["items"] - } - ] + "then": { + "$ref": "#/classes/resource" }, - { + "else": { "type": "array", "items": { - "type": "object" + "$ref": "#/classes/resource" } } - - ] + } }, "target": { "anyOf": [ @@ -1018,8 +1034,12 @@ "type": "string", "pattern": "^Canvas$", "default": "Canvas" + }, + "items": { + "type": "array" } - } + }, + "required": ["items"] } ] @@ -1037,9 +1057,27 @@ } }, "$id": "http://iiif.io/api/presentation/3/schema.json" , - "oneOf": [ - { "$ref": "#/classes/manifest" }, - { "$ref": "#/classes/collection" }, - { "$ref": "#/classes/annotationPage" } - ] + "if": { + "type": "object", + "properties": { + "type": {"const":"Manifest"} + } + }, + "then": { + "$ref": "#/classes/manifest" + }, + "else": { + "if": { + "type": "object", + "properties": { + "type": {"const":"Collection"} + } + }, + "then": { + "$ref": "#/classes/collection" + }, + "else": { + "$ref": "#/classes/annotationPage" + } + } } diff --git a/schema/schemavalidator.py b/schema/schemavalidator.py index 5fcf6e1..46a2634 100755 --- a/schema/schemavalidator.py +++ b/schema/schemavalidator.py @@ -28,22 +28,56 @@ def validate(data, version, url): print ('Failed to load JSON due to: {}'.format(err)) raise + error = '' + errorsJson = [] try: validator = Draft7Validator(schema) - results = validator.iter_errors(json.loads(data)) + validator.validate(json.loads(data)) + print ('Passed Validation!') + okay = 1 except SchemaError as err: print('Problem with the supplied schema:\n') print(err) raise + except ValidationError as err: + results = validator.iter_errors(json.loads(data)) - okay = 0 - #print (best_match(results)) - errors = sorted(results, key=relevance) - #errors = [best_match(results)] - error = '' - errorsJson = [] - if errors: + okay = 0 + #print (best_match(results)) + errors = sorted(results, key=relevance) + #errors = [best_match(results)] + errorCount = 1 + for err in errors: + detail = '' + if err and '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] = '{ ... }' + + #print (json.dumps(err.schema,indent=2)) + 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 + + }) + errorCount +=1 + + if False: #errors: print('Validation Failed') + + if len(errors) == 1 and 'is not valid under any of the given schemas' in errors[0].message: errors = errors[0].context @@ -121,9 +155,6 @@ def validate(data, version, url): # } okay = 0 - else: - print ('Passed Validation!') - okay = 1 return { 'received': data, diff --git a/tests/test_validator.py b/tests/test_validator.py index 44feb14..aef1cf5 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -178,71 +178,7 @@ 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) - - #print (errorParser) - # annotationPage - path = [ u'oneOf', 2, u'properties', u'items', u'items', 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'oneOf', 2, u'properties', u'items', u'items', u'required'] - iiifPath = [u'items', 0] - self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to annotation page incorrectly') - - # annotationPage - path = [u'oneOf', 2, u'properties', u'type', u'pattern'] - iiifPath = [u'type'] - self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to annotation page incorrectly') - - # annotationPage - path = [u'oneOf', 2, u'additionalProperties'] - iiifPath = [] - self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to annotation page incorrectly') - - # Collection - path = [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'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'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'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'oneOf', 0, u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'items', u'items', u'properties', u'items', u'items', u'allOf', 1, u'properties', u'body', u'anyOf'] - 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 = ['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): + def test_brokenImage(self): v = val_mod.Validator() filename = 'fixtures/3/broken_simple_image.json' @@ -254,6 +190,8 @@ def test_version3errors(self): response = self.helperRunValidation(v, filename) self.helperTestValidationErrors(filename, response, errorPaths) + def test_brokenService(self): + v = val_mod.Validator() filename = 'fixtures/3/broken_service.json' errorPaths = [ '/thumbnail[0]/service', @@ -262,8 +200,11 @@ def test_version3errors(self): response = self.helperRunValidation(v, filename) self.helperTestValidationErrors(filename, response, errorPaths) + def test_brokenLabel(self): + v = val_mod.Validator() filename = 'fixtures/3/old_format_label.json' errorPaths = [ + '/label', '/label', '/' ] @@ -278,7 +219,8 @@ def test_lang_rights(self): '/label', '/items[0]/label[0]', '/items[0]/', - '/items[0]/items[0]/items[0]/body/', + '/items[0]/', + '/items[0]/items[0]/items[0]/body/label/', '/rights', '/metadata[0]/label/', '/metadata[0]/value/', From e888a6ed13f8f6f7501b31581bd447d302c7ae21 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 2 Mar 2023 17:31:53 +0000 Subject: [PATCH 08/23] Adding latest changes --- schema/iiif_3_0.json | 48 ++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index ef10dfe..117416c 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -1057,27 +1057,37 @@ } }, "$id": "http://iiif.io/api/presentation/3/schema.json" , - "if": { - "type": "object", - "properties": { - "type": {"const":"Manifest"} - } - }, - "then": { - "$ref": "#/classes/manifest" - }, - "else": { - "if": { - "type": "object", - "properties": { - "type": {"const":"Collection"} + "allOf": [ + { + "if": { + "type": "object", + "properties": { + "type": {"const":"Manifest"} + } + }, + "then": { + "$ref": "#/classes/manifest" } }, - "then": { - "$ref": "#/classes/collection" - }, - "else": { + { + "if": { + "type": "object", + "properties": { + "type": {"const":"Collection"} + } + }, + "then": { + "$ref": "#/classes/collection" + } + }, + { + "if": { + "type": "object", + "properties": { + "type": {"const":"AnnotationPage"} + } + }, "$ref": "#/classes/annotationPage" } - } + ] } From 6d0f72c8ceb2f4f094bd150a06fa236e426c1c88 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Wed, 10 Feb 2021 23:47:57 +0000 Subject: [PATCH 09/23] Removing retrieved from json response --- iiif-presentation-validator.py | 3 --- schema/schemavalidator.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/iiif-presentation-validator.py b/iiif-presentation-validator.py index 48b6927..0d2c375 100755 --- a/iiif-presentation-validator.py +++ b/iiif-presentation-validator.py @@ -84,7 +84,6 @@ def check_manifest(self, data, version, url=None, warnings=[]): }) else: infojson = { - 'received': data, 'okay': 0, 'error': str(e), 'url': url, @@ -93,7 +92,6 @@ def check_manifest(self, data, version, url=None, warnings=[]): except Exception as e: traceback.print_exc() infojson = { - 'received': data, 'okay': 0, 'error': 'Presentation Validator bug: "{}". Please create a Validator Issue, including a link to the manifest.'.format(e), 'url': url, @@ -124,7 +122,6 @@ def check_manifest(self, data, version, url=None, warnings=[]): warnings.extend(reader.get_warnings()) infojson = { - 'received': data, 'okay': okay, 'warnings': warnings, 'error': str(err), diff --git a/schema/schemavalidator.py b/schema/schemavalidator.py index 46a2634..76f0699 100755 --- a/schema/schemavalidator.py +++ b/schema/schemavalidator.py @@ -147,7 +147,6 @@ def validate(data, version, url): # Return: # infojson = { - # 'received': data, # 'okay': okay, # 'warnings': warnings, # 'error': str(err), @@ -157,7 +156,6 @@ def validate(data, version, url): okay = 0 return { - 'received': data, 'okay': okay, 'warnings': [], 'error': error, From ece3222c9c56f8aed34dde2d765b3efa526155a3 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 2 Mar 2023 18:55:40 +0000 Subject: [PATCH 10/23] Expanding definition for target source --- schema/iiif_3_0.json | 87 +++++++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index 117416c..2265144 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -6,7 +6,7 @@ "type": "string", "format": "uri", "pattern": "^http.*$", - "title": "Id must be presesnt and must be a URI" + "title": "Id must be present and must be a URI" }, "lngString": { "title": "Language string, must have a language and value must be an array.", @@ -87,6 +87,7 @@ "additionalProperties": true, "properties": { "id": { "$ref": "#/types/id" }, + "label": {"$ref": "#/types/lngString" }, "type": { "type": "string", "pattern": "^Manifest$|^AnnotationPage$|^Collection$|^AnnotationCollection$|^Canvas$|^Range$" @@ -791,8 +792,6 @@ } ] }, - - "annotationCollection": { "allOf": [ { "$ref": "#/types/class" }, @@ -830,31 +829,36 @@ "required": ["items"] }, "then": { - "type": "object", - "properties": { - "@context": {}, - "id": { "$ref": "#/types/id" }, - "type": { - "type": "string", - "pattern": "^AnnotationPage$", - "default": "AnnotationPage" - }, - "label": {"$ref": "#/types/lngString" }, - "service": { "$ref": "#/classes/service" }, - "rendering": { "$ref": "#/types/external" }, - "thumbnail": { - "type": "array", - "items": { "$ref": "#/classes/resource" } - }, - "items": { - "type": "array", - "items": { - "$ref": "#/classes/annotation" - } + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "@context": {}, + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^AnnotationPage$", + "default": "AnnotationPage" + }, + "label": {"$ref": "#/types/lngString" }, + "service": { "$ref": "#/classes/service" }, + "rendering": { "$ref": "#/types/external" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/classes/annotation" + } + } + }, + "required": ["id", "type", "items"], + "additionalProperties": false } - }, - "required": ["id", "type", "items"], - "additionalProperties": false + ] }, "else": { "$ref": "#/classes/annotationPageRef" @@ -961,7 +965,12 @@ }, "format": { "$ref": "#/types/format" }, "accessibility": { "type": "string"}, - "source": { "$ref": "#/types/id" }, + "source": { + "oneOf": [ + { "$ref": "#/types/id" }, + { "$ref": "#/types/class" } + ] + }, "selector": { "oneOf": [ { "$ref": "#/classes/annoSelector" }, @@ -989,7 +998,27 @@ "type": { "type": "string" }, "t": { "$ref": "#/types/duration" } }, - "required": ["type"] + "required": ["type","t"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^FragmentSelector$", + "default": "FragmentSelector" + }, + "conformsTo": { + "type": "string", + "format": "uri", + "pattern": "^http.*$", + "default": "http://www.w3.org/TR/media-frags/" + }, + "value": { + "type:": "string" + } + }, + "required": ["type","value"] } ] }, From b70ce23333d94e5a51125aeb479b8b46e1b8db11 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 2 Mar 2023 19:05:39 +0000 Subject: [PATCH 11/23] Adding annotationPageRef --- schema/iiif_3_0.json | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index 2265144..f0926c1 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -455,7 +455,10 @@ "annotations": { "type": "array", "items": { - "$ref": "#/classes/annotationPage" + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] } } }, @@ -530,7 +533,10 @@ "annotations": { "type": "array", "items": { - "$ref": "#/classes/annotationPage" + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] } } }, @@ -609,12 +615,11 @@ "type": { "type": "string", "pattern": "^AnnotationPage$" - }, - "label": {"$ref": "#/types/lngString" } + } } } ] - }, + }, "canvas": { "allOf": [ { "$ref": "#/types/class" }, @@ -659,7 +664,10 @@ "annotations": { "type": "array", "items": { - "$ref": "#/classes/annotationPage" + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] } } }, @@ -716,7 +724,10 @@ "annotations": { "type": "array", "items": { - "$ref": "#/classes/annotationPage" + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] } } }, @@ -774,7 +785,10 @@ "annotations": { "type": "array", "items": { - "$ref": "#/classes/annotationPage" + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] } } }, @@ -805,9 +819,9 @@ }, "rendering": { "$ref": "#/types/external" }, "partOf": { "$ref": "#/classes/partOf" }, - "next": { "$ref": "#/classes/annotationPage" }, - "first": { "$ref": "#/classes/annotationPage" }, - "last": { "$ref": "#/classes/annotationPage" }, + "next": { "$ref": "#/classes/annotationPageRef" }, + "first": { "$ref": "#/classes/annotationPageRef" }, + "last": { "$ref": "#/classes/annotationPageRef" }, "service": { "$ref": "#/classes/service" }, "thumbnail": { "type": "array", @@ -1041,7 +1055,10 @@ "annotations": { "type": "array", "items": { - "$ref": "#/classes/annotationPage" + "oneOf": [ + { "$ref": "#/classes/annotationPage" }, + { "$ref": "#/classes/annotationPageRef"} + ] } }, "thumbnail": { From d5fe161d7ff0374ca28c579c78492c11f2d16c2a Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Fri, 3 Mar 2023 17:37:33 +0000 Subject: [PATCH 12/23] Fixing tests --- schema/iiif_3_0.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index f0926c1..ae50688 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -85,6 +85,8 @@ "reference": { "type": "object", "additionalProperties": true, + "title": "Reference", + "description":"Id. type, label but not items are required", "properties": { "id": { "$ref": "#/types/id" }, "label": {"$ref": "#/types/lngString" }, @@ -1086,13 +1088,11 @@ } }, "required": ["items"] - } ] }, { "$ref": "#/classes/range" }, - { "$ref": "#/classes/canvasRef" }, - { "$ref": "#/classes/rangeRef" } + { "$ref": "#/classes/canvasRef" } ] } } From 5b53d0bcfa0f90749765f80c1192c769a5b60f9b Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Fri, 3 Mar 2023 23:37:26 +0000 Subject: [PATCH 13/23] Moving valid fixtures to separate directory --- fixtures/3/{ => valid}/accompanyingCanvas.json | 0 fixtures/3/{ => valid}/annoPage.json | 0 fixtures/3/{ => valid}/annoPageMultipleMotivations.json | 0 fixtures/3/{ => valid}/anno_pointselector.json | 0 fixtures/3/{ => valid}/anno_source.json | 0 fixtures/3/{ => valid}/choice.json | 0 fixtures/3/{ => valid}/collection.json | 0 fixtures/3/{ => valid}/collection_of_collections.json | 0 fixtures/3/{ => valid}/extension_anno.json | 0 fixtures/3/{ => valid}/full_example.json | 0 fixtures/3/{ => valid}/multi_bodies.json | 0 fixtures/3/{ => valid}/navPlace.json | 0 fixtures/3/{ => valid}/old_cc_license.json | 0 fixtures/3/{ => valid}/placeholderCanvas.json | 0 fixtures/3/{ => valid}/publicdomain.json | 0 fixtures/3/{ => valid}/range_range.json | 0 fixtures/3/{ => valid}/rightsstatement_license.json | 0 fixtures/3/{ => valid}/simple_video.json | 0 fixtures/3/{ => valid}/version2image.json | 0 19 files changed, 0 insertions(+), 0 deletions(-) rename fixtures/3/{ => valid}/accompanyingCanvas.json (100%) rename fixtures/3/{ => valid}/annoPage.json (100%) rename fixtures/3/{ => valid}/annoPageMultipleMotivations.json (100%) rename fixtures/3/{ => valid}/anno_pointselector.json (100%) rename fixtures/3/{ => valid}/anno_source.json (100%) rename fixtures/3/{ => valid}/choice.json (100%) rename fixtures/3/{ => valid}/collection.json (100%) rename fixtures/3/{ => valid}/collection_of_collections.json (100%) rename fixtures/3/{ => valid}/extension_anno.json (100%) rename fixtures/3/{ => valid}/full_example.json (100%) rename fixtures/3/{ => valid}/multi_bodies.json (100%) rename fixtures/3/{ => valid}/navPlace.json (100%) rename fixtures/3/{ => valid}/old_cc_license.json (100%) rename fixtures/3/{ => valid}/placeholderCanvas.json (100%) rename fixtures/3/{ => valid}/publicdomain.json (100%) rename fixtures/3/{ => valid}/range_range.json (100%) rename fixtures/3/{ => valid}/rightsstatement_license.json (100%) rename fixtures/3/{ => valid}/simple_video.json (100%) rename fixtures/3/{ => valid}/version2image.json (100%) diff --git a/fixtures/3/accompanyingCanvas.json b/fixtures/3/valid/accompanyingCanvas.json similarity index 100% rename from fixtures/3/accompanyingCanvas.json rename to fixtures/3/valid/accompanyingCanvas.json diff --git a/fixtures/3/annoPage.json b/fixtures/3/valid/annoPage.json similarity index 100% rename from fixtures/3/annoPage.json rename to fixtures/3/valid/annoPage.json diff --git a/fixtures/3/annoPageMultipleMotivations.json b/fixtures/3/valid/annoPageMultipleMotivations.json similarity index 100% rename from fixtures/3/annoPageMultipleMotivations.json rename to fixtures/3/valid/annoPageMultipleMotivations.json diff --git a/fixtures/3/anno_pointselector.json b/fixtures/3/valid/anno_pointselector.json similarity index 100% rename from fixtures/3/anno_pointselector.json rename to fixtures/3/valid/anno_pointselector.json diff --git a/fixtures/3/anno_source.json b/fixtures/3/valid/anno_source.json similarity index 100% rename from fixtures/3/anno_source.json rename to fixtures/3/valid/anno_source.json diff --git a/fixtures/3/choice.json b/fixtures/3/valid/choice.json similarity index 100% rename from fixtures/3/choice.json rename to fixtures/3/valid/choice.json diff --git a/fixtures/3/collection.json b/fixtures/3/valid/collection.json similarity index 100% rename from fixtures/3/collection.json rename to fixtures/3/valid/collection.json diff --git a/fixtures/3/collection_of_collections.json b/fixtures/3/valid/collection_of_collections.json similarity index 100% rename from fixtures/3/collection_of_collections.json rename to fixtures/3/valid/collection_of_collections.json diff --git a/fixtures/3/extension_anno.json b/fixtures/3/valid/extension_anno.json similarity index 100% rename from fixtures/3/extension_anno.json rename to fixtures/3/valid/extension_anno.json diff --git a/fixtures/3/full_example.json b/fixtures/3/valid/full_example.json similarity index 100% rename from fixtures/3/full_example.json rename to fixtures/3/valid/full_example.json diff --git a/fixtures/3/multi_bodies.json b/fixtures/3/valid/multi_bodies.json similarity index 100% rename from fixtures/3/multi_bodies.json rename to fixtures/3/valid/multi_bodies.json diff --git a/fixtures/3/navPlace.json b/fixtures/3/valid/navPlace.json similarity index 100% rename from fixtures/3/navPlace.json rename to fixtures/3/valid/navPlace.json diff --git a/fixtures/3/old_cc_license.json b/fixtures/3/valid/old_cc_license.json similarity index 100% rename from fixtures/3/old_cc_license.json rename to fixtures/3/valid/old_cc_license.json diff --git a/fixtures/3/placeholderCanvas.json b/fixtures/3/valid/placeholderCanvas.json similarity index 100% rename from fixtures/3/placeholderCanvas.json rename to fixtures/3/valid/placeholderCanvas.json diff --git a/fixtures/3/publicdomain.json b/fixtures/3/valid/publicdomain.json similarity index 100% rename from fixtures/3/publicdomain.json rename to fixtures/3/valid/publicdomain.json diff --git a/fixtures/3/range_range.json b/fixtures/3/valid/range_range.json similarity index 100% rename from fixtures/3/range_range.json rename to fixtures/3/valid/range_range.json diff --git a/fixtures/3/rightsstatement_license.json b/fixtures/3/valid/rightsstatement_license.json similarity index 100% rename from fixtures/3/rightsstatement_license.json rename to fixtures/3/valid/rightsstatement_license.json diff --git a/fixtures/3/simple_video.json b/fixtures/3/valid/simple_video.json similarity index 100% rename from fixtures/3/simple_video.json rename to fixtures/3/valid/simple_video.json diff --git a/fixtures/3/version2image.json b/fixtures/3/valid/version2image.json similarity index 100% rename from fixtures/3/version2image.json rename to fixtures/3/valid/version2image.json From 6046d4a315329276986eb855832402380ca651ef Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Fri, 3 Mar 2023 23:38:18 +0000 Subject: [PATCH 14/23] Moving invalid tests to its own directory --- fixtures/3/{ => invalid}/broken_choice.json | 0 fixtures/3/{ => invalid}/broken_collection.json | 0 fixtures/3/{ => invalid}/broken_embedded_annos.json | 0 fixtures/3/{ => invalid}/broken_service.json | 0 fixtures/3/{ => invalid}/broken_simple_image.json | 0 fixtures/3/{ => invalid}/collection_of_canvases.json | 0 fixtures/3/{ => invalid}/non_cc_license.json | 0 fixtures/3/{ => invalid}/old_format_label.json | 0 fixtures/3/{ => invalid}/rights_lang_issues.json | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename fixtures/3/{ => invalid}/broken_choice.json (100%) rename fixtures/3/{ => invalid}/broken_collection.json (100%) rename fixtures/3/{ => invalid}/broken_embedded_annos.json (100%) rename fixtures/3/{ => invalid}/broken_service.json (100%) rename fixtures/3/{ => invalid}/broken_simple_image.json (100%) rename fixtures/3/{ => invalid}/collection_of_canvases.json (100%) rename fixtures/3/{ => invalid}/non_cc_license.json (100%) rename fixtures/3/{ => invalid}/old_format_label.json (100%) rename fixtures/3/{ => invalid}/rights_lang_issues.json (100%) diff --git a/fixtures/3/broken_choice.json b/fixtures/3/invalid/broken_choice.json similarity index 100% rename from fixtures/3/broken_choice.json rename to fixtures/3/invalid/broken_choice.json diff --git a/fixtures/3/broken_collection.json b/fixtures/3/invalid/broken_collection.json similarity index 100% rename from fixtures/3/broken_collection.json rename to fixtures/3/invalid/broken_collection.json diff --git a/fixtures/3/broken_embedded_annos.json b/fixtures/3/invalid/broken_embedded_annos.json similarity index 100% rename from fixtures/3/broken_embedded_annos.json rename to fixtures/3/invalid/broken_embedded_annos.json diff --git a/fixtures/3/broken_service.json b/fixtures/3/invalid/broken_service.json similarity index 100% rename from fixtures/3/broken_service.json rename to fixtures/3/invalid/broken_service.json diff --git a/fixtures/3/broken_simple_image.json b/fixtures/3/invalid/broken_simple_image.json similarity index 100% rename from fixtures/3/broken_simple_image.json rename to fixtures/3/invalid/broken_simple_image.json diff --git a/fixtures/3/collection_of_canvases.json b/fixtures/3/invalid/collection_of_canvases.json similarity index 100% rename from fixtures/3/collection_of_canvases.json rename to fixtures/3/invalid/collection_of_canvases.json diff --git a/fixtures/3/non_cc_license.json b/fixtures/3/invalid/non_cc_license.json similarity index 100% rename from fixtures/3/non_cc_license.json rename to fixtures/3/invalid/non_cc_license.json diff --git a/fixtures/3/old_format_label.json b/fixtures/3/invalid/old_format_label.json similarity index 100% rename from fixtures/3/old_format_label.json rename to fixtures/3/invalid/old_format_label.json diff --git a/fixtures/3/rights_lang_issues.json b/fixtures/3/invalid/rights_lang_issues.json similarity index 100% rename from fixtures/3/rights_lang_issues.json rename to fixtures/3/invalid/rights_lang_issues.json From 66db5364c2d295a0fdc98e8c58ae1a8e6e0a3f61 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Wed, 8 Mar 2023 00:18:15 +0000 Subject: [PATCH 15/23] Moving to if/then/else --- .../3/valid/0306-manifest-link-annos.json | 36 ++ .../3/valid/0306-manifest-link-manifest.json | 52 ++ schema/error_processor.py | 399 -------------- schema/iiif_3_0.json | 486 ++++++++++-------- schema/schemavalidator.py | 96 +--- tests/test_schema.py | 196 +++++++ tests/test_validator.py | 158 ------ 7 files changed, 559 insertions(+), 864 deletions(-) create mode 100644 fixtures/3/valid/0306-manifest-link-annos.json create mode 100644 fixtures/3/valid/0306-manifest-link-manifest.json delete mode 100755 schema/error_processor.py create mode 100644 tests/test_schema.py diff --git a/fixtures/3/valid/0306-manifest-link-annos.json b/fixtures/3/valid/0306-manifest-link-annos.json new file mode 100644 index 0000000..febb3ed --- /dev/null +++ b/fixtures/3/valid/0306-manifest-link-annos.json @@ -0,0 +1,36 @@ +{ + "@context": "http://iiif.io/api/presentation/3/context.json", + "id": "https://preview.iiif.io/cookbook/0306-linking-annotations-to-manifests/recipe/0306-linking-annotations-to-manifests/annotationpage.json", + "type": "AnnotationPage", + "items": [ + { + "id": "https://preview.iiif.io/cookbook/0306-linking-annotations-to-manifests/recipe/0306-linking-annotations-to-manifests/canvas-1/annopage-2/anno-1", + "type": "Annotation", + "motivation": "commenting", + "body": { + "type": "TextualBody", + "language": "de", + "format": "text/plain", + "value": "Der Gänseliesel-Brunnen" + }, + "target": { + "type": "SpecificResource", + "source": { + "id": "https://preview.iiif.io/cookbook/0306-linking-annotations-to-manifests/recipe/0306-linking-annotations-to-manifests/canvas-1", + "type": "Canvas", + "partOf": [ + { + "id": "https://preview.iiif.io/cookbook/0306-linking-annotations-to-manifests/recipe/0306-linking-annotations-to-manifests/annotationpage.json", + "type": "Manifest" + } + ] + }, + "selector": { + "type": "FragmentSelector", + "conformsTo": "http://www.w3.org/TR/media-frags/", + "value": "xywh=300,800,1200,1200" + } + } + } + ] +} \ No newline at end of file diff --git a/fixtures/3/valid/0306-manifest-link-manifest.json b/fixtures/3/valid/0306-manifest-link-manifest.json new file mode 100644 index 0000000..4996ed5 --- /dev/null +++ b/fixtures/3/valid/0306-manifest-link-manifest.json @@ -0,0 +1,52 @@ +{ + "@context": "http://iiif.io/api/presentation/3/context.json", + "id": "https://preview.iiif.io/cookbook/0306-linking-annotations-to-manifests/recipe/0306-linking-annotations-to-manifests/manifest.json", + "type": "Manifest", + "label": { + "en": [ + "Picture of Göttingen taken during the 2019 IIIF Conference" + ] + }, + "items": [ + { + "id": "https://preview.iiif.io/cookbook/0306-linking-annotations-to-manifests/recipe/0306-linking-annotations-to-manifests/canvas-1", + "type": "Canvas", + "height": 3024, + "width": 4032, + "items": [ + { + "id": "https://preview.iiif.io/cookbook/0306-linking-annotations-to-manifests/recipe/0306-linking-annotations-to-manifests/canvas-1/annopage-1", + "type": "AnnotationPage", + "items": [ + { + "id": "https://preview.iiif.io/cookbook/0306-linking-annotations-to-manifests/recipe/0306-linking-annotations-to-manifests/canvas-1/annopage-1/anno-1", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", + "type": "Image", + "format": "image/jpeg", + "height": 3024, + "width": 4032, + "service": [ + { + "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", + "profile": "level1", + "type": "ImageService3" + } + ] + }, + "target": "https://preview.iiif.io/cookbook/0306-linking-annotations-to-manifests/recipe/0306-linking-annotations-to-manifests/canvas-1" + } + ] + } + ], + "annotations": [ + { + "id": "https://preview.iiif.io/cookbook/0306-linking-annotations-to-manifests/recipe/0306-linking-annotations-to-manifests/annotationpage.json", + "type": "AnnotationPage" + } + ] + } + ] +} \ No newline at end of file diff --git a/schema/error_processor.py b/schema/error_processor.py deleted file mode 100755 index f2ad3a8..0000000 --- a/schema/error_processor.py +++ /dev/null @@ -1,399 +0,0 @@ -#!/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 another 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 again - #print ('Schema path: {} error path: {}'.format(err.absolute_schema_path, error_path)) - if err.absolute_schema_path[-1] == 'oneOf' and err.absolute_schema_path != error_path and 'rights' not in err.absolute_schema_path: - error_path.append(oneOfIndex) # this is is related to one of the original oneOfs at index oneOfIndex - error_path.extend(err.absolute_schema_path) # but we found another oneOf test at this location - result = (self.diagnoseWhichOneOf(error_path, IIIFJsonPath)) # so recursivly discovery real error - - if isinstance(result, ValidationError): - store_errs.append(result) - else: - store_errs.extend(result) - - #print ('would add: {} by addErrors is {}'.format(err.message, addErrors)) - else: - 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() - IIIFJsonPath.reverse() - for error in valid_errors: - error.absolute_schema_path.extendleft(error_path) - error.absolute_path.extendleft(IIIFJsonPath) - # print ('Err {}, path {}'.format(error.message, error.path)) - return valid_errors - 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: - try: - if isinstance(schemaEl[pathPart], dict) and "$ref" in schemaEl[pathPart]: - schemaEl = self.resolver.resolve(schemaEl[pathPart]['$ref'])[1] - else: - schemaEl = schemaEl[pathPart] - except KeyError as error: - # print (schemaEl) - raise KeyError - - 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 - - if isinstance(schemaEl, dict) and "$ref" in schemaEl: - #print ('Found ref, trying to resolve: {}'.format(schemaEl['$ref'])) - return self.parse(error_path, self.resolver.resolve(schemaEl['$ref'])[1], iiif_asset, IIIFJsonPath, parent, jsonPath) - - - #print ("Path: {} ".format(error_path)) - 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']) - elif 'const' in option: - value.append(option['const']) - #print ('Using values: {}'.format(value)) - elif 'anyOf' in schemaEl['type']: - value = [] - for option in schemaEl['type']['anyOf']: - if 'pattern' in option: - value.append(option['pattern']) - elif 'const' in option: - 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 'properties' in schemaEl 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 - #print (iiifPath) - indexDelta = 0 - for index in find(iiifPath, '_'): - index += indexDelta - #print ('Replacing {} with {}'.format(iiifPath[index], indexes[count])) - iiifPath = iiifPath[:index] + str(indexes[count]) + iiifPath[index + 1:] - # if you replace [_] with a number greater than 9 you are taking up two spaces in the - # string so the index in the for loop starts to be off by one. Calculating the delta - # sorts this out - if len(str(indexes[count])) > 1: - indexDelta += len(str(indexes[count])) -1 - count += 1 - - #print ('JsonPath: {} IIIF Path {} type: {}'.format(iiifPath, IIIFJsonPath, schemaType)) - path = parse(iiifPath) - results = path.find(iiif_asset) - #print ('Path: {} Results: '.format(path, results)) - if not results: - # type not found so return True as this maybe the correct error - return True - typeValue = results[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/iiif_3_0.json b/schema/iiif_3_0.json index ae50688..44804a8 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -86,7 +86,7 @@ "type": "object", "additionalProperties": true, "title": "Reference", - "description":"Id. type, label but not items are required", + "description":"Id. type but not items are required", "properties": { "id": { "$ref": "#/types/id" }, "label": {"$ref": "#/types/lngString" }, @@ -101,6 +101,45 @@ }, "required": ["id", "type"], "not": { "required": [ "items" ] } + }, + "context": { + "if": { + "type":"string" + }, + "then":{ + "type": "string", + "const": "http://iiif.io/api/presentation/3/context.json" + }, + "else": { + "type": "array", + "contains": { + "type":"string", + "const": "http://iiif.io/api/presentation/3/context.json" + }, + "items": { + "type": "string", + "format": "uri", + "pattern": "^http.*$" + } + } + }, + "annotationsLink": { + "type": "array", + "items": { + "if": { + "type": "object", + "properties": { + "items": {"type":"array"} + }, + "required": ["items"] + }, + "then": { + "$ref": "#/classes/annotationPage" + }, + "else": { + "$ref": "#/classes/annotationPageRef" + } + } } }, @@ -199,12 +238,7 @@ "type": "array", "items": { "$ref": "#/classes/resource" } }, - "annotations": { - "type": "array", - "items": { - "$ref": "#/classes/annotationPage" - } - } + "annotations": { "$ref": "#/types/annotationsLink" } }, "required": ["id", "type"] } @@ -227,30 +261,39 @@ "description": "must be an array and have an id and type or @id and @type", "type": "array", "items": { - "oneOf": [ - { - "allOf": [ - { "$ref": "#/types/class" }, - { - "type": "object", - "properties": { - "profile": { "type": "string" }, - "service": { "$ref": "#/classes/service" } - } - } - ] + "if": { + "type": "object", + "properties": { + "@id": { "type": "string" } }, - { - "type": "object", - "properties": { - "@id": { "$ref": "#/types/id" }, - "@type": { "type": "string" }, - "profile": { "type": "string" }, - "service": { "$ref": "#/classes/service" } - }, - "required": ["@id", "@type"] - } - ] + "required": ["@id"] + }, + "then": { + "title": "ServiceV2", + "description": "A version 2 IIIF service needs an @id and @type", + "type": "object", + "properties": { + "@id": { "$ref": "#/types/id" }, + "@type": { "type": "string" }, + "profile": { "type": "string" }, + "service": { "$ref": "#/classes/service" } + }, + "required": ["@id", "@type"] + }, + "else": { + "allOf": [ + { "$ref": "#/types/class" }, + { + "title": "ServiceV3", + "description": "A version 3 IIIF service needs an id and type", + "type": "object", + "properties": { + "profile": { "type": "string" }, + "service": { "$ref": "#/classes/service" } + } + } + ] + } } }, "rights": { @@ -416,6 +459,7 @@ "type": "object", "properties": { "type": { + "@context": { "$ref": "#/types/context" }, "type": "string", "pattern": "^Collection", "default": "Collection", @@ -454,15 +498,7 @@ ] } }, - "annotations": { - "type": "array", - "items": { - "oneOf": [ - { "$ref": "#/classes/annotationPage" }, - { "$ref": "#/classes/annotationPageRef"} - ] - } - } + "annotations": { "$ref": "#/types/annotationsLink" } }, "required": ["id", "type", "label"] } @@ -475,22 +511,7 @@ "type": "object", "additionalProperties": false, "properties": { - "@context": { - "oneOf": [ - { - "type": "array", - "items": { - "type": "string", - "format": "uri", - "pattern": "^http.*$" - } - }, - { - "type": "string", - "const": "http://iiif.io/api/presentation/3/context.json" - } - ] - }, + "@context": { "$ref": "#/types/context" }, "id": { "$ref": "#/types/id" }, "label": {"$ref": "#/types/lngString" }, "type": { @@ -532,15 +553,7 @@ "$ref": "#/classes/range" } }, - "annotations": { - "type": "array", - "items": { - "oneOf": [ - { "$ref": "#/classes/annotationPage" }, - { "$ref": "#/classes/annotationPageRef"} - ] - } - } + "annotations": { "$ref": "#/types/annotationsLink" } }, "required": ["id", "type", "label"] } @@ -663,15 +676,7 @@ "$ref": "#/classes/annotationPage" } }, - "annotations": { - "type": "array", - "items": { - "oneOf": [ - { "$ref": "#/classes/annotationPage" }, - { "$ref": "#/classes/annotationPageRef"} - ] - } - } + "annotations": { "$ref": "#/types/annotationsLink" } }, "required": ["items"], "anyOf":[ @@ -723,15 +728,7 @@ "$ref": "#/classes/annotationPage" } }, - "annotations": { - "type": "array", - "items": { - "oneOf": [ - { "$ref": "#/classes/annotationPage" }, - { "$ref": "#/classes/annotationPageRef"} - ] - } - } + "annotations": { "$ref": "#/types/annotationsLink" } }, "required": ["items"], "anyOf":[ @@ -784,15 +781,7 @@ "$ref": "#/classes/annotationPage" } }, - "annotations": { - "type": "array", - "items": { - "oneOf": [ - { "$ref": "#/classes/annotationPage" }, - { "$ref": "#/classes/annotationPageRef"} - ] - } - } + "annotations": { "$ref": "#/types/annotationsLink" } }, "required": ["items"], "anyOf":[ @@ -840,45 +829,36 @@ ] }, "annotationPage": { - "if": { - "type": "object", - "required": ["items"] - }, - "then": { - "allOf": [ - { "$ref": "#/types/class" }, - { - "type": "object", - "properties": { - "@context": {}, - "id": { "$ref": "#/types/id" }, - "type": { - "type": "string", - "pattern": "^AnnotationPage$", - "default": "AnnotationPage" - }, - "label": {"$ref": "#/types/lngString" }, - "service": { "$ref": "#/classes/service" }, - "rendering": { "$ref": "#/types/external" }, - "thumbnail": { - "type": "array", - "items": { "$ref": "#/classes/resource" } - }, + "allOf": [ + { "$ref": "#/types/class" }, + { + "type": "object", + "properties": { + "@context": { "$ref": "#/types/context" }, + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^AnnotationPage$", + "default": "AnnotationPage" + }, + "label": {"$ref": "#/types/lngString" }, + "service": { "$ref": "#/classes/service" }, + "rendering": { "$ref": "#/types/external" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "items": { + "type": "array", "items": { - "type": "array", - "items": { - "$ref": "#/classes/annotation" - } + "$ref": "#/classes/annotation" } - }, - "required": ["id", "type", "items"], - "additionalProperties": false - } - ] - }, - "else": { - "$ref": "#/classes/annotationPageRef" - } + } + }, + "required": ["id", "type", "items"], + "additionalProperties": false + } + ] }, "annotation": { "allOf": [ @@ -898,15 +878,18 @@ "items": { "$ref": "#/classes/resource" } }, "motivation": { - "oneOf": [ - { "type": "string" }, - { - "type": "array", - "items": { - "type": "string" - } + "if": { + "type": "array" + }, + "then": { + "type": "array", + "items": { + "type": "string" } - ] + }, + "else": { + "type": "string" + } }, "body": { "if": { @@ -950,23 +933,46 @@ ] }, "annoTarget": { - "oneOf": [ + "allOf":[ { - "type": "string", - "format": "uri", - "pattern": "^http.*$" + "if": { + "type": "string" + }, + "then": { + "type": "string", + "format": "uri", + "pattern": "^http.*$" + } }, { - "$ref": "#/classes/specificResource" + "if": { + "type": "object", + "properties": { + "type": {"const":"SpecificResource"} + } + }, + "then": { + "$ref": "#/classes/specificResource" + } }, { - "title": "Annotation target can also contain just scope and source", - "type": "object", - "properties": { - "source": { "$ref": "#/types/id" }, - "scope": { "$ref": "#/types/id" } - }, - "required": ["source", "scope"] + "if": { + "type": "object", + "properties": { + "source": { "type": "string"}, + "scope": { "type": "string"} + }, + "required": ["source", "scope"] + }, + "then": { + "title": "Annotation target can also contain just scope and source", + "type": "object", + "properties": { + "source": { "$ref": "#/types/id" }, + "scope": { "$ref": "#/types/id" } + }, + "required": ["source", "scope"] + } } ] }, @@ -982,59 +988,90 @@ "format": { "$ref": "#/types/format" }, "accessibility": { "type": "string"}, "source": { - "oneOf": [ - { "$ref": "#/types/id" }, - { "$ref": "#/types/class" } - ] + "if": { + "type":"string" + }, + "then": { + "$ref": "#/types/id" + }, + "else": { + "$ref": "#/types/class" + } }, "selector": { - "oneOf": [ - { "$ref": "#/classes/annoSelector" }, - { - "type": "array", - "items": { - "$ref": "#/classes/annoSelector" - } + "if": { + "type":"object" + }, + "then": { + "$ref": "#/classes/annoSelector" + }, + "else": { + "type": "array", + "items": { + "$ref": "#/classes/annoSelector" } - ] + } } }, "required": ["source", "selector"] }, "annoSelector": { - "oneOf": [ - { - "type": "string", - "format": "uri", - "pattern": "^http.*$" - }, + "allOf": [ { - "type": "object", - "properties": { - "type": { "type": "string" }, - "t": { "$ref": "#/types/duration" } + "if": { + "type":"string" }, - "required": ["type","t"] + "then": { + "type": "string", + "format": "uri", + "pattern": "^http.*$" + } }, { - "type": "object", - "properties": { - "type": { - "type": "string", - "pattern": "^FragmentSelector$", - "default": "FragmentSelector" + "if": { + "type":"object", + "properties": { + "type": { "type": "string" }, + "t": {} }, - "conformsTo": { - "type": "string", - "format": "uri", - "pattern": "^http.*$", - "default": "http://www.w3.org/TR/media-frags/" + "required": ["type","t"] + }, + "then": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "t": { "$ref": "#/types/duration" } }, - "value": { - "type:": "string" - } + "required": ["type","t"] + } + }, + { + "if": { + "type": "object", + "properties": { + "type": {"const":"FragmentSelector"} + } }, - "required": ["type","value"] + "then": { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^FragmentSelector$", + "default": "FragmentSelector" + }, + "conformsTo": { + "type": "string", + "format": "uri", + "pattern": "^http.*$", + "default": "http://www.w3.org/TR/media-frags/" + }, + "value": { + "type:": "string" + } + }, + "required": ["type","value"] + } } ] }, @@ -1054,15 +1091,7 @@ "service": { "$ref": "#/classes/service" }, "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, - "annotations": { - "type": "array", - "items": { - "oneOf": [ - { "$ref": "#/classes/annotationPage" }, - { "$ref": "#/classes/annotationPageRef"} - ] - } - }, + "annotations": { "$ref": "#/types/annotationsLink" }, "thumbnail": { "type": "array", "items": { "$ref": "#/classes/resource" } @@ -1070,30 +1099,49 @@ "items": { "type": "array", "items": { - "oneOf": [ - { "$ref": "#/classes/specificResource" }, - { - "allOf": [ - { "$ref": "#/types/class" }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "pattern": "^Canvas$", - "default": "Canvas" - }, - "items": { - "type": "array" - } - }, - "required": ["items"] + "if": { + "type":"object", + "properties": { + "type": { + "type": "string", + "const": "SpecificResource" + } + } + }, + "then": { + "$ref": "#/classes/specificResource" + }, + "else": { + "if": { + "type":"object", + "properties": { + "type": { + "type": "string", + "const": "Canvas" } - ] + } }, - { "$ref": "#/classes/range" }, - { "$ref": "#/classes/canvasRef" } - ] + "then": { + "if": { + "type":"object", + "properties": { + "items": { + "type": "array" + } + }, + "required": ["items"] + }, + "then": { + "$ref": "#/classes/canvas" + }, + "else": { + "$ref": "#/classes/canvasRef" + } + }, + "else": { + "$ref": "#/classes/range" + } + } } } }, @@ -1133,7 +1181,9 @@ "type": {"const":"AnnotationPage"} } }, - "$ref": "#/classes/annotationPage" + "then": { + "$ref": "#/classes/annotationPage" + } } ] } diff --git a/schema/schemavalidator.py b/schema/schemavalidator.py index 76f0699..5722437 100755 --- a/schema/schemavalidator.py +++ b/schema/schemavalidator.py @@ -5,7 +5,6 @@ import json 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 = '' @@ -19,7 +18,7 @@ def printPath(pathObj, fields): path += '/[{}]'.format(fields) return path -def validate(data, version, url): +def validate(data, version, url=None): if version == '3.0': with open('schema/iiif_3_0.json') as json_file: try: @@ -74,87 +73,6 @@ def validate(data, version, url): }) errorCount +=1 - if False: #errors: - print('Validation Failed') - - - if len(errors) == 1 and 'is not valid under any of the given schemas' in errors[0].message: - errors = errors[0].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': - try: - err = errorParser.diagnoseWhichOneOf(list(err.absolute_schema_path), list(err.absolute_path)) - except RecursionError as error: - print ('Failed to diagnose error due to recursion error. Schema: {} IIIF path: {}'.format(err.absolute_schema_path, err.absolute_path)) - - relevantErrors.append(err) - if isinstance(err, ValidationError): - relevantErrors.append(err) - else: - relevantErrors.extend(err) - #else: - # print ('Dismissing schema: {} path: {}'.format(err.absolute_schema_path, err.absolute_path)) - i += 1 - # Remove dupes - seen_titles = set() - errors = [] - for errorDup in relevantErrors: - errorPath = errorParser.pathToJsonPath(errorDup.path) - if errorPath not in seen_titles: - errors.append(errorDup) - seen_titles.add(errorPath) - 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 - - # Return: - # infojson = { - # 'okay': okay, - # 'warnings': warnings, - # 'error': str(err), - # 'url': url - # } - - okay = 0 - return { 'okay': okay, 'warnings': [], @@ -188,13 +106,13 @@ def json_path(absolute_path): for error in result['errorList']: 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 (" 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'])) + print (" Full: {}".format(error['error'])) diff --git a/tests/test_schema.py b/tests/test_schema.py new file mode 100644 index 0000000..6cd9415 --- /dev/null +++ b/tests/test_schema.py @@ -0,0 +1,196 @@ +"""Test the schema validator""" +import unittest +import json +import os, sys + +sys.path.append('.') +#sys.path.append(os.path.dirname(__file__)) + +from schema import schemavalidator + +class TestSchema(unittest.TestCase): + + def test_valid_manifests(self): + for root, subdirs, files in os.walk("fixtures/3/valid"): + for f in files: + if f.endswith(".json"): + filename = os.path.join(root, f) + + with open(filename, 'r') as fh: + print ('Testing: {}'.format(filename)) + data = fh.read() + j = schemavalidator.validate(data, '3.0') + + if j['okay'] != 1: + if 'errorList' in j: + self.printValidationerror(filename, j['errorList']) + else: + print ('Failed to find errors but manifest {} failed validation'.format(filename)) + print (j) + + self.assertEqual(j['okay'], 1, 'Expected manifest {} to pass validation but it failed'.format(filename)) + + def test_broken_image(self): + filename = 'fixtures/3/invalid/broken_simple_image.json' + response = self.validateFile(filename) + + errorPaths = [ + "/provider[0]/logo[0]/['id' is a required property]", + "/provider[0]/seeAlso[0]/['id' is a required property]", + # Canvas requires id: + "/items[0]/['id' is a required property]", + # IIIF Context missing + "/@context" + ] + + self.checkValidationErrors(filename, response, errorPaths) + + def test_broken_choice(self): + filename = 'fixtures/3/invalid/broken_choice.json' + response = self.validateFile(filename) + + # Missing height + errorPaths = [ "/items[0]/['height' is a dependency of 'width']" ] + + self.checkValidationErrors(filename, response, errorPaths) + + def test_broken_collection(self): + filename = 'fixtures/3/invalid/broken_collection.json' + response = self.validateFile(filename) + + # Collection requires label + errorPaths = [ "/['label' is a required property]" ] + + self.checkValidationErrors(filename, response, errorPaths) + + def test_broken_embedded_annos(self): + filename = 'fixtures/3/invalid/broken_embedded_annos.json' + response = self.validateFile(filename) + + # Expected annotations not allowed in AnnotationPage + errorPaths = [ '/items[0]/items[0]/' ] + + self.checkValidationErrors(filename, response, errorPaths) + + def test_broken_license(self): + filename = "fixtures/3/invalid/non_cc_license.json" + response = self.validateFile(filename) + + errorPaths = [ '/rights/' ] + + self.checkValidationErrors(filename, response, errorPaths) + + def test_collection_of_canvases(self): + filename = "fixtures/3/invalid/collection_of_canvases.json" + + response = self.validateFile(filename) + + errorPaths = [ + '/items[0]', # a canvas shouldn't be allowed in a collection + '/items[1]' # a range shouldn't be allowed in a collection + ] + + self.checkValidationErrors(filename, response, errorPaths) + + def test_broken_service(self): + filename = 'fixtures/3/invalid/broken_service.json' + response = self.validateFile(filename) + + errorPaths = [ + '/thumbnail[0]/service', # Service needs to be an array + '/items[0]/items[0]/items[0]/body/service' # service needs to be an array + ] + + self.checkValidationErrors(filename, response, errorPaths) + + def test_brokenLabel(self): + filename = 'fixtures/3/invalid/old_format_label.json' + response = self.validateFile(filename) + + errorPaths = [ + '/label', # label needs to be an object + '/label', # label needs to be an object + '/' # Not allowed to have extra props at root. For example sequences + ] + + self.checkValidationErrors(filename, response, errorPaths) + + def test_lang_rights(self): + filename = 'fixtures/3/invalid/rights_lang_issues.json' + response = self.validateFile(filename) + + errorPaths = [ + '/items[0]/label[0]', # Can't use @none as a language + '/items[0]/', # Canvas require items, (height and width) or (duration) or (height, width and duration) + '/items[0]/', + "/label/", # Can't use @none as a language + '/items[0]/items[0]/items[0]/body/label/', # Can't use @none as a language + '/rights', # Can't use https link to license + '/metadata[0]/label/', # Can't use @none as a language + '/metadata[0]/value/', # Can't use @none as a language + '/metadata[1]/label/', # Can't use @none as a language + '/metadata[1]/value/', # Can't use @none as a language + '/metadata[2]/label/', # Can't use @none as a language + '/metadata[2]/value/', # Can't use @none as a language + '/metadata[3]/label/', # Can't use @none as a language + '/metadata[3]/value/', # Can't use @none as a language + '/metadata[4]/label/', # Can't use @none as a language + '/metadata[4]/value/', # Can't use @none as a language + '/metadata[5]/label/', # Can't use @none as a language + '/metadata[5]/value/', # Can't use @none as a language + '/metadata[6]/label/', # Can't use @none as a language + '/metadata[6]/value/', # Can't use @none as a language + '/metadata[7]/label/', # Can't use @none as a language + '/metadata[7]/value/', # Can't use @none as a language + '/metadata[8]/label/', # Can't use @none as a language + '/metadata[8]/value/', # Can't use @none as a language + '/metadata[9]/label/', # Can't use @none as a language + '/metadata[9]/value/' # Can't use @none as a language + ] + + self.checkValidationErrors(filename, response, errorPaths) + + def validateFile(self, filename): + with open(filename, 'r') as fh: + print ('Testing: {}'.format(filename)) + data = fh.read() + return schemavalidator.validate(data, '3.0') + + def printValidationerror(self, filename, errors): + print ('Failed to validate: {}'.format(filename)) + errorCount = 1 + for err in errors: + print(err['title']) + print(err['detail']) + print('\n Path for error: {}'.format(err['path'])) + print('\n Context: {}'.format(err['context'])) + errorCount += 1 + + def checkValidationErrors(self, filename, response, errorPaths): + self.assertEqual(response['okay'], 0, 'Expected {} to fail validation but it past. Errors: {}'.format(filename, self.errorAsString(response['errorList']))) + self.assertEqual(len(response['errorList']), len(errorPaths), 'Expected {} validation errors but found {} for file {}\n{}'.format(len(errorPaths), len(response['errorList']), filename, self.errorAsString(response['errorList']))) + + 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 {}. Errors: {}'.format(error['path'], filename, self.errorAsString(response['errorList']))) + + def errorAsString(self, error): + if type(error) == list: + response = "\n\n" + for err in error: + response += self.errorAsString(err) + response += "\n****************************\n\n" + else: + response = error['title'] + response += '\n' + error['detail'] + response += '\nPath: ' + error['path'] + response += '\nContext: ' + str(error['context']) + + return response + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_validator.py b/tests/test_validator.py index aef1cf5..e832981 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -24,8 +24,6 @@ finally: fh.close() -from schema.error_processor import IIIFErrorParser - def read_fixture(fixture): """Read data from text fixture.""" with open(fixture, 'r') as fh: @@ -127,161 +125,5 @@ def test06_index_route(self): html = v.index_route() self.assertTrue(html.startswith('')) - def test07_check_manifest3(self): - v = val_mod.Validator() - # good manifests - for good in ['fixtures/3/simple_video.json', - 'fixtures/3/full_example.json', - 'fixtures/3/choice.json', - 'fixtures/3/collection.json', - 'fixtures/3/collection_of_collections.json', - 'fixtures/3/version2image.json', - 'fixtures/3/annoPage.json', - 'fixtures/3/anno_pointselector.json', - 'fixtures/3/annoPageMultipleMotivations.json', - 'fixtures/3/old_cc_license.json', - 'fixtures/3/rightsstatement_license.json', - 'fixtures/3/extension_anno.json', - 'fixtures/3/multi_bodies.json', - 'fixtures/3/publicdomain.json', - 'fixtures/3/navPlace.json', - 'fixtures/3/anno_source.json', - 'fixtures/3/range_range.json', - 'fixtures/3/accompanyingCanvas.json', - 'fixtures/3/placeholderCanvas.json' - ]: - with open(good, 'r') as fh: - print ('Testing: {}'.format(good)) - data = fh.read() - j = json.loads(v.check_manifest(data, '3.0')) - if j['okay'] != 1: - if 'errorList' in j: - self.printValidationerror(good, j['errorList']) - else: - print ('Failed to find errors but manifest {} failed validation'.format(good)) - print (j) - - self.assertEqual(j['okay'], 1, 'Expected manifest {} to pass validation but it failed'.format(good)) - - for bad_data in ['fixtures/3/broken_simple_image.json', - 'fixtures/3/broken_choice.json', - 'fixtures/3/broken_collection.json', - 'fixtures/3/broken_embedded_annos.json', - 'fixtures/3/non_cc_license.json', - 'fixtures/3/collection_of_canvases.json']: - with open(bad_data, 'r') as fh: - data = fh.read() - j = json.loads(v.check_manifest(data, '3.0')) - - if j['okay'] == 1: - print ("Expected {} to fail validation but it passed....".format(bad_data)) - - self.assertEqual(j['okay'], 0) - - def test_brokenImage(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) - - def test_brokenService(self): - v = val_mod.Validator() - filename = 'fixtures/3/broken_service.json' - errorPaths = [ - '/thumbnail[0]/service', - '/items[0]/items[0]/items[0]/body/' - ] - response = self.helperRunValidation(v, filename) - self.helperTestValidationErrors(filename, response, errorPaths) - - def test_brokenLabel(self): - v = val_mod.Validator() - filename = 'fixtures/3/old_format_label.json' - errorPaths = [ - '/label', - '/label', - '/' - ] - response = self.helperRunValidation(v, filename) - self.helperTestValidationErrors(filename, response, errorPaths) - - def test_lang_rights(self): - v = val_mod.Validator() - - filename = 'fixtures/3/rights_lang_issues.json' - errorPaths = [ - '/label', - '/items[0]/label[0]', - '/items[0]/', - '/items[0]/', - '/items[0]/items[0]/items[0]/body/label/', - '/rights', - '/metadata[0]/label/', - '/metadata[0]/value/', - '/metadata[1]/label/', - '/metadata[1]/value/', - '/metadata[2]/label/', - '/metadata[2]/value/', - '/metadata[3]/label/', - '/metadata[3]/value/', - '/metadata[4]/label/', - '/metadata[4]/value/', - '/metadata[5]/label/', - '/metadata[5]/value/', - '/metadata[6]/label/', - '/metadata[6]/value/', - '/metadata[7]/label/', - '/metadata[7]/value/', - '/metadata[8]/label/', - '/metadata[8]/value/', - '/metadata[9]/label/', - '/metadata[9]/value/' - ] - response = self.helperRunValidation(v, filename) - self.helperTestValidationErrors(filename, response, errorPaths) - - def formatErrors(self, errorList): - response = '' - for error in errorList: - response += "Title: {}\n".format(error['title']) - response += "Path: {}\n".format(error['path']) - response += "Context: {}\n".format(error['path']) - response += "****************************\n" - - return response - - 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 {}\n{}'.format(len(errorPaths), len(response['errorList']), filename, self.formatErrors(response['errorList']))) - - - 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)) - errorCount = 1 - for err in errors: - print(err['title']) - print(err['detail']) - print('\n Path for error: {}'.format(err['path'])) - print('\n Context: {}'.format(err['context'])) - errorCount += 1 - if __name__ == '__main__': unittest.main() From 18800bb7d970f8cd26ab874be3b42f637ebb1a4a Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 9 Mar 2023 12:41:26 +0000 Subject: [PATCH 16/23] Typing the point selector --- schema/iiif_3_0.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index 44804a8..8b3bc97 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -1031,15 +1031,17 @@ "if": { "type":"object", "properties": { - "type": { "type": "string" }, - "t": {} - }, - "required": ["type","t"] + "type": {"const":"PointSelector"} + } }, "then": { "type": "object", "properties": { - "type": { "type": "string" }, + "type": { + "type": "string", + "pattern": "^PointSelector$", + "default": "PointSelector" + }, "t": { "$ref": "#/types/duration" } }, "required": ["type","t"] From b897aa7335fb54b21cd5d8bfffbf4f8939d0f4fd Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 9 Mar 2023 00:57:50 +0000 Subject: [PATCH 17/23] Adding SVG selector --- schema/iiif_3_0.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index 8b3bc97..d70c624 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -1074,7 +1074,30 @@ }, "required": ["type","value"] } + }, + { + "if": { + "type": "object", + "properties": { + "type": {"const":"SvgSelector"} + } + }, + "then": { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^SvgSelector$", + "default": "SvgSelector" + }, + "value": { + "type:": "string" + } + }, + "required": ["type","value"] + } } + ] }, "range": { From 735d31cef0b7a53ee5a20d69aaebc9786c5053b7 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 9 Mar 2023 12:12:19 +0000 Subject: [PATCH 18/23] Adding ImageApiSelector --- schema/iiif_3_0.json | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index d70c624..b08badb 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -1003,19 +1003,19 @@ "type":"object" }, "then": { - "$ref": "#/classes/annoSelector" + "$ref": "#/classes/selector" }, "else": { "type": "array", "items": { - "$ref": "#/classes/annoSelector" + "$ref": "#/classes/selector" } } } }, "required": ["source", "selector"] }, - "annoSelector": { + "selector": { "allOf": [ { "if": { @@ -1096,8 +1096,31 @@ }, "required": ["type","value"] } + }, + { + "if": { + "type": "object", + "properties": { + "type": {"const":"ImageApiSelector"} + } + }, + "then": { + "type": "object", + "properties": { + "type": { + "type": "string", + "pattern": "^ImageApiSelector$", + "default": "ImageApiSelector" + }, + "region": { "type:": "string" }, + "size": { "type:": "string" }, + "rotation": { "type:": "string" }, + "quality": { "type:": "string" }, + "format": { "type:": "string" } + }, + "required": ["type"] + } } - ] }, "range": { From 9c074fcffe829982bbc2083d77c022e50d9a217d Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 9 Mar 2023 13:09:39 +0000 Subject: [PATCH 19/23] Adding specific resource to body --- schema/iiif_3_0.json | 110 +++++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 46 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index b08badb..b9a4f80 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -194,55 +194,73 @@ "required": ["type", "items"] }, "resource": { - "title": "ContentResource", - "if": { - "type": "object", - "properties": { - "type": { "const": "TextualBody" } - } - }, - "then": { - "title": "Annotation bodies which are TextualBody MUST have an type and value property.", - "type": "object", - "properties": { - "id": { "$ref": "#/types/id" }, - "type": { - "type": "string", - "pattern": "^TextualBody$", - "default": "TextualBody" + "allOf": [ + { + "title": "ContentResource", + "if": { + "type": "object", + "properties": { + "type": { "const": "TextualBody" } + } }, - "value": { "type": "string" }, - "format": { "$ref": "#/types/format" }, - "language": { "type": "string"} + "then": { + "title": "Annotation bodies which are TextualBody MUST have an type and value property.", + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^TextualBody$", + "default": "TextualBody" + }, + "value": { "type": "string" }, + "format": { "$ref": "#/types/format" }, + "language": { "type": "string"} + }, + "required": ["value", "type"] + } }, - "required": ["value", "type"] - }, - "else": { - "title": "ContentResource: (not a TextualBody): ", - "description": "Annotation bodies MUST have an id and type property.", - "type": "object", - "properties": { - "id": { "$ref": "#/types/id" }, - "type": { - "type": "string" - }, - "height": { "$ref": "#/types/dimension" }, - "width": { "$ref": "#/types/dimension" }, - "duration": { "$ref": "#/types/duration" }, - "language": { "type": "string"}, - "rendering": { "$ref": "#/types/external" }, - "service": { "$ref": "#/classes/service" }, - "format": { "$ref": "#/types/format" }, - "label": {"$ref": "#/types/lngString" }, - "thumbnail": { - "type": "array", - "items": { "$ref": "#/classes/resource" } + { + "title": "SpecificResource", + "if": { + "type":"object", + "properties": { + "type": { + "type": "string", + "const": "SpecificResource" + } + } }, - "annotations": { "$ref": "#/types/annotationsLink" } + "then": { + "$ref": "#/classes/specificResource" + } }, - "required": ["id", "type"] - } - + { + "title": "ContentResource: (not a TextualBody): ", + "description": "Annotation bodies MUST have an id and type property.", + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string" + }, + "height": { "$ref": "#/types/dimension" }, + "width": { "$ref": "#/types/dimension" }, + "duration": { "$ref": "#/types/duration" }, + "language": { "type": "string"}, + "rendering": { "$ref": "#/types/external" }, + "service": { "$ref": "#/classes/service" }, + "format": { "$ref": "#/types/format" }, + "label": {"$ref": "#/types/lngString" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "annotations": { "$ref": "#/types/annotationsLink" } + }, + "required": ["id", "type"] + } + ] }, "imgSvr": { "allOf": [ @@ -1013,7 +1031,7 @@ } } }, - "required": ["source", "selector"] + "required": ["source"] }, "selector": { "allOf": [ From 45bf97c84e2803a5a27a008009509be9b47de664 Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Thu, 9 Mar 2023 13:12:35 +0000 Subject: [PATCH 20/23] Moving to cascading if statements --- schema/iiif_3_0.json | 74 +++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index b9a4f80..407262b 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -194,48 +194,44 @@ "required": ["type", "items"] }, "resource": { - "allOf": [ - { - "title": "ContentResource", - "if": { - "type": "object", - "properties": { - "type": { "const": "TextualBody" } - } + "title": "ContentResource", + "if": { + "type": "object", + "properties": { + "type": { "const": "TextualBody" } + } + }, + "then": { + "title": "Annotation bodies which are TextualBody MUST have an type and value property.", + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^TextualBody$", + "default": "TextualBody" }, - "then": { - "title": "Annotation bodies which are TextualBody MUST have an type and value property.", - "type": "object", - "properties": { - "id": { "$ref": "#/types/id" }, - "type": { - "type": "string", - "pattern": "^TextualBody$", - "default": "TextualBody" - }, - "value": { "type": "string" }, - "format": { "$ref": "#/types/format" }, - "language": { "type": "string"} - }, - "required": ["value", "type"] - } + "value": { "type": "string" }, + "format": { "$ref": "#/types/format" }, + "language": { "type": "string"} }, - { + "required": ["value", "type"] + }, + "else": { + "if": { + "type":"object", + "properties": { + "type": { + "type": "string", + "const": "SpecificResource" + } + } + }, + "then": { "title": "SpecificResource", - "if": { - "type":"object", - "properties": { - "type": { - "type": "string", - "const": "SpecificResource" - } - } - }, - "then": { - "$ref": "#/classes/specificResource" - } + "$ref": "#/classes/specificResource" }, - { + "else": { "title": "ContentResource: (not a TextualBody): ", "description": "Annotation bodies MUST have an id and type property.", "type": "object", @@ -260,7 +256,7 @@ }, "required": ["id", "type"] } - ] + } }, "imgSvr": { "allOf": [ From 9e4c5ad3d793fe5f41db0d263ef1d9574bad4a4f Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Mon, 13 Mar 2023 23:17:05 +0000 Subject: [PATCH 21/23] Simplifying if statement --- schema/iiif_3_0.json | 187 +++++++++++++++++++++++++------------------ 1 file changed, 111 insertions(+), 76 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index 407262b..f8a3e80 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -195,68 +195,88 @@ }, "resource": { "title": "ContentResource", - "if": { - "type": "object", - "properties": { - "type": { "const": "TextualBody" } - } - }, - "then": { - "title": "Annotation bodies which are TextualBody MUST have an type and value property.", - "type": "object", - "properties": { - "id": { "$ref": "#/types/id" }, - "type": { - "type": "string", - "pattern": "^TextualBody$", - "default": "TextualBody" - }, - "value": { "type": "string" }, - "format": { "$ref": "#/types/format" }, - "language": { "type": "string"} - }, - "required": ["value", "type"] - }, - "else": { - "if": { - "type":"object", - "properties": { - "type": { - "type": "string", - "const": "SpecificResource" + "allOf":[ + { + "if": { + "type": "object", + "properties": { + "type": { "const": "TextualBody" } } - } + }, + "then": { + "title": "Annotation bodies which are TextualBody MUST have an type and value property.", + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string", + "pattern": "^TextualBody$", + "default": "TextualBody" + }, + "value": { "type": "string" }, + "format": { "$ref": "#/types/format" }, + "language": { "type": "string"} + }, + "required": ["value", "type"] + } }, - "then": { - "title": "SpecificResource", - "$ref": "#/classes/specificResource" + { + "if": { + "type":"object", + "properties": { + "type": { + "type": "string", + "const": "SpecificResource" + } + } + }, + "then": { + "title": "SpecificResource", + "$ref": "#/classes/specificResource" + } }, - "else": { - "title": "ContentResource: (not a TextualBody): ", - "description": "Annotation bodies MUST have an id and type property.", - "type": "object", - "properties": { - "id": { "$ref": "#/types/id" }, - "type": { - "type": "string" - }, - "height": { "$ref": "#/types/dimension" }, - "width": { "$ref": "#/types/dimension" }, - "duration": { "$ref": "#/types/duration" }, - "language": { "type": "string"}, - "rendering": { "$ref": "#/types/external" }, - "service": { "$ref": "#/classes/service" }, - "format": { "$ref": "#/types/format" }, - "label": {"$ref": "#/types/lngString" }, - "thumbnail": { - "type": "array", - "items": { "$ref": "#/classes/resource" } - }, - "annotations": { "$ref": "#/types/annotationsLink" } + { + "if": { + "type":"object", + "properties": { + "type": { + "type": "string", + "not":{ + "oneOf":[ + { "const": "SpecificResource"}, + { "const": "TextualBody" } + ] + } + } + } }, - "required": ["id", "type"] + "then": { + "title": "ContentResource: (not a TextualBody): ", + "description": "Annotation bodies MUST have an id and type property.", + "type": "object", + "properties": { + "id": { "$ref": "#/types/id" }, + "type": { + "type": "string" + }, + "height": { "$ref": "#/types/dimension" }, + "width": { "$ref": "#/types/dimension" }, + "duration": { "$ref": "#/types/duration" }, + "language": { "type": "string"}, + "rendering": { "$ref": "#/types/external" }, + "service": { "$ref": "#/classes/service" }, + "format": { "$ref": "#/types/format" }, + "label": {"$ref": "#/types/lngString" }, + "thumbnail": { + "type": "array", + "items": { "$ref": "#/classes/resource" } + }, + "annotations": { "$ref": "#/types/annotationsLink" } + }, + "required": ["id", "type"] + } } - } + ] }, "imgSvr": { "allOf": [ @@ -906,29 +926,44 @@ } }, "body": { - "if": { - "type": "object", - "properties": { - "type": { "const": "Choice" } - } - }, - "then": { - "$ref": "#/classes/choice" - }, - "else": { - "if": { - "type": "object" - }, - "then": { - "$ref": "#/classes/resource" + "allOf": [ + { + "if": { + "type": "object", + "properties": { + "type": { "const": "Choice" } + } + }, + "then": { + "$ref": "#/classes/choice" + } }, - "else": { - "type": "array", - "items": { + { + "if": { + "type": "object" + }, + "then": { "$ref": "#/classes/resource" + }, + "else": { + "type": "array", + "items": { + "$ref": "#/classes/resource" + } + } + }, + { + "if": { + "type": "array" + }, + "then": { + "type": "array", + "items": { + "$ref": "#/classes/resource" + } } } - } + ] }, "target": { "anyOf": [ From 07785f7be83395180a4ad12af582907815752fed Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Mon, 13 Mar 2023 23:29:04 +0000 Subject: [PATCH 22/23] Fixing name clash with other library --- tests/test_schema.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index 6cd9415..ccba6d6 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -3,8 +3,7 @@ import json import os, sys -sys.path.append('.') -#sys.path.append(os.path.dirname(__file__)) +sys.path.insert(0,'.') from schema import schemavalidator From b29df86aabc6b16a8fa6d296256b86d33271e2eb Mon Sep 17 00:00:00 2001 From: Glen Robson Date: Mon, 13 Mar 2023 23:29:21 +0000 Subject: [PATCH 23/23] refactoring if/then/else block --- schema/iiif_3_0.json | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/schema/iiif_3_0.json b/schema/iiif_3_0.json index f8a3e80..684197a 100644 --- a/schema/iiif_3_0.json +++ b/schema/iiif_3_0.json @@ -940,16 +940,13 @@ }, { "if": { - "type": "object" + "type": "object", + "properties": { + "type": {"not": { "const": "Choice" }} + } }, "then": { "$ref": "#/classes/resource" - }, - "else": { - "type": "array", - "items": { - "$ref": "#/classes/resource" - } } }, {