Skip to content

Commit 3c4dbcf

Browse files
committed
Implement Data Validation Error Format
1 parent 8e3ff7d commit 3c4dbcf

File tree

7 files changed

+76
-10
lines changed

7 files changed

+76
-10
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ If the data directory contains a file `bartoc.json` with an array of JSKOS recor
182182

183183
There is a minimal HTML interface at root path (**GET /**) to try out the API. This is more useful than an interface generated automatically, for instance with Swagger. The API is not meant to be publically available (there is no authentification), so there is no need for an [OpenAPI](https://swagger.io/specification/) document anyway.
184184

185+
A HTTP 400 error with response body in [Data Validation Error Format](https://gbv.github.io/validation-error-format/) is returned if collection metadata or mapping source metadata does not conform to its corresponding JSON Schema.
186+
185187
### General endpoints
186188

187189
#### GET /status.json

app.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,18 @@ def init(**config):
4141
mappings = MappingRegistry(**app.config)
4242

4343

44-
@app.errorhandler(ValidationError)
45-
def handle_validation_error(e):
46-
return jsonify(error="ValidationError", message=str(e), code=400), 400
47-
48-
4944
@app.errorhandler(ApiError)
5045
def handle_error(e):
5146
return jsonify(e.to_dict()), type(e).code
5247

5348

49+
@app.errorhandler(ValidationError)
50+
def handle_error(e):
51+
e = e.to_dict()
52+
e["code"] = 400
53+
return jsonify(e), 400
54+
55+
5456
def route(method, path, fn):
5557
fn.__name__ = f'{method}-{path}'
5658
app.add_url_rule(path, methods=[method], view_func=fn)

lib/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from .collections import CollectionRegistry
22
from .terminologies import TerminologyRegistry
33
from .mappings import MappingRegistry
4-
from .errors import ApiError, NotFound, NotAllowed, ValidationError, ServerError
4+
from .errors import ApiError, NotFound, NotAllowed, ServerError
55
from .utils import read_json, write_json
66
from .rdf import TripleStore, triple_iterator
77
from .rdffilter import RDFFilter
8+
from .validate import validateJSON, ValidationError
89

910

1011
__all__ = [CollectionRegistry, TerminologyRegistry, triple_iterator,
1112
MappingRegistry, ApiError, NotFound, NotAllowed, read_json,
1213
write_json, ValidationError, ServerError, TripleStore,
13-
RDFFilter]
14+
RDFFilter, validateJSON]

lib/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from jsonschema import ValidationError # noqa
1+
from .validate import ValidationError # noqa
22

33

44
class ApiError(Exception):

lib/registry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from datetime import datetime
33
from shutil import copy, copyfileobj, rmtree
44
import urllib
5-
from jsonschema import validate
5+
from .validate import validateJSON
66
from .rdf import jsonld2nt, TripleStore
77
from .rdffilter import RDFFilter
88
from .log import Log
@@ -60,7 +60,7 @@ def validate(self, item, id=None):
6060
item["uri"] = self.prefix + id
6161
item["partOf"] = [self.graph]
6262
if self.schema:
63-
validate(instance=item, schema=self.schema)
63+
validateJSON(item, schema=self.schema)
6464
return item
6565

6666
def list(self):

lib/validate.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import jsonschema
2+
3+
# Data Validation Error Format
4+
5+
6+
class ValidationError(Exception):
7+
def __init__(self, message, position=None):
8+
super().__init__(message)
9+
self.position = position
10+
11+
def to_dict(self):
12+
e = {"message": str(self)}
13+
if self.position:
14+
e["position"] = self.position
15+
return e
16+
17+
18+
def validateJSON(data, schema):
19+
try:
20+
jsonschema.validate(data, schema)
21+
except jsonschema.ValidationError as err:
22+
pos = ""
23+
for elem in err.absolute_path:
24+
if isinstance(elem, int):
25+
pos += "/" + str(elem)
26+
else:
27+
pos += "/" + elem.replace("~", "~0").replace("/", "~1")
28+
pos = {"jsonpointer": pos}
29+
raise ValidationError(err.message, pos)

tests/test_validate.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import pytest
2+
3+
from lib import ValidationError, validateJSON
4+
5+
6+
def test_jsonschema():
7+
data = { # Example from JSON Pointer RFC
8+
"foo": ["bar"],
9+
"": 0,
10+
"a/b": 1,
11+
"c%d": 2,
12+
"e^\nf": 3,
13+
" ": 7,
14+
"m~n": 8
15+
}
16+
17+
def fail(prop, pos, check={"type": "string"}):
18+
try:
19+
schema = {"type": "object", "properties": {}}
20+
schema["properties"][prop] = check
21+
validateJSON(data, schema)
22+
assert prop == "ValidationError should have been thrown!"
23+
except ValidationError as e:
24+
assert e.position == {"jsonpointer": pos}
25+
26+
fail("foo", "/foo/0", {"type": "array", "items": {"type": "number"}})
27+
fail("", "/")
28+
fail("a/b", "/a~1b")
29+
fail("c%d", "/c%d")
30+
fail("e^\nf", "/e^\nf")
31+
fail(" ", "/ ")
32+
fail("m~n", "/m~0n")

0 commit comments

Comments
 (0)