Skip to content

Commit

Permalink
First cut at a Validator class
Browse files Browse the repository at this point in the history
This implements a very simple wrapper to the RapidJSON SchemaValidator()
facility, addressing issue #71.
  • Loading branch information
lelit committed Aug 17, 2017
1 parent fa05ee1 commit 2b06e64
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 4 deletions.
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
loads
encoder
decoder
validator

.. data:: __author__

Expand Down
37 changes: 37 additions & 0 deletions docs/validator.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
=================
Validator class
=================

.. module:: rapidjson

.. testsetup::

from rapidjson import Validator

.. class:: Validator(json_schema)

:param json_schema: the `JSON schema`__, specified as a ``str`` instance or an *UTF-8*
``bytes`` instance

__ http://json-schema.org/documentation.html

.. method:: __call__(json)

:param json: the ``JSON`` value, specified as a ``str`` instance or an *UTF-8*
``bytes`` instance, that will be validated

The given `json` value will be validated accordingly to the *schema*: a ``ValueError``
will be raised if the validation fails, and the exception will contain three arguments,
respectively the type of the error, the position in the schema and the position in the
``JSON`` document where the error occurred:

.. doctest::

>>> validate = Validator('{"required": ["a", "b"]}')
>>> validate('{"a": null, "b": 1}')
>>> try:
... validate('{"a": null, "c": false}')
... except ValueError as error:
... print(error.args)
...
('required', '#', '#')
199 changes: 195 additions & 4 deletions rapidjson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <vector>

#include "rapidjson/reader.h"
#include "rapidjson/schema.h"
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"
#include "rapidjson/prettywriter.h"
Expand Down Expand Up @@ -144,6 +145,11 @@ static PyObject* encoder_call(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* encoder_new(PyTypeObject* type, PyObject* args, PyObject* kwargs);


static PyObject* validator_call(PyObject* self, PyObject* args, PyObject* kwargs);
static void validator_dealloc(PyObject* self);
static PyObject* validator_new(PyTypeObject* type, PyObject* args, PyObject* kwargs);


/////////////
// Decoder //
/////////////
Expand Down Expand Up @@ -1101,8 +1107,7 @@ static PyMemberDef decoder_members[] = {
};


static
PyTypeObject Decoder_Type = {
static PyTypeObject Decoder_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"rapidjson.Decoder", /* tp_name */
sizeof(DecoderObject), /* tp_basicsize */
Expand Down Expand Up @@ -2175,8 +2180,7 @@ static PyMemberDef encoder_members[] = {
};


static
PyTypeObject Encoder_Type = {
static PyTypeObject Encoder_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"rapidjson.Encoder", /* tp_name */
sizeof(EncoderObject), /* tp_basicsize */
Expand Down Expand Up @@ -2395,6 +2399,187 @@ encoder_new(PyTypeObject* type, PyObject* args, PyObject* kwargs)
}


///////////////
// Validator //
///////////////


typedef struct {
PyObject_HEAD
SchemaDocument *schema;
} ValidatorObject;


PyDoc_STRVAR(validator_doc,
"Validator(json_schema)\n"
"\n"
"Create and return a new Validator instance from the given `json_schema`"
" string.");


static PyTypeObject Validator_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"rapidjson.Validator", /* tp_name */
sizeof(ValidatorObject), /* tp_basicsize */
0, /* tp_itemsize */
(destructor) validator_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_compare */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
(ternaryfunc) validator_call, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
validator_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
0, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
validator_new, /* tp_new */
PyObject_Del, /* tp_free */
};


static PyObject* validator_call(PyObject* self, PyObject* args, PyObject* kwargs)
{
PyObject* jsonObject;

if (!PyArg_ParseTuple(args, "O", &jsonObject))
return NULL;

const char* jsonStr;

if (PyBytes_Check(jsonObject)) {
jsonStr = PyBytes_AsString(jsonObject);
if (jsonStr == NULL)
return NULL;
}
else if (PyUnicode_Check(jsonObject)) {
jsonStr = PyUnicode_AsUTF8(jsonObject);
if (jsonStr == NULL)
return NULL;
}
else {
PyErr_SetString(PyExc_TypeError, "Expected string or utf-8 encoded bytes");
return NULL;
}

Document d;
bool error;

Py_BEGIN_ALLOW_THREADS
error = d.Parse(jsonStr).HasParseError();
Py_END_ALLOW_THREADS

if (error) {
PyErr_SetString(PyExc_ValueError, "Invalid JSON");
return NULL;
}

SchemaValidator validator(*((ValidatorObject*) self)->schema);
bool accept;

Py_BEGIN_ALLOW_THREADS
accept = d.Accept(validator);
Py_END_ALLOW_THREADS

if (!accept) {
StringBuffer sptr;
StringBuffer dptr;

Py_BEGIN_ALLOW_THREADS
validator.GetInvalidSchemaPointer().StringifyUriFragment(sptr);
validator.GetInvalidDocumentPointer().StringifyUriFragment(dptr);
Py_END_ALLOW_THREADS

PyErr_SetObject(PyExc_ValueError, Py_BuildValue("sss",
validator.GetInvalidSchemaKeyword(),
sptr.GetString(),
dptr.GetString()));
sptr.Clear();
dptr.Clear();

return NULL;
}

Py_RETURN_NONE;
}


static void validator_dealloc(PyObject* self)
{
ValidatorObject* s = (ValidatorObject*) self;
delete s->schema;
Py_TYPE(self)->tp_free(self);
}


static PyObject* validator_new(PyTypeObject* type, PyObject* args, PyObject* kwargs)
{
PyObject* jsonObject;

if (!PyArg_ParseTuple(args, "O", &jsonObject))
return NULL;

const char* jsonStr;

if (PyBytes_Check(jsonObject)) {
jsonStr = PyBytes_AsString(jsonObject);
if (jsonStr == NULL)
return NULL;
}
else if (PyUnicode_Check(jsonObject)) {
jsonStr = PyUnicode_AsUTF8(jsonObject);
if (jsonStr == NULL)
return NULL;
}
else {
PyErr_SetString(PyExc_TypeError, "Expected string or utf-8 encoded bytes");
return NULL;
}

Document d;
bool error;

Py_BEGIN_ALLOW_THREADS
error = d.Parse(jsonStr).HasParseError();
Py_END_ALLOW_THREADS

if (error) {
PyErr_SetString(PyExc_ValueError, "Invalid JSON");
return NULL;
}

ValidatorObject* v = (ValidatorObject*) type->tp_alloc(type, 0);
if (v == NULL)
return NULL;

v->schema = new SchemaDocument(d);

return (PyObject*) v;
}


////////////
// Module //
////////////
Expand Down Expand Up @@ -2432,6 +2617,9 @@ PyInit_rapidjson()
if (PyType_Ready(&Encoder_Type) < 0)
goto error;

if (PyType_Ready(&Validator_Type) < 0)
goto error;

PyDateTime_IMPORT;

datetimeModule = PyImport_ImportModule("datetime");
Expand Down Expand Up @@ -2565,6 +2753,9 @@ PyInit_rapidjson()
Py_INCREF(&Encoder_Type);
PyModule_AddObject(m, "Encoder", (PyObject*) &Encoder_Type);

Py_INCREF(&Validator_Type);
PyModule_AddObject(m, "Validator", (PyObject*) &Validator_Type);

return m;

error:
Expand Down
27 changes: 27 additions & 0 deletions tests/test_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pytest

import rapidjson as rj


@pytest.mark.parametrize('schema,json', (
('{ "type": ["number", "string"] }', '42'),
('{ "type": ["number", "string"] }', '"Life, the universe, and everything"'),
))
@pytest.mark.unit
def test_valid(schema, json):
validate = rj.Validator(schema)
validate(json)


@pytest.mark.parametrize('schema,json,details', (
('{ "type": ["number", "string"] }',
'["Life", "the universe", "and everything"]',
('type', '#', '#'),
),
))
@pytest.mark.unit
def test_invalid(schema, json, details):
validate = rj.Validator(schema)
with pytest.raises(ValueError) as error:
validate(json)
assert error.value.args == details

0 comments on commit 2b06e64

Please sign in to comment.