44//! constants module. Both functions accept JSON schemas as strings and report
55//! invalid inputs or hard unsupported core-library cases as `ValueError`.
66
7- use pyo3:: exceptions:: PyValueError ;
7+ use pyo3:: exceptions:: { PyTypeError , PyValueError } ;
88use pyo3:: prelude:: * ;
9+ use pyo3:: types:: { PyAny , PyBool , PyDict , PyFloat , PyInt , PyList , PyString , PyTuple } ;
910
1011use :: jsoncompat:: { Role , SchemaDocument , check_compat, validate_compatibility_input} ;
1112use json_schema_fuzz:: { GenerateError , GenerationConfig , ValueGenerator } ;
1213
13- use serde_json:: Value as JsonValue ;
14+ use serde_json:: { Map as JsonMap , Number as JsonNumber , Value as JsonValue } ;
15+
16+ #[ pyclass( name = "Validator" , module = "jsoncompat" , unsendable) ]
17+ struct ValidatorPy {
18+ schema : SchemaDocument ,
19+ }
20+
21+ #[ pymethods]
22+ impl ValidatorPy {
23+ /// Check whether a JSON value encoded as a string satisfies this validator's schema.
24+ fn is_valid ( & self , instance_json : & str ) -> PyResult < bool > {
25+ self . is_valid_json ( instance_json)
26+ }
27+
28+ /// Check whether a JSON value encoded as a string satisfies this validator's schema.
29+ fn is_valid_json ( & self , instance_json : & str ) -> PyResult < bool > {
30+ let instance = parse_json ( instance_json) ?;
31+ validate_value_for_schema ( & self . schema , & instance)
32+ }
33+
34+ /// Check whether a Python JSON-compatible value satisfies this validator's schema.
35+ fn is_valid_value ( & self , instance : & Bound < ' _ , PyAny > ) -> PyResult < bool > {
36+ let instance = py_to_json_value ( instance) ?;
37+ validate_value_for_schema ( & self . schema , & instance)
38+ }
39+ }
1440
1541fn validated_schema ( raw : & JsonValue ) -> Result < SchemaDocument , String > {
1642 let schema = SchemaDocument :: from_json ( raw) . map_err ( |error| error. to_string ( ) ) ?;
@@ -27,6 +53,82 @@ fn compatibility_schema(raw: &JsonValue) -> Result<SchemaDocument, String> {
2753 Ok ( schema)
2854}
2955
56+ fn validate_value_for_schema ( schema : & SchemaDocument , instance : & JsonValue ) -> PyResult < bool > {
57+ schema
58+ . is_valid ( instance)
59+ . map_err ( |e| PyErr :: new :: < PyValueError , _ > ( format ! ( "Validation failed: {e}" ) ) )
60+ }
61+
62+ fn py_to_json_value ( value : & Bound < ' _ , PyAny > ) -> PyResult < JsonValue > {
63+ if value. is_none ( ) {
64+ return Ok ( JsonValue :: Null ) ;
65+ }
66+ if value. is_instance_of :: < PyBool > ( ) {
67+ return Ok ( JsonValue :: Bool ( value. extract :: < bool > ( ) ?) ) ;
68+ }
69+ if value. is_instance_of :: < PyInt > ( ) {
70+ return py_int_to_json_value ( value) ;
71+ }
72+ if value. is_instance_of :: < PyFloat > ( ) {
73+ let number = value. extract :: < f64 > ( ) ?;
74+ if !number. is_finite ( ) {
75+ return Err ( PyErr :: new :: < PyValueError , _ > ( "JSON numbers must be finite" ) ) ;
76+ }
77+ let Some ( number) = JsonNumber :: from_f64 ( number) else {
78+ return Err ( PyErr :: new :: < PyValueError , _ > (
79+ "failed to convert Python float to JSON number" ,
80+ ) ) ;
81+ } ;
82+ return Ok ( JsonValue :: Number ( number) ) ;
83+ }
84+ if value. is_instance_of :: < PyString > ( ) {
85+ return Ok ( JsonValue :: String ( value. extract :: < String > ( ) ?) ) ;
86+ }
87+ if let Ok ( list) = value. cast :: < PyList > ( ) {
88+ return list
89+ . iter ( )
90+ . map ( |item| py_to_json_value ( & item) )
91+ . collect :: < PyResult < Vec < _ > > > ( )
92+ . map ( JsonValue :: Array ) ;
93+ }
94+ if let Ok ( tuple) = value. cast :: < PyTuple > ( ) {
95+ return tuple
96+ . iter ( )
97+ . map ( |item| py_to_json_value ( & item) )
98+ . collect :: < PyResult < Vec < _ > > > ( )
99+ . map ( JsonValue :: Array ) ;
100+ }
101+ if let Ok ( dict) = value. cast :: < PyDict > ( ) {
102+ let mut object = JsonMap :: with_capacity ( dict. len ( ) ) ;
103+ for ( key, item) in dict {
104+ if !key. is_instance_of :: < PyString > ( ) {
105+ return Err ( PyErr :: new :: < PyTypeError , _ > (
106+ "JSON object keys must be strings" ,
107+ ) ) ;
108+ }
109+ object. insert ( key. extract :: < String > ( ) ?, py_to_json_value ( & item) ?) ;
110+ }
111+ return Ok ( JsonValue :: Object ( object) ) ;
112+ }
113+
114+ Err ( PyErr :: new :: < PyTypeError , _ > ( format ! (
115+ "expected a JSON-compatible value, got {}" ,
116+ value. get_type( ) . name( ) ?
117+ ) ) )
118+ }
119+
120+ fn py_int_to_json_value ( value : & Bound < ' _ , PyAny > ) -> PyResult < JsonValue > {
121+ if let Ok ( number) = value. extract :: < i64 > ( ) {
122+ return Ok ( JsonValue :: Number ( JsonNumber :: from ( number) ) ) ;
123+ }
124+ if let Ok ( number) = value. extract :: < u64 > ( ) {
125+ return Ok ( JsonValue :: Number ( JsonNumber :: from ( number) ) ) ;
126+ }
127+ Err ( PyErr :: new :: < PyValueError , _ > (
128+ "JSON integer is outside the supported range" ,
129+ ) )
130+ }
131+
30132/// Parse a JSON string into a serde_json::Value, converting any error into a Python ValueError.
31133fn parse_json ( s : & str ) -> PyResult < JsonValue > {
32134 serde_json:: from_str ( s) . map_err ( |e| PyErr :: new :: < PyValueError , _ > ( format ! ( "Invalid JSON: {e}" ) ) )
@@ -115,12 +217,24 @@ fn generate_value_py(schema_json: &str, depth: u8) -> PyResult<String> {
115217 } )
116218}
117219
220+ /// Build a reusable validator for one JSON Schema document.
221+ #[ pyfunction]
222+ #[ pyo3( signature = ( schema_json) , name = "validator_for" ) ]
223+ fn validator_for_py ( schema_json : & str ) -> PyResult < ValidatorPy > {
224+ let raw = parse_json ( schema_json) ?;
225+ let schema = validated_schema ( & raw )
226+ . map_err ( |e| PyErr :: new :: < PyValueError , _ > ( format ! ( "Invalid schema: {e}" ) ) ) ?;
227+ Ok ( ValidatorPy { schema } )
228+ }
229+
118230/// Python module definition
119231#[ pymodule]
120232#[ pyo3( name = "jsoncompat" ) ]
121233fn jsoncompat ( py : Python , m : & Bound < ' _ , PyModule > ) -> PyResult < ( ) > {
122234 m. add_function ( wrap_pyfunction ! ( check_compat_py, m) ?) ?;
123235 m. add_function ( wrap_pyfunction ! ( generate_value_py, m) ?) ?;
236+ m. add_function ( wrap_pyfunction ! ( validator_for_py, m) ?) ?;
237+ m. add_class :: < ValidatorPy > ( ) ?;
124238
125239 let role_constants = PyModule :: new ( py, "Role" ) ?;
126240 role_constants. add ( "SERIALIZER" , "serializer" ) ?;
0 commit comments