feat(python): A way to compile schemas from a string

Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
This commit is contained in:
Dmitry Dygalo 2021-10-29 17:25:24 +02:00 committed by Dmitry Dygalo
parent 0e150641e1
commit a95a754496
7 changed files with 100 additions and 8 deletions

View File

@ -27,7 +27,7 @@ jobs:
- uses: actions/setup-python@v2
with:
python-version: 3.7
python-version: 3.9
- run: pip install pre-commit
- run: pre-commit run --all-files

View File

@ -1,5 +1,5 @@
default_language_version:
python: python3.7
python: python3.9
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks

View File

@ -2,10 +2,19 @@
## [Unreleased]
### Added
- `JSONSchema.from_str` method that accepts a string to construct a compiled schema.
Useful if you have a schema as string, because you don't have to call `json.loads` on your side - parsing will happen on the Rust side.
### Fixed
- Set `jsonschema_rs.JSONSchema.__module__` to `jsonschema_rs`.
### Performance
- Minor performance improvements.
## [0.12.3] - 2021-10-22
### Added

View File

@ -46,7 +46,14 @@ or:
validator = jsonschema_rs.JSONSchema({"minimum": 42})
validator.validate(41) # raises ValidationError
**NOTE**. This library is in early development.
If you have a schema as a JSON string, then you could use `jsonschema_rs.JSONSchema.from_str` to avoid parsing on the Python side:
.. code:: python
import jsonschema_rs
validator = jsonschema_rs.JSONSchema.from_str('{"minimum": 42}')
...
Performance
-----------

View File

@ -18,8 +18,13 @@ def load_json(filename):
return json.load(fd)
def load_from_benches(filename):
return load_json(f"../../jsonschema/benches/data/{filename}")
def load_json_str(filename):
with open(filename) as fd:
return fd.read()
def load_from_benches(filename, loader=load_json):
return loader(f"../../jsonschema/benches/data/{filename}")
OPENAPI = load_from_benches("openapi.json")
@ -79,6 +84,24 @@ def args(request, variant, is_compiled):
return partial(fastjsonschema.validate, use_default=False), schema, instance
@pytest.mark.parametrize(
"name", ("openapi.json", "swagger.json", "geojson.json", "citm_catalog_schema.json", "fast_schema.json")
)
@pytest.mark.parametrize(
"func",
(
lambda x: jsonschema_rs.JSONSchema(json.loads(x)),
jsonschema_rs.JSONSchema.from_str,
),
ids=["py-parse", "rs-parse"],
)
@pytest.mark.benchmark(group="create schema")
def test_create_schema(benchmark, func, name):
benchmark.group = f"{name}: {benchmark.group}"
schema = load_from_benches(name, loader=load_json_str)
benchmark(func, schema)
# Small schemas

View File

@ -16,10 +16,10 @@
use jsonschema::{paths::JSONPointer, Draft};
use pyo3::{
exceptions,
exceptions::{self, PyValueError},
prelude::*,
types::{PyAny, PyList, PyType},
wrap_pyfunction, PyIterProtocol, PyObjectProtocol,
wrap_pyfunction, AsPyPointer, PyIterProtocol, PyObjectProtocol,
};
mod ser;
@ -348,6 +348,47 @@ impl JSONSchema {
Err(error) => Err(into_py_err(py, error)?),
}
}
/// from_str(string, draft=None, with_meta_schemas=False)
///
/// Create `JSONSchema` from a serialized JSON string.
///
/// >>> compiled = JSONSchema.from_str('{"minimum": 5}')
///
/// Use it if you have your schema as a string and want to utilize Rust JSON parsing.
#[classmethod]
#[pyo3(text_signature = "(string, draft=None, with_meta_schemas=False)")]
fn from_str(
_: &PyType,
py: Python,
pyschema: &PyAny,
draft: Option<u8>,
with_meta_schemas: Option<bool>,
) -> PyResult<Self> {
let obj_ptr = pyschema.as_ptr();
let object_type = unsafe { pyo3::ffi::Py_TYPE(obj_ptr) };
if unsafe { object_type != types::STR_TYPE } {
let type_name =
unsafe { std::ffi::CStr::from_ptr((*object_type).tp_name).to_string_lossy() };
Err(PyValueError::new_err(format!(
"Expected string, got {}",
type_name
)))
} else {
let mut str_size: pyo3::ffi::Py_ssize_t = 0;
let uni = unsafe { string::read_utf8_from_str(obj_ptr, &mut str_size) };
let slice = unsafe { std::slice::from_raw_parts(uni, str_size as usize) };
let raw_schema = serde_json::from_slice(slice)
.map_err(|error| PyValueError::new_err(format!("Invalid string: {}", error)))?;
let options = make_options(draft, with_meta_schemas)?;
match options.compile(&raw_schema) {
Ok(schema) => Ok(JSONSchema {
schema,
repr: get_schema_repr(&raw_schema),
}),
Err(error) => Err(into_py_err(py, error)?),
}
}
}
/// is_valid(instance)
///

View File

@ -47,12 +47,24 @@ def test_repr():
assert repr(JSONSchema({"minimum": 5})) == '<JSONSchema: {"minimum":5}>'
@pytest.mark.parametrize("func", (JSONSchema({"minimum": 5}).validate, partial(validate, {"minimum": 5})))
@pytest.mark.parametrize(
"func",
(
JSONSchema({"minimum": 5}).validate,
JSONSchema.from_str('{"minimum": 5}').validate,
partial(validate, {"minimum": 5}),
),
)
def test_validate(func):
with pytest.raises(ValidationError, match="2 is less than the minimum of 5"):
func(2)
def test_from_str_error():
with pytest.raises(ValueError, match="Expected string, got int"):
JSONSchema.from_str(42)
def test_recursive_dict():
instance = {}
instance["foo"] = instance