Skip to content

Commit 1e9fe6e

Browse files
committed
Integrating v4 schema support
1 parent 5f301d0 commit 1e9fe6e

7 files changed

Lines changed: 202 additions & 33 deletions

File tree

fixtures/4/bad/dup_id.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"@context": "http://iiif.io/api/presentation/4/context.json",
3+
"id": "https://iiif.io/api/presentation/4.0/example/03_canvas.json",
4+
"type": "Manifest",
5+
"label": {
6+
"en": [
7+
"Canvas and first annotation page have same id"
8+
]
9+
},
10+
"items": [
11+
{
12+
"id": "https://iiif.io/api/presentation/4.0/example/03_canvas/canvas/p1",
13+
"type": "Canvas",
14+
"height": 1800,
15+
"width": 1200,
16+
"items": [
17+
{
18+
"id": "https://iiif.io/api/presentation/4.0/example/03_canvas/canvas/p1",
19+
"type": "AnnotationPage",
20+
"items": [
21+
{
22+
"id": "https://iiif.io/api/presentation/4.0/example/03_canvas/annotation/p0001-image",
23+
"type": "Annotation",
24+
"motivation": [ "painting" ],
25+
"body": {
26+
"id": "http://iiif.io/api/presentation/2.1/example/fixtures/resources/page1-full.png",
27+
"type": "Image",
28+
"format": "image/png",
29+
"height": 1800,
30+
"width": 1200
31+
},
32+
"target": {
33+
"id": "https://iiif.io/api/presentation/4.0/example/03_canvas/canvas/p1",
34+
"type": "Canvas"
35+
}
36+
}
37+
]
38+
}
39+
]
40+
}
41+
]
42+
}

fixtures/4/ok/02_timeline.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"@context": "http://iiif.io/api/presentation/4/context.json",
3+
"id": "https://iiif.io/api/presentation/4.0/example/02_timeline.json",
4+
"type": "Manifest",
5+
"label": {
6+
"en": [
7+
"Simplest Audio Example (IIIF Presentation v4)"
8+
]
9+
},
10+
"items": [
11+
{
12+
"id": "https://iiif.io/api/presentation/4.0/example/02",
13+
"type": "Timeline",
14+
"duration": 1985.024,
15+
"items": [
16+
{
17+
"id": "https://iiif.io/api/presentation/4.0/example/02/page",
18+
"type": "AnnotationPage",
19+
"items": [
20+
{
21+
"id": "https://iiif.io/api/presentation/4.0/example/02/page/anno",
22+
"type": "Annotation",
23+
"motivation": ["painting"],
24+
"body": {
25+
"id": "https://fixtures.iiif.io/audio/indiana/mahler-symphony-3/CD1/medium/128Kbps.mp4",
26+
"type": "Audio",
27+
"format": "audio/mp4",
28+
"duration": 1985.024
29+
},
30+
"target": {
31+
"id": "https://iiif.io/api/presentation/4.0/example/02",
32+
"type": "Timeline"
33+
}
34+
}
35+
]
36+
}
37+
]
38+
}
39+
]
40+
}

presentation_validator/v3/schemavalidator.py

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,39 @@ def printPath(pathObj, fields):
2424
path += '/[{}]'.format(fields)
2525
return path
2626

27+
def create_snippet(data):
28+
# Take possibly a large JSON document and only show the fields at the current level
29+
for key in data:
30+
if isinstance(data[key], list):
31+
data[key] = '[ ... ]'
32+
elif isinstance(data[key], dict):
33+
data[key] = '{ ... }'
34+
35+
return data
36+
37+
def convertValidationError(err, errorCount, total):
38+
detail = ''
39+
if 'title' in err.schema:
40+
detail = err.schema['title']
41+
description = ''
42+
if 'description' in err.schema:
43+
detail += ' ' + err.schema['description']
44+
context = err.instance
45+
if isinstance(context, dict):
46+
for key in context:
47+
if isinstance(context[key], list):
48+
context[key] = '[ ... ]'
49+
elif isinstance(context[key], dict):
50+
context[key] = '{ ... }'
51+
52+
return ErrorDetail(
53+
f"Error {errorCount} of {total}.\n Message: {err.message}",
54+
detail,
55+
description,
56+
printPath(err.path, err.message),
57+
context,
58+
err)
59+
2760
def validate(data, version, url):
2861
if version == IIIFVersion.V3_0:
2962
with open(f'{SCHEMA_DIR}/iiif_3_0.json') as json_file:
@@ -90,31 +123,12 @@ def validate(data, version, url):
90123
if errorPath not in seen_titles:
91124
errors.append(errorDup)
92125
seen_titles.add(errorPath)
126+
93127
errorCount = 1
94128
# Now create some useful messsages to pass on
95129
for err in errors:
96-
detail = ''
97-
if 'title' in err.schema:
98-
detail = err.schema['title']
99-
description = ''
100-
if 'description' in err.schema:
101-
detail += ' ' + err.schema['description']
102-
context = err.instance
103-
if isinstance(context, dict):
104-
for key in context:
105-
if isinstance(context[key], list):
106-
context[key] = '[ ... ]'
107-
elif isinstance(context[key], dict):
108-
context[key] = '{ ... }'
109-
110-
result.errorList.append(ErrorDetail(
111-
'Error {} of {}.\n Message: {}'.format(errorCount, len(errors), err.message),
112-
detail,
113-
description,
114-
printPath(err.path, err.message),
115-
context,
116-
err))
117-
#print (json.dumps(err.instance, indent=4))
130+
result.errorList.append(convertValidationError(err, errorCount, len(errors)))
131+
118132
errorCount += 1
119133

120134
# Return:

presentation_validator/v4/unique_ids.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import sys
22
import json
3+
from presentation_validator.model import ErrorDetail
4+
from presentation_validator.v3.schemavalidator import create_snippet
35

46
ignore = ["target", "lookAt", "range","structures","first","last","start","source"]
57
# create a method where you pass in a manifest and it checks to see if the id is unique
@@ -8,13 +10,14 @@ def check(manifest):
810

911
duplicates = []
1012
ids = []
11-
print ("Looking at manifest")
1213
checkNode(manifest, ids, duplicates)
1314

1415
if len(duplicates) > 0:
15-
raise ValueError(f"Duplicate ids: {duplicates}")
16+
return duplicates
17+
else:
18+
return None
1619

17-
def checkNode(node, ids=[], duplicates=[]):
20+
def checkNode(node, ids=[], duplicates=[], path = ""):
1821
if type(node) != dict:
1922
return
2023

@@ -23,19 +26,28 @@ def checkNode(node, ids=[], duplicates=[]):
2326
if type(value) != str:
2427
raise ValueError(f"Id must be a string: {value}")
2528
if value in ids:
26-
duplicates.append(value)
29+
duplicates.append(ErrorDetail(
30+
f"Duplicate id found",
31+
"The id field must be unique",
32+
f"Duplicate id: {value}",
33+
path + "/" + key,
34+
create_snippet(node),
35+
None
36+
))
2737
ids.append(value)
2838
else:
2939
# Don't look further in fields that point to other resources
3040
if key in ignore:
3141
continue
3242

3343
if type(value) == list:
44+
count = 0
3445
for item in value:
35-
checkNode(item, ids, duplicates)
46+
checkNode(item, ids, duplicates, path + "/" + key + "[" + str(count) + "]")
47+
count += 1
3648

3749
elif type(value) != str:
38-
checkNode(value, ids, duplicates)
50+
checkNode(value, ids, duplicates, path + "/" + key)
3951

4052
def main():
4153
# pass in manifest by command line argument

presentation_validator/v4/validation4.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
from pathlib import Path
33
import json
44
import sys
5-
from unique_ids import check
5+
from presentation_validator.model import ValidationResult
6+
from presentation_validator.v3.schemavalidator import convertValidationError
7+
from presentation_validator.v4.unique_ids import check
68

79
from jsonschema import Draft202012Validator
10+
from jsonschema.exceptions import relevance
811
from referencing import Registry, Resource
912
from referencing.jsonschema import DRAFT202012
1013

@@ -37,11 +40,30 @@ def validate(instance):
3740
main = schemas[f"{BASE_URI}/main.json"]
3841
validator = Draft202012Validator(main, registry=registry)
3942

40-
validator.validate(instance)
43+
result = ValidationResult()
4144

42-
# check ids
43-
check(instance)
44-
print ("Validation successful")
45+
results = validator.iter_errors(instance)
46+
errors = sorted(results, key=relevance)
47+
48+
if errors:
49+
result.passed = False
50+
errorCount = 1
51+
# Now create some useful messsages to pass on
52+
for err in errors:
53+
result.errorList.append(convertValidationError(err, errorCount, len(errors)))
54+
55+
errorCount += 1
56+
else:
57+
result.passed = True
58+
59+
duplicate_ids = check(instance)
60+
if duplicate_ids:
61+
result.passed = False
62+
63+
# Add all of the examples of duplicated ids
64+
result.errorList.extend(duplicate_ids)
65+
66+
return result
4567

4668
def main():
4769
# Check if command line argument is provided

presentation_validator/validator.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from jsonschema.exceptions import ValidationError
33
from presentation_validator.model import ValidationResult, ErrorDetail
44
from presentation_validator.v3 import schemavalidator
5+
from presentation_validator.v4 import validation4
56
from presentation_validator.enum import IIIFVersion
67

78
import requests
@@ -67,6 +68,9 @@ def check_manifest(
6768
traceback.print_exc()
6869
result.passed = False
6970
result.error = f'Presentation Validator bug: "{e}". Please create a <a href="https://github.com/IIIF/presentation-validator/issues">Validator Issue</a>, including a link to the manifest.'
71+
72+
elif version == IIIFVersion.V4_0:
73+
result = validation4.validate(manifest)
7074
else:
7175
if isinstance(data, dict):
7276
data = json.dumps(data, indent=3)

tests/test_validator.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import unittest
33
from unittest.mock import Mock
44
import importlib
5+
from pathlib import Path
56

67
from bottle import Response, request, LocalRequest
78

@@ -108,6 +109,40 @@ def test_bad_manifests_v3(self):
108109

109110
self.assertFalse(result.passed)
110111

112+
def test_good_manifests_v4(self):
113+
base = Path("fixtures/4/ok")
114+
data = []
115+
for path in base.rglob("*.json"):
116+
with path.open("r", encoding="utf-8") as f:
117+
print ('Testing: {}'.format(path))
118+
data = json.load(f)
119+
120+
result = check_manifest(data, '4.0')
121+
if not result.passed:
122+
if 'errorList' in result.errorList:
123+
self.printValidationerror(path, result.errorList)
124+
else:
125+
print ('Failed to find errors but manifest {} failed validation'.format(path))
126+
print (json.dumps(result.json(), indent=2))
127+
128+
self.assertTrue(result.passed, 'Expected manifest {} to pass validation but it failed'.format(path))
129+
130+
def test_bad_manifests_v4(self):
131+
base = Path("fixtures/4/bad")
132+
data = []
133+
for path in base.rglob("*.json"):
134+
with path.open("r", encoding="utf-8") as f:
135+
print ('Testing: {}'.format(path))
136+
data = json.load(f)
137+
138+
result = check_manifest(data, '3.0')
139+
140+
if result.passed:
141+
print(f"Expected {path} to fail validation but it passed....")
142+
143+
self.assertFalse(result.passed)
144+
145+
111146
def printValidationerror(self, filename, errors):
112147
print('Failed to validate: {}'.format(filename))
113148

0 commit comments

Comments
 (0)