From 4806e5c8b4976347d0d50f4deb35f949245fdb0d Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Tue, 24 Dec 2024 13:29:42 +0100 Subject: [PATCH] feat(python): Extend ValidationErrorKind with error-specific context Signed-off-by: Dmitry Dygalo --- CHANGELOG.md | 4 + crates/jsonschema-py/CHANGELOG.md | 5 + crates/jsonschema-py/README.md | 27 +- .../python/jsonschema_rs/__init__.pyi | 129 +++++- crates/jsonschema-py/src/lib.rs | 371 ++++++++++++------ .../jsonschema-py/tests-py/test_jsonschema.py | 49 ++- crates/jsonschema/src/primitive_type.rs | 6 + 7 files changed, 461 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28613901..e0cf9eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Implement `ExactSizeIterator` for `PrimitiveTypesBitMapIterator`. + ## [0.27.0] - 2024-12-23 ### Added diff --git a/crates/jsonschema-py/CHANGELOG.md b/crates/jsonschema-py/CHANGELOG.md index 0cf14ca9..c5132cb2 100644 --- a/crates/jsonschema-py/CHANGELOG.md +++ b/crates/jsonschema-py/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Added + +- Extend `ValidationErrorKind` with error-specific context data. +- Missing type annotations for `retriever` & `mask` arguments. + ## [0.27.0] - 2024-12-23 ### Added diff --git a/crates/jsonschema-py/README.md b/crates/jsonschema-py/README.md index aa1a5bc9..229a2fd1 100644 --- a/crates/jsonschema-py/README.md +++ b/crates/jsonschema-py/README.md @@ -197,7 +197,32 @@ validator.is_valid({ }) # False ``` -## Error Message Masking +## Error Handling + +`jsonschema-rs` provides detailed validation errors through the `ValidationError` class, which includes both basic error information and specific details about what caused the validation to fail: + +```python +import jsonschema_rs + +schema = {"type": "string", "maxLength": 5} + +try: + jsonschema_rs.validate(schema, "too long") +except jsonschema_rs.ValidationError as error: + # Basic error information + print(error.message) # '"too long" is longer than 5 characters' + print(error.instance_path) # Location in the instance that failed + print(error.schema_path) # Location in the schema that failed + + # Detailed error information via `kind` + if isinstance(error.kind, jsonschema_rs.ValidationErrorKind.MaxLength): + assert error.kind.limit == 5 + print(f"Exceeded maximum length of {error.kind.limit}") +``` + +For a complete list of all error kinds and their attributes, see the [type definitions file](https://github.com/Stranger6667/jsonschema/blob/master/crates/jsonschema-py/python/jsonschema_rs/__init__.pyi) + +### Error Message Masking When working with sensitive data, you might want to hide actual values from error messages. You can mask instance values in error messages by providing a placeholder: diff --git a/crates/jsonschema-py/python/jsonschema_rs/__init__.pyi b/crates/jsonschema-py/python/jsonschema_rs/__init__.pyi index 37d8ebc3..ccc7d5a9 100644 --- a/crates/jsonschema-py/python/jsonschema_rs/__init__.pyi +++ b/crates/jsonschema-py/python/jsonschema_rs/__init__.pyi @@ -1,8 +1,13 @@ from collections.abc import Iterator -from typing import Any, Callable, TypeVar +from typing import Any, Callable, Protocol, TypeAlias, TypeVar _SchemaT = TypeVar("_SchemaT", bool, dict[str, Any]) _FormatFunc = TypeVar("_FormatFunc", bound=Callable[[str], bool]) +JSONType: TypeAlias = dict[str, Any] | list | str | int | float | bool | None +JSONPrimitive: TypeAlias = str | int | float | bool | None + +class RetrieverProtocol(Protocol): + def __call__(self, uri: str) -> JSONType: ... def is_valid( schema: _SchemaT, @@ -12,6 +17,8 @@ def is_valid( formats: dict[str, _FormatFunc] | None = None, validate_formats: bool | None = None, ignore_unknown_formats: bool = True, + retriever: RetrieverProtocol | None = None, + mask: str | None = None, ) -> bool: ... def validate( schema: _SchemaT, @@ -21,6 +28,8 @@ def validate( formats: dict[str, _FormatFunc] | None = None, validate_formats: bool | None = None, ignore_unknown_formats: bool = True, + retriever: RetrieverProtocol | None = None, + mask: str | None = None, ) -> None: ... def iter_errors( schema: _SchemaT, @@ -30,16 +39,118 @@ def iter_errors( formats: dict[str, _FormatFunc] | None = None, validate_formats: bool | None = None, ignore_unknown_formats: bool = True, + retriever: RetrieverProtocol | None = None, + mask: str | None = None, ) -> Iterator[ValidationError]: ... -class ValidationErrorKind: ... +class ReferencingError: + message: str + +class ValidationErrorKind: + class AdditionalItems: + limit: int + + class AdditionalProperties: + unexpected: list[str] + + class AnyOf: ... + + class BacktrackLimitExceeded: + error: str + + class Constant: + expected_value: JSONType + + class Contains: ... + + class ContentEncoding: + content_encoding: str + + class ContentMediaType: + content_media_type: str + + class Custom: + message: str + + class Enum: + options: list[JSONType] + + class ExclusiveMaximum: + limit: JSONPrimitive + + class ExclusiveMinimum: + limit: JSONPrimitive + + class FalseSchema: ... + + class Format: + format: str + + class FromUtf8: + error: str + + class MaxItems: + limit: int + + class Maximum: + limit: JSONPrimitive + + class MaxLength: + limit: int + + class MaxProperties: + limit: int + + class MinItems: + limit: int + + class Minimum: + limit: JSONPrimitive + + class MinLength: + limit: int + + class MinProperties: + limit: int + + class MultipleOf: + multiple_of: float + + class Not: + schema: JSONType + + class OneOfMultipleValid: ... + class OneOfNotValid: ... + + class Pattern: + pattern: str + + class PropertyNames: + error: "ValidationError" + + class Required: + property: str + + class Type: + types: list[str] + + class UnevaluatedItems: + unexpected: list[int] + + class UnevaluatedProperties: + unexpected: list[str] + + class UniqueItems: ... + + class Referencing: + error: ReferencingError class ValidationError(ValueError): message: str schema_path: list[str | int] instance_path: list[str | int] kind: ValidationErrorKind - instance: list | dict | str | int | float | bool | None + instance: JSONType Draft4: int Draft6: int @@ -54,6 +165,8 @@ class Draft4Validator: formats: dict[str, _FormatFunc] | None = None, validate_formats: bool | None = None, ignore_unknown_formats: bool = True, + retriever: RetrieverProtocol | None = None, + mask: str | None = None, ) -> None: ... def is_valid(self, instance: Any) -> bool: ... def validate(self, instance: Any) -> None: ... @@ -66,6 +179,8 @@ class Draft6Validator: formats: dict[str, _FormatFunc] | None = None, validate_formats: bool | None = None, ignore_unknown_formats: bool = True, + retriever: RetrieverProtocol | None = None, + mask: str | None = None, ) -> None: ... def is_valid(self, instance: Any) -> bool: ... def validate(self, instance: Any) -> None: ... @@ -78,6 +193,8 @@ class Draft7Validator: formats: dict[str, _FormatFunc] | None = None, validate_formats: bool | None = None, ignore_unknown_formats: bool = True, + retriever: RetrieverProtocol | None = None, + mask: str | None = None, ) -> None: ... def is_valid(self, instance: Any) -> bool: ... def validate(self, instance: Any) -> None: ... @@ -90,6 +207,8 @@ class Draft201909Validator: formats: dict[str, _FormatFunc] | None = None, validate_formats: bool | None = None, ignore_unknown_formats: bool = True, + retriever: RetrieverProtocol | None = None, + mask: str | None = None, ) -> None: ... def is_valid(self, instance: Any) -> bool: ... def validate(self, instance: Any) -> None: ... @@ -102,6 +221,8 @@ class Draft202012Validator: formats: dict[str, _FormatFunc] | None = None, validate_formats: bool | None = None, ignore_unknown_formats: bool = True, + retriever: RetrieverProtocol | None = None, + mask: str | None = None, ) -> None: ... def is_valid(self, instance: Any) -> bool: ... def validate(self, instance: Any) -> None: ... @@ -112,4 +233,6 @@ def validator_for( formats: dict[str, _FormatFunc] | None = None, validate_formats: bool | None = None, ignore_unknown_formats: bool = True, + retriever: RetrieverProtocol | None = None, + mask: str | None = None, ) -> Draft4Validator | Draft6Validator | Draft7Validator | Draft201909Validator | Draft202012Validator: ... diff --git a/crates/jsonschema-py/src/lib.rs b/crates/jsonschema-py/src/lib.rs index 7da0f24e..c0c9ec5b 100644 --- a/crates/jsonschema-py/src/lib.rs +++ b/crates/jsonschema-py/src/lib.rs @@ -40,7 +40,7 @@ struct ValidationError { #[pyo3(get)] instance_path: Py, #[pyo3(get)] - kind: ValidationErrorKind, + kind: Py, #[pyo3(get)] instance: PyObject, } @@ -53,7 +53,7 @@ impl ValidationError { long_message: String, schema_path: Py, instance_path: Py, - kind: ValidationErrorKind, + kind: Py, instance: PyObject, ) -> Self { ValidationError { @@ -73,131 +73,235 @@ impl ValidationError { } } -#[pyclass(eq, eq_int)] -#[derive(Debug, PartialEq, Clone)] +/// Errors that can occur during reference resolution and resource handling. +#[pyclass(extends=exceptions::PyException, module="jsonschema_rs")] +#[derive(Debug, Clone, PartialEq)] +struct ReferencingError { + message: String, +} + +#[pymethods] +impl ReferencingError { + #[new] + fn new(message: String) -> Self { + ReferencingError { message } + } + fn __str__(&self) -> String { + self.message.clone() + } + fn __repr__(&self) -> String { + format!("", self.message) + } +} + +/// Type of validation failure with its contextual data. +#[pyclass] +#[derive(Debug)] 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 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, + AdditionalItems { limit: usize }, + AdditionalProperties { unexpected: Py }, + AnyOf {}, + BacktrackLimitExceeded { error: String }, + Constant { expected_value: PyObject }, + Contains {}, + ContentEncoding { content_encoding: String }, + ContentMediaType { content_media_type: String }, + Custom { message: String }, + Enum { options: PyObject }, + ExclusiveMaximum { limit: PyObject }, + ExclusiveMinimum { limit: PyObject }, + FalseSchema {}, + Format { format: String }, + FromUtf8 { error: String }, + MaxItems { limit: u64 }, + Maximum { limit: PyObject }, + MaxLength { limit: u64 }, + MaxProperties { limit: u64 }, + MinItems { limit: u64 }, + Minimum { limit: PyObject }, + MinLength { limit: u64 }, + MinProperties { limit: u64 }, + MultipleOf { multiple_of: f64 }, + Not { schema: PyObject }, + OneOfMultipleValid {}, + OneOfNotValid {}, + Pattern { pattern: String }, + PropertyNames { error: Py }, + Required { property: PyObject }, + Type { types: Py }, + UnevaluatedItems { unexpected: Py }, + UnevaluatedProperties { unexpected: Py }, + UniqueItems {}, + Referencing { error: Py }, +} + +impl ValidationErrorKind { + fn try_new( + py: Python<'_>, + kind: jsonschema::error::ValidationErrorKind, + mask: Option<&str>, + ) -> PyResult { + Ok(match kind { + jsonschema::error::ValidationErrorKind::AdditionalItems { limit } => { + ValidationErrorKind::AdditionalItems { limit } + } + jsonschema::error::ValidationErrorKind::AdditionalProperties { unexpected } => { + ValidationErrorKind::AdditionalProperties { + unexpected: PyList::new(py, unexpected)?.unbind(), + } + } + jsonschema::error::ValidationErrorKind::AnyOf => ValidationErrorKind::AnyOf {}, + jsonschema::error::ValidationErrorKind::BacktrackLimitExceeded { error } => { + ValidationErrorKind::BacktrackLimitExceeded { + error: error.to_string(), + } + } + jsonschema::error::ValidationErrorKind::Constant { expected_value } => { + ValidationErrorKind::Constant { + expected_value: pythonize::pythonize(py, &expected_value)?.unbind(), + } + } + jsonschema::error::ValidationErrorKind::Contains => ValidationErrorKind::Contains {}, + jsonschema::error::ValidationErrorKind::ContentEncoding { content_encoding } => { + ValidationErrorKind::ContentEncoding { content_encoding } + } + jsonschema::error::ValidationErrorKind::ContentMediaType { content_media_type } => { + ValidationErrorKind::ContentMediaType { content_media_type } + } + jsonschema::error::ValidationErrorKind::Custom { message } => { + ValidationErrorKind::Custom { message } + } + jsonschema::error::ValidationErrorKind::Enum { options } => ValidationErrorKind::Enum { + options: pythonize::pythonize(py, &options)?.unbind(), + }, + jsonschema::error::ValidationErrorKind::ExclusiveMaximum { limit } => { + ValidationErrorKind::ExclusiveMaximum { + limit: pythonize::pythonize(py, &limit)?.unbind(), + } + } + jsonschema::error::ValidationErrorKind::ExclusiveMinimum { limit } => { + ValidationErrorKind::ExclusiveMinimum { + limit: pythonize::pythonize(py, &limit)?.unbind(), + } + } + jsonschema::error::ValidationErrorKind::FalseSchema => { + ValidationErrorKind::FalseSchema {} + } + jsonschema::error::ValidationErrorKind::Format { format } => { + ValidationErrorKind::Format { format } + } + jsonschema::error::ValidationErrorKind::FromUtf8 { error } => { + ValidationErrorKind::FromUtf8 { + error: error.to_string(), + } + } + jsonschema::error::ValidationErrorKind::MaxItems { limit } => { + ValidationErrorKind::MaxItems { limit } + } + jsonschema::error::ValidationErrorKind::Maximum { limit } => { + ValidationErrorKind::Maximum { + limit: pythonize::pythonize(py, &limit)?.unbind(), + } + } + jsonschema::error::ValidationErrorKind::MaxLength { limit } => { + ValidationErrorKind::MaxLength { limit } + } + jsonschema::error::ValidationErrorKind::MaxProperties { limit } => { + ValidationErrorKind::MaxProperties { limit } + } + jsonschema::error::ValidationErrorKind::MinItems { limit } => { + ValidationErrorKind::MinItems { limit } + } + jsonschema::error::ValidationErrorKind::Minimum { limit } => { + ValidationErrorKind::Minimum { + limit: pythonize::pythonize(py, &limit)?.unbind(), + } + } + jsonschema::error::ValidationErrorKind::MinLength { limit } => { + ValidationErrorKind::MinLength { limit } + } + jsonschema::error::ValidationErrorKind::MinProperties { limit } => { + ValidationErrorKind::MinProperties { limit } + } + jsonschema::error::ValidationErrorKind::MultipleOf { multiple_of } => { + ValidationErrorKind::MultipleOf { multiple_of } + } + jsonschema::error::ValidationErrorKind::Not { schema } => ValidationErrorKind::Not { + schema: pythonize::pythonize(py, &schema)?.unbind(), + }, jsonschema::error::ValidationErrorKind::OneOfMultipleValid => { - ValidationErrorKind::OneOfMultipleValid + ValidationErrorKind::OneOfMultipleValid {} } jsonschema::error::ValidationErrorKind::OneOfNotValid => { - ValidationErrorKind::OneOfNotValid + ValidationErrorKind::OneOfNotValid {} } - jsonschema::error::ValidationErrorKind::Pattern { .. } => ValidationErrorKind::Pattern, - jsonschema::error::ValidationErrorKind::PropertyNames { .. } => { - ValidationErrorKind::PropertyNames + jsonschema::error::ValidationErrorKind::Pattern { pattern } => { + ValidationErrorKind::Pattern { pattern } } - jsonschema::error::ValidationErrorKind::Required { .. } => { - ValidationErrorKind::Required + jsonschema::error::ValidationErrorKind::PropertyNames { error } => { + ValidationErrorKind::PropertyNames { + error: { + let (message, verbose_message, schema_path, instance_path, kind, instance) = + into_validation_error_args(py, *error, mask)?; + Py::new( + py, + ValidationError { + message, + verbose_message, + schema_path, + instance_path, + kind: Py::new(py, kind)?, + instance, + }, + )? + }, + } } - jsonschema::error::ValidationErrorKind::Type { .. } => ValidationErrorKind::Type, - jsonschema::error::ValidationErrorKind::UnevaluatedItems { .. } => { - ValidationErrorKind::UnevaluatedItems + jsonschema::error::ValidationErrorKind::Required { property } => { + ValidationErrorKind::Required { + property: pythonize::pythonize(py, &property)?.unbind(), + } } - jsonschema::error::ValidationErrorKind::UnevaluatedProperties { .. } => { - ValidationErrorKind::UnevaluatedProperties + jsonschema::error::ValidationErrorKind::Type { kind } => ValidationErrorKind::Type { + types: { + match kind { + jsonschema::error::TypeKind::Single(primitive_type) => { + PyList::new(py, [primitive_type.to_string()].iter())?.unbind() + } + jsonschema::error::TypeKind::Multiple(primitive_types_bit_map) => { + PyList::new( + py, + primitive_types_bit_map.into_iter().map(|ty| ty.to_string()), + )? + .unbind() + } + } + }, + }, + jsonschema::error::ValidationErrorKind::UnevaluatedItems { unexpected } => { + ValidationErrorKind::UnevaluatedItems { + unexpected: PyList::new(py, unexpected)?.unbind(), + } } - jsonschema::error::ValidationErrorKind::UniqueItems => ValidationErrorKind::UniqueItems, - jsonschema::error::ValidationErrorKind::Referencing(_) => { - ValidationErrorKind::Referencing + jsonschema::error::ValidationErrorKind::UnevaluatedProperties { unexpected } => { + ValidationErrorKind::UnevaluatedProperties { + unexpected: PyList::new(py, unexpected)?.unbind(), + } } - } + jsonschema::error::ValidationErrorKind::UniqueItems => { + ValidationErrorKind::UniqueItems {} + } + jsonschema::error::ValidationErrorKind::Referencing(error) => { + ValidationErrorKind::Referencing { + error: Py::new( + py, + ReferencingError { + message: error.to_string(), + }, + )?, + } + } + }) } } @@ -216,12 +320,19 @@ impl ValidationErrorIter { } } -fn into_py_err( +#[allow(clippy::type_complexity)] +fn into_validation_error_args( py: Python<'_>, error: jsonschema::ValidationError<'_>, mask: Option<&str>, -) -> PyResult { - let pyerror_type = PyType::new::(py); +) -> PyResult<( + String, + String, + Py, + Py, + ValidationErrorKind, + PyObject, +)> { let message = if let Some(mask) = mask { error.masked_with(mask).to_string() } else { @@ -251,8 +362,25 @@ fn into_py_err( .map(into_path) .collect::, _>>()?; let instance_path = PyList::new(py, elements)?.unbind(); - let kind: ValidationErrorKind = error.kind.into(); + let kind = ValidationErrorKind::try_new(py, error.kind, mask)?; let instance = pythonize::pythonize(py, error.instance.as_ref())?.unbind(); + Ok(( + message, + verbose_message, + schema_path, + instance_path, + kind, + instance, + )) +} +fn into_py_err( + py: Python<'_>, + error: jsonschema::ValidationError<'_>, + mask: Option<&str>, +) -> PyResult { + let (message, verbose_message, schema_path, instance_path, kind, instance) = + into_validation_error_args(py, error, mask)?; + let pyerror_type = PyType::new::(py); Ok(PyErr::from_type( pyerror_type, ( @@ -976,6 +1104,7 @@ fn jsonschema_rs(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add("ValidationError", py.get_type::())?; + module.add("ReferencingError", py.get_type::())?; module.add("ValidationErrorKind", py.get_type::())?; module.add("Draft4", DRAFT4)?; module.add("Draft6", DRAFT6)?; diff --git a/crates/jsonschema-py/tests-py/test_jsonschema.py b/crates/jsonschema-py/tests-py/test_jsonschema.py index 73df7fa6..1406a3e4 100644 --- a/crates/jsonschema-py/tests-py/test_jsonschema.py +++ b/crates/jsonschema-py/tests-py/test_jsonschema.py @@ -76,7 +76,7 @@ def test_repr(): def test_validate(func): with pytest.raises(ValidationError, match="2 is less than the minimum of 5") as exc: func(2) - assert exc.value.kind == ValidationErrorKind.Minimum + assert isinstance(exc.value.kind, ValidationErrorKind.Minimum) assert exc.value.instance == 2 @@ -135,10 +135,49 @@ 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 isinstance(exc.value.kind, ValidationErrorKind.Type) assert exc.value.instance == 1 +@pytest.mark.parametrize( + ["schema", "instance", "kind", "attrs"], + [ + ({"maxItems": 1}, [1, 2], ValidationErrorKind.MaxItems, {"limit": 1}), + ({"anyOf": [{"type": "string"}, {"type": "number"}]}, True, ValidationErrorKind.AnyOf, {}), + ({"const": "test"}, "wrong", ValidationErrorKind.Constant, {"expected_value": "test"}), + ({"contains": {"type": "string"}}, [1, 2, 3], ValidationErrorKind.Contains, {}), + ({"enum": [1, 2, 3]}, 4, ValidationErrorKind.Enum, {"options": [1, 2, 3]}), + ({"exclusiveMaximum": 5}, 5, ValidationErrorKind.ExclusiveMaximum, {"limit": 5}), + ({"exclusiveMinimum": 5}, 5, ValidationErrorKind.ExclusiveMinimum, {"limit": 5}), + (False, "anything", ValidationErrorKind.FalseSchema, {}), + ({"format": "email"}, "not-an-email", ValidationErrorKind.Format, {"format": "email"}), + ({"maximum": 5}, 6, ValidationErrorKind.Maximum, {"limit": 5}), + ({"maxLength": 5}, "too long string", ValidationErrorKind.MaxLength, {"limit": 5}), + ({"maxProperties": 1}, {"a": 1, "b": 2}, ValidationErrorKind.MaxProperties, {"limit": 1}), + ({"minItems": 2}, [1], ValidationErrorKind.MinItems, {"limit": 2}), + ({"minimum": 5}, 4, ValidationErrorKind.Minimum, {"limit": 5}), + ({"minLength": 5}, "foo", ValidationErrorKind.MinLength, {"limit": 5}), + ({"minProperties": 2}, {"a": 1}, ValidationErrorKind.MinProperties, {"limit": 2}), + ({"multipleOf": 2}, 3, ValidationErrorKind.MultipleOf, {"multiple_of": 2.0}), + ({"not": {"type": "string"}}, "string", ValidationErrorKind.Not, {"schema": {"type": "string"}}), + ({"oneOf": [{"type": "string"}, {"type": "string"}]}, "1", ValidationErrorKind.OneOfMultipleValid, {}), + ({"pattern": "^test$"}, "wrong", ValidationErrorKind.Pattern, {"pattern": "^test$"}), + ({"required": ["missing"]}, {}, ValidationErrorKind.Required, {"property": "missing"}), + ({"type": "string"}, 1, ValidationErrorKind.Type, {"types": ["string"]}), + ({"uniqueItems": True}, [1, 1], ValidationErrorKind.UniqueItems, {}), + ], +) +def test_validation_error_kinds(schema, instance, kind, attrs): + with pytest.raises(ValidationError) as exc: + validate(schema, instance, validate_formats=True) + + assert isinstance(exc.value.kind, kind) + + for attr, expected_value in attrs.items(): + assert hasattr(exc.value.kind, attr) + assert getattr(exc.value.kind, attr) == expected_value + + @given(minimum=st.integers().map(abs)) def test_minimum(minimum): with suppress(SystemError, ValueError): @@ -183,7 +222,7 @@ def test_error_message(): On instance["foo"]: null""" ) - assert exc.kind == ValidationErrorKind.Type + assert isinstance(exc.kind, ValidationErrorKind.Type) assert exc.instance is None @@ -193,13 +232,13 @@ def test_error_instance(): validate({"type": "array"}, instance) pytest.fail("Validation error should happen") except ValidationError as exc: - assert exc.kind == ValidationErrorKind.Type + assert isinstance(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 isinstance(exc.kind, ValidationErrorKind.Type) assert exc.instance == instance["a"] diff --git a/crates/jsonschema/src/primitive_type.rs b/crates/jsonschema/src/primitive_type.rs index 40e411ab..f37715a3 100644 --- a/crates/jsonschema/src/primitive_type.rs +++ b/crates/jsonschema/src/primitive_type.rs @@ -154,8 +154,14 @@ impl Iterator for PrimitiveTypesBitMapIterator { Some(bit_map_representation_primitive_type(least_significant_bit)) } } + fn size_hint(&self) -> (usize, Option) { + let count = self.bit_map.inner.count_ones() as usize; + (count, Some(count)) + } } +impl ExactSizeIterator for PrimitiveTypesBitMapIterator {} + #[cfg(test)] mod tests { use super::*;