feat(python): A way to compile schemas from a string
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
This commit is contained in:
parent
0e150641e1
commit
a95a754496
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
default_language_version:
|
||||
python: python3.7
|
||||
python: python3.9
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
-----------
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
///
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue