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
|
- uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.9
|
||||||
|
|
||||||
- run: pip install pre-commit
|
- run: pip install pre-commit
|
||||||
- run: pre-commit run --all-files
|
- run: pre-commit run --all-files
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
default_language_version:
|
default_language_version:
|
||||||
python: python3.7
|
python: python3.9
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
|
|
@ -2,10 +2,19 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### Fixed
|
||||||
|
|
||||||
- Set `jsonschema_rs.JSONSchema.__module__` to `jsonschema_rs`.
|
- Set `jsonschema_rs.JSONSchema.__module__` to `jsonschema_rs`.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Minor performance improvements.
|
||||||
|
|
||||||
## [0.12.3] - 2021-10-22
|
## [0.12.3] - 2021-10-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -46,7 +46,14 @@ or:
|
||||||
validator = jsonschema_rs.JSONSchema({"minimum": 42})
|
validator = jsonschema_rs.JSONSchema({"minimum": 42})
|
||||||
validator.validate(41) # raises ValidationError
|
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
|
Performance
|
||||||
-----------
|
-----------
|
||||||
|
|
|
@ -18,8 +18,13 @@ def load_json(filename):
|
||||||
return json.load(fd)
|
return json.load(fd)
|
||||||
|
|
||||||
|
|
||||||
def load_from_benches(filename):
|
def load_json_str(filename):
|
||||||
return load_json(f"../../jsonschema/benches/data/{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")
|
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
|
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
|
# Small schemas
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,10 @@
|
||||||
|
|
||||||
use jsonschema::{paths::JSONPointer, Draft};
|
use jsonschema::{paths::JSONPointer, Draft};
|
||||||
use pyo3::{
|
use pyo3::{
|
||||||
exceptions,
|
exceptions::{self, PyValueError},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
types::{PyAny, PyList, PyType},
|
types::{PyAny, PyList, PyType},
|
||||||
wrap_pyfunction, PyIterProtocol, PyObjectProtocol,
|
wrap_pyfunction, AsPyPointer, PyIterProtocol, PyObjectProtocol,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod ser;
|
mod ser;
|
||||||
|
@ -348,6 +348,47 @@ impl JSONSchema {
|
||||||
Err(error) => Err(into_py_err(py, error)?),
|
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)
|
/// is_valid(instance)
|
||||||
///
|
///
|
||||||
|
|
|
@ -47,12 +47,24 @@ def test_repr():
|
||||||
assert repr(JSONSchema({"minimum": 5})) == '<JSONSchema: {"minimum":5}>'
|
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):
|
def test_validate(func):
|
||||||
with pytest.raises(ValidationError, match="2 is less than the minimum of 5"):
|
with pytest.raises(ValidationError, match="2 is less than the minimum of 5"):
|
||||||
func(2)
|
func(2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_str_error():
|
||||||
|
with pytest.raises(ValueError, match="Expected string, got int"):
|
||||||
|
JSONSchema.from_str(42)
|
||||||
|
|
||||||
|
|
||||||
def test_recursive_dict():
|
def test_recursive_dict():
|
||||||
instance = {}
|
instance = {}
|
||||||
instance["foo"] = instance
|
instance["foo"] = instance
|
||||||
|
|
Loading…
Reference in New Issue