Skip to content

Commit

Permalink
feat(python): Added ValidationError.kind and `ValidationError.insta…
Browse files Browse the repository at this point in the history
…nce` attributes

Signed-off-by: Dmitry Dygalo <[email protected]>
  • Loading branch information
Stranger6667 committed Dec 23, 2024
1 parent b3d2d6c commit 2abb512
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 2 deletions.
1 change: 1 addition & 0 deletions crates/jsonschema-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Custom retrievers for external references. [#372](https://github.com/Stranger6667/jsonschema/issues/372)
- Added the `mask` argument to validators for hiding sensitive data in error messages. [#434](https://github.com/Stranger6667/jsonschema/issues/434)
- Added `ValidationError.kind` and `ValidationError.instance` attributes. [#650](https://github.com/Stranger6667/jsonschema/issues/650)

### Changed

Expand Down
1 change: 1 addition & 0 deletions crates/jsonschema-py/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ jsonschema = { path = "../jsonschema/" }
serde.workspace = true
serde_json.workspace = true
pyo3 = { version = "0.23.3", features = ["extension-module"] }
pythonize = "0.23"
pyo3-built = "0.5"
148 changes: 147 additions & 1 deletion crates/jsonschema-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ struct ValidationError {
schema_path: Py<PyList>,
#[pyo3(get)]
instance_path: Py<PyList>,
#[pyo3(get)]
kind: ValidationErrorKind,
#[pyo3(get)]
instance: PyObject,
}

#[pymethods]
Expand All @@ -49,12 +53,16 @@ impl ValidationError {
long_message: String,
schema_path: Py<PyList>,
instance_path: Py<PyList>,
kind: ValidationErrorKind,
instance: PyObject,
) -> Self {
ValidationError {
message,
verbose_message: long_message,
schema_path,
instance_path,
kind,
instance,
}
}
fn __str__(&self) -> String {
Expand All @@ -65,6 +73,134 @@ impl ValidationError {
}
}

#[pyclass(eq, eq_int)]
#[derive(Debug, PartialEq, Clone)]
enum ValidationErrorKind {
AdditionalItems,
AdditionalProperties,
AnyOf,
BacktrackLimitExceeded,
Constant,
Contains,
ContentEncoding,
ContentMediaType,
Custom,
Enum,
ExclusiveMaximum,
ExclusiveMinimum,
FalseSchema,
Format,
FromUtf8,
MaxItems,
Maximum,
MaxLength,
MaxProperties,
MinItems,
Minimum,
MinLength,
MinProperties,
MultipleOf,
Not,
OneOfMultipleValid,
OneOfNotValid,
Pattern,
PropertyNames,
Required,
Type,
UnevaluatedItems,
UnevaluatedProperties,
UniqueItems,
Referencing,
}

impl From<jsonschema::error::ValidationErrorKind> for ValidationErrorKind {
fn from(kind: jsonschema::error::ValidationErrorKind) -> Self {
match kind {
jsonschema::error::ValidationErrorKind::AdditionalItems { .. } => {
ValidationErrorKind::AdditionalItems
}
jsonschema::error::ValidationErrorKind::AdditionalProperties { .. } => {
ValidationErrorKind::AdditionalProperties
}
jsonschema::error::ValidationErrorKind::AnyOf => ValidationErrorKind::AnyOf,
jsonschema::error::ValidationErrorKind::BacktrackLimitExceeded { .. } => {
ValidationErrorKind::BacktrackLimitExceeded
}
jsonschema::error::ValidationErrorKind::Constant { .. } => {
ValidationErrorKind::Constant
}
jsonschema::error::ValidationErrorKind::Contains => ValidationErrorKind::Contains,
jsonschema::error::ValidationErrorKind::ContentEncoding { .. } => {
ValidationErrorKind::ContentEncoding
}
jsonschema::error::ValidationErrorKind::ContentMediaType { .. } => {
ValidationErrorKind::ContentMediaType
}
jsonschema::error::ValidationErrorKind::Custom { .. } => ValidationErrorKind::Custom,
jsonschema::error::ValidationErrorKind::Enum { .. } => ValidationErrorKind::Enum,
jsonschema::error::ValidationErrorKind::ExclusiveMaximum { .. } => {
ValidationErrorKind::ExclusiveMaximum
}
jsonschema::error::ValidationErrorKind::ExclusiveMinimum { .. } => {
ValidationErrorKind::ExclusiveMinimum
}
jsonschema::error::ValidationErrorKind::FalseSchema => ValidationErrorKind::FalseSchema,
jsonschema::error::ValidationErrorKind::Format { .. } => ValidationErrorKind::Format,
jsonschema::error::ValidationErrorKind::FromUtf8 { .. } => {
ValidationErrorKind::FromUtf8
}
jsonschema::error::ValidationErrorKind::MaxItems { .. } => {
ValidationErrorKind::MaxItems
}
jsonschema::error::ValidationErrorKind::Maximum { .. } => ValidationErrorKind::Maximum,
jsonschema::error::ValidationErrorKind::MaxLength { .. } => {
ValidationErrorKind::MaxLength
}
jsonschema::error::ValidationErrorKind::MaxProperties { .. } => {
ValidationErrorKind::MaxProperties
}
jsonschema::error::ValidationErrorKind::MinItems { .. } => {
ValidationErrorKind::MinItems
}
jsonschema::error::ValidationErrorKind::Minimum { .. } => ValidationErrorKind::Minimum,
jsonschema::error::ValidationErrorKind::MinLength { .. } => {
ValidationErrorKind::MinLength
}
jsonschema::error::ValidationErrorKind::MinProperties { .. } => {
ValidationErrorKind::MinProperties
}
jsonschema::error::ValidationErrorKind::MultipleOf { .. } => {
ValidationErrorKind::MultipleOf
}
jsonschema::error::ValidationErrorKind::Not { .. } => ValidationErrorKind::Not,
jsonschema::error::ValidationErrorKind::OneOfMultipleValid => {
ValidationErrorKind::OneOfMultipleValid
}
jsonschema::error::ValidationErrorKind::OneOfNotValid => {
ValidationErrorKind::OneOfNotValid
}
jsonschema::error::ValidationErrorKind::Pattern { .. } => ValidationErrorKind::Pattern,
jsonschema::error::ValidationErrorKind::PropertyNames { .. } => {
ValidationErrorKind::PropertyNames
}
jsonschema::error::ValidationErrorKind::Required { .. } => {
ValidationErrorKind::Required
}
jsonschema::error::ValidationErrorKind::Type { .. } => ValidationErrorKind::Type,
jsonschema::error::ValidationErrorKind::UnevaluatedItems { .. } => {
ValidationErrorKind::UnevaluatedItems
}
jsonschema::error::ValidationErrorKind::UnevaluatedProperties { .. } => {
ValidationErrorKind::UnevaluatedProperties
}
jsonschema::error::ValidationErrorKind::UniqueItems => ValidationErrorKind::UniqueItems,
jsonschema::error::ValidationErrorKind::Referencing(_) => {
ValidationErrorKind::Referencing
}
}
}
}

#[pyclass]
struct ValidationErrorIter {
iter: std::vec::IntoIter<PyErr>,
Expand Down Expand Up @@ -115,9 +251,18 @@ fn into_py_err(
.map(into_path)
.collect::<Result<Vec<_>, _>>()?;
let instance_path = PyList::new(py, elements)?.unbind();
let kind: ValidationErrorKind = error.kind.into();
let instance = pythonize::pythonize(py, error.instance.as_ref())?.unbind();
Ok(PyErr::from_type(
pyerror_type,
(message, verbose_message, schema_path, instance_path),
(
message,
verbose_message,
schema_path,
instance_path,
kind,
instance,
),
))
}

Expand Down Expand Up @@ -831,6 +976,7 @@ fn jsonschema_rs(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
module.add_class::<Draft201909Validator>()?;
module.add_class::<Draft202012Validator>()?;
module.add("ValidationError", py.get_type::<ValidationError>())?;
module.add("ValidationErrorKind", py.get_type::<ValidationErrorKind>())?;
module.add("Draft4", DRAFT4)?;
module.add("Draft6", DRAFT6)?;
module.add("Draft7", DRAFT7)?;
Expand Down
25 changes: 24 additions & 1 deletion crates/jsonschema-py/tests-py/test_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from jsonschema_rs import (
ValidationError,
ValidationErrorKind,
is_valid,
iter_errors,
validate,
Expand Down Expand Up @@ -73,8 +74,10 @@ def test_repr():
),
)
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") as exc:
func(2)
assert exc.value.kind == ValidationErrorKind.Minimum
assert exc.value.instance == 2


def test_from_str_error():
Expand Down Expand Up @@ -132,6 +135,8 @@ def test_paths():
assert exc.value.schema_path == ["prefixItems", 0, "type"]
assert exc.value.instance_path == [0]
assert exc.value.message == '1 is not of type "string"'
assert exc.value.kind == ValidationErrorKind.Type
assert exc.value.instance == 1


@given(minimum=st.integers().map(abs))
Expand Down Expand Up @@ -178,6 +183,24 @@ def test_error_message():
On instance["foo"]:
null"""
)
assert exc.kind == ValidationErrorKind.Type
assert exc.instance is None


def test_error_instance():
instance = {"a": [42]}
try:
validate({"type": "array"}, instance)
pytest.fail("Validation error should happen")
except ValidationError as exc:
assert exc.kind == ValidationErrorKind.Type
assert exc.instance == instance
try:
validate({"properties": {"a": {"type": "object"}}}, instance)
pytest.fail("Validation error should happen")
except ValidationError as exc:
assert exc.kind == ValidationErrorKind.Type
assert exc.instance == instance["a"]


SCHEMA = {"properties": {"foo": {"type": "integer"}, "bar": {"type": "string"}}}
Expand Down

0 comments on commit 2abb512

Please sign in to comment.