Skip to content

Commit c9f2cfa

Browse files
committed
Rust backend
1 parent a093f9c commit c9f2cfa

File tree

9 files changed

+416
-11
lines changed

9 files changed

+416
-11
lines changed

.github/workflows/python-tests.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ on:
1010

1111
jobs:
1212
tests:
13-
name: "py${{ matrix.python-version }}-${{ matrix.os }}"
13+
name: "py${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.backend }}"
1414
runs-on: ${{ matrix.os }}
1515
strategy:
1616
matrix:
1717
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
1818
os: [windows-latest, ubuntu-latest]
19+
backend: ['jsonschema']
20+
include:
21+
- python-version: '3.12'
22+
os: ubuntu-latest
23+
backend: 'jsonschema-rs'
24+
- python-version: '3.13'
25+
os: windows-latest
26+
backend: 'jsonschema-rs'
1927
fail-fast: false
2028
steps:
2129
- uses: actions/checkout@v4
@@ -49,9 +57,14 @@ jobs:
4957
- name: Install dependencies
5058
run: poetry install --all-extras
5159

60+
- name: Install jsonschema-rs
61+
if: matrix.backend != 'jsonschema'
62+
run: poetry run pip install ${{ matrix.backend }}
63+
5264
- name: Test
5365
env:
5466
PYTEST_ADDOPTS: "--color=yes"
67+
OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND: ${{ matrix.backend }}
5568
run: poetry run pytest
5669

5770
- name: Static type check

README.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@ Rules:
131131
* Set ``0`` to disable the resolved cache.
132132
* Invalid values (non-integer or negative) fall back to ``128``.
133133

134+
You can also choose schema validator backend:
135+
136+
.. code-block:: bash
137+
138+
OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND=jsonschema-rs
139+
140+
Allowed values are ``auto`` (default), ``jsonschema``, and
141+
``jsonschema-rs``.
142+
Invalid values raise a warning and fall back to ``auto``.
143+
134144
Related projects
135145
################
136146

docs/cli.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,7 @@ Performance note:
7373
You can tune resolved-path caching with
7474
``OPENAPI_SPEC_VALIDATOR_RESOLVED_CACHE_MAXSIZE``.
7575
Default is ``128``; set ``0`` to disable.
76+
77+
You can also select schema validator backend with
78+
``OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND``
79+
(``auto``/``jsonschema``/``jsonschema-rs``).

docs/python.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,13 @@ Rules:
7575
* Default is ``128``.
7676
* Set ``0`` to disable the resolved cache.
7777
* Invalid values (non-integer or negative) fall back to ``128``.
78+
79+
Schema validator backend can be selected with:
80+
81+
.. code-block:: bash
82+
83+
OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND=jsonschema-rs
84+
85+
Allowed values are ``auto`` (default), ``jsonschema``, and
86+
``jsonschema-rs``.
87+
Invalid values raise a warning and fall back to ``auto``.

openapi_spec_validator/schemas/__init__.py

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,60 @@
11
"""OpenAIP spec validator schemas module."""
22

33
from functools import partial
4+
from typing import Any
45

56
from jsonschema.validators import Draft4Validator
67
from jsonschema.validators import Draft202012Validator
78
from lazy_object_proxy import Proxy
89

910
from openapi_spec_validator.schemas.utils import get_schema_content
11+
from openapi_spec_validator.settings import get_schema_validator_backend
1012

11-
__all__ = ["schema_v2", "schema_v3", "schema_v30", "schema_v31", "schema_v32"]
13+
_create_jsonschema_rs_validator_impl: Any = None
14+
15+
# Import jsonschema-rs adapters
16+
try:
17+
from openapi_spec_validator.schemas.jsonschema_rs_adapters import (
18+
create_validator as _create_jsonschema_rs_validator_impl,
19+
)
20+
from openapi_spec_validator.schemas.jsonschema_rs_adapters import (
21+
get_validator_backend,
22+
)
23+
from openapi_spec_validator.schemas.jsonschema_rs_adapters import (
24+
has_jsonschema_rs_validators,
25+
)
26+
27+
_USE_JSONSCHEMA_RS = has_jsonschema_rs_validators()
28+
except ImportError:
29+
_create_jsonschema_rs_validator_impl = None
30+
_USE_JSONSCHEMA_RS = False
31+
32+
def has_jsonschema_rs_validators() -> bool:
33+
return False
34+
35+
def get_validator_backend() -> str:
36+
return "python (jsonschema)"
37+
38+
39+
_BACKEND_MODE = get_schema_validator_backend()
40+
41+
if _BACKEND_MODE == "jsonschema":
42+
_USE_JSONSCHEMA_RS = False
43+
elif _BACKEND_MODE == "jsonschema-rs" and not _USE_JSONSCHEMA_RS:
44+
raise ImportError(
45+
"OPENAPI_SPEC_VALIDATOR_SCHEMA_VALIDATOR_BACKEND=jsonschema-rs "
46+
"is set but jsonschema-rs is not available. "
47+
"Install it with: pip install jsonschema-rs"
48+
)
49+
50+
__all__ = [
51+
"schema_v2",
52+
"schema_v3",
53+
"schema_v30",
54+
"schema_v31",
55+
"schema_v32",
56+
"get_validator_backend",
57+
]
1258

1359
get_schema_content_v2 = partial(get_schema_content, "2.0")
1460
get_schema_content_v30 = partial(get_schema_content, "3.0")
@@ -23,10 +69,53 @@
2369
# alias to the latest v3 version
2470
schema_v3 = schema_v32
2571

26-
get_openapi_v2_schema_validator = partial(Draft4Validator, schema_v2)
27-
get_openapi_v30_schema_validator = partial(Draft4Validator, schema_v30)
28-
get_openapi_v31_schema_validator = partial(Draft202012Validator, schema_v31)
29-
get_openapi_v32_schema_validator = partial(Draft202012Validator, schema_v32)
72+
73+
def _create_jsonschema_rs_schema_validator(
74+
schema: dict[str, Any],
75+
draft: str,
76+
) -> Any:
77+
if _create_jsonschema_rs_validator_impl is None:
78+
raise ImportError(
79+
"jsonschema-rs is not available. "
80+
"Install it with: pip install jsonschema-rs"
81+
)
82+
return _create_jsonschema_rs_validator_impl(schema, draft)
83+
84+
85+
# Validator factory functions with Rust/Python selection
86+
def get_openapi_v2_schema_validator() -> Any:
87+
"""Create OpenAPI 2.0 schema validator (Draft4)."""
88+
if _USE_JSONSCHEMA_RS:
89+
return _create_jsonschema_rs_schema_validator(dict(schema_v2), draft="draft4")
90+
return Draft4Validator(schema_v2)
91+
92+
93+
def get_openapi_v30_schema_validator() -> Any:
94+
"""Create OpenAPI 3.0 schema validator (Draft4)."""
95+
if _USE_JSONSCHEMA_RS:
96+
return _create_jsonschema_rs_schema_validator(dict(schema_v30), draft="draft4")
97+
return Draft4Validator(schema_v30)
98+
99+
100+
def get_openapi_v31_schema_validator() -> Any:
101+
"""Create OpenAPI 3.1 schema validator (Draft 2020-12)."""
102+
if _USE_JSONSCHEMA_RS:
103+
return _create_jsonschema_rs_schema_validator(
104+
dict(schema_v31),
105+
draft="draft202012",
106+
)
107+
return Draft202012Validator(schema_v31)
108+
109+
110+
def get_openapi_v32_schema_validator() -> Any:
111+
"""Create OpenAPI 3.2 schema validator (Draft 2020-12)."""
112+
if _USE_JSONSCHEMA_RS:
113+
return _create_jsonschema_rs_schema_validator(
114+
dict(schema_v32),
115+
draft="draft202012",
116+
)
117+
return Draft202012Validator(schema_v32)
118+
30119

31120
openapi_v2_schema_validator = Proxy(get_openapi_v2_schema_validator)
32121
openapi_v30_schema_validator = Proxy(get_openapi_v30_schema_validator)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# openapi_spec_validator/schemas/rust_adapters.py
2+
"""
3+
Proof-of-Concept: jsonschema-rs adapter for openapi-spec-validator.
4+
5+
This module provides a compatibility layer between jsonschema-rs (Rust)
6+
and the existing jsonschema (Python) validator interface.
7+
"""
8+
9+
from typing import Any
10+
from typing import Iterator
11+
from typing import TYPE_CHECKING
12+
from typing import cast
13+
14+
if TYPE_CHECKING:
15+
class ValidationErrorBase(Exception):
16+
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
17+
18+
else:
19+
from jsonschema.exceptions import ValidationError as ValidationErrorBase
20+
21+
# Try to import jsonschema-rs
22+
try:
23+
import jsonschema_rs
24+
25+
HAS_JSONSCHEMA_RS = True
26+
except ImportError:
27+
HAS_JSONSCHEMA_RS = False
28+
jsonschema_rs = None # type: ignore
29+
30+
31+
class RustValidatorError(ValidationErrorBase):
32+
"""ValidationError compatible with jsonschema, but originating from Rust validator."""
33+
34+
pass
35+
36+
37+
class RustValidatorWrapper:
38+
"""
39+
Wrapper that makes jsonschema-rs validator compatible with jsonschema interface.
40+
41+
This allows drop-in replacement while maintaining the same API surface.
42+
"""
43+
44+
def __init__(self, schema: dict[str, Any], validator: Any):
45+
"""
46+
Initialize Rust validator wrapper.
47+
48+
Args:
49+
schema: JSON Schema to validate against
50+
cls: JSON Schema validator
51+
"""
52+
if not HAS_JSONSCHEMA_RS:
53+
raise ImportError(
54+
"jsonschema-rs is not installed. Install it with: "
55+
"pip install jsonschema-rs"
56+
)
57+
58+
self.schema = schema
59+
self._rs_validator = validator
60+
61+
def iter_errors(self, instance: Any) -> Iterator[ValidationErrorBase]:
62+
"""
63+
Validate instance and yield errors in jsonschema format.
64+
65+
This method converts jsonschema-rs errors to jsonschema ValidationError
66+
format for compatibility with existing code.
67+
"""
68+
# Try to validate - jsonschema-rs returns ValidationError on failure
69+
result = self._rs_validator.validate(instance)
70+
71+
if result is not None:
72+
# result contains validation errors
73+
# jsonschema-rs returns an iterator of errors
74+
for error in self._rs_validator.iter_errors(instance):
75+
yield self._convert_rust_error(error, instance)
76+
77+
def validate(self, instance: Any) -> None:
78+
"""
79+
Validate instance and raise ValidationError if invalid.
80+
81+
Compatible with jsonschema Validator.validate() method.
82+
"""
83+
try:
84+
self._rs_validator.validate(instance)
85+
except jsonschema_rs.ValidationError as e:
86+
# Convert and raise as Python ValidationError
87+
py_error = self._convert_rust_error_exception(e, instance)
88+
raise py_error from e
89+
90+
def is_valid(self, instance: Any) -> bool:
91+
"""Check if instance is valid against schema."""
92+
return cast(bool, self._rs_validator.is_valid(instance))
93+
94+
def _convert_rust_error(
95+
self, rust_error: Any, instance: Any
96+
) -> ValidationErrorBase:
97+
"""
98+
Convert jsonschema-rs error format to jsonschema ValidationError.
99+
100+
jsonschema-rs error structure:
101+
- message: str
102+
- instance_path: list
103+
- schema_path: list (if available)
104+
"""
105+
message = str(rust_error)
106+
107+
# Extract path information if available
108+
# Note: jsonschema-rs error format may differ - adjust as needed
109+
instance_path = getattr(rust_error, "instance_path", [])
110+
schema_path = getattr(rust_error, "schema_path", [])
111+
112+
return RustValidatorError(
113+
message=message,
114+
path=list(instance_path) if instance_path else [],
115+
schema_path=list(schema_path) if schema_path else [],
116+
instance=instance,
117+
schema=self.schema,
118+
)
119+
120+
def _convert_rust_error_exception(
121+
self, rust_error: "jsonschema_rs.ValidationError", instance: Any
122+
) -> ValidationErrorBase:
123+
"""Convert jsonschema-rs ValidationError exception to Python format."""
124+
message = str(rust_error)
125+
126+
return RustValidatorError(
127+
message=message,
128+
instance=instance,
129+
schema=self.schema,
130+
)
131+
132+
133+
def create_validator(
134+
schema: dict[str, Any], draft: str = "draft202012"
135+
) -> RustValidatorWrapper:
136+
"""
137+
Factory function to create Rust-backed validator.
138+
139+
Args:
140+
schema: JSON Schema to validate against
141+
draft: JSON Schema draft version
142+
143+
Returns:
144+
RustValidatorWrapper instance
145+
"""
146+
147+
# Create appropriate Rust validator based on draft
148+
validator: Any
149+
if draft == "draft4":
150+
validator = jsonschema_rs.Draft4Validator(schema)
151+
elif draft == "draft7":
152+
validator = jsonschema_rs.Draft7Validator(schema)
153+
elif draft == "draft201909":
154+
validator = jsonschema_rs.Draft201909Validator(schema)
155+
elif draft == "draft202012":
156+
validator = jsonschema_rs.Draft202012Validator(schema)
157+
else:
158+
raise ValueError(f"Unsupported draft: {draft}")
159+
160+
return RustValidatorWrapper(schema, validator=validator)
161+
162+
163+
# Convenience function to check if Rust validators are available
164+
def has_jsonschema_rs_validators() -> bool:
165+
"""Check if jsonschema-rs is available."""
166+
return HAS_JSONSCHEMA_RS
167+
168+
169+
def get_validator_backend() -> str:
170+
"""Get current validator backend (rust or python)."""
171+
if HAS_JSONSCHEMA_RS:
172+
return "rust (jsonschema-rs)"
173+
return "python (jsonschema)"

0 commit comments

Comments
 (0)