diff --git a/docs/python.md b/docs/python.md index 35815cd..ed1acd4 100644 --- a/docs/python.md +++ b/docs/python.md @@ -11,3 +11,4 @@ pip install cql2 ::: cql2.Expr ::: cql2.SqlQuery +::: cql2.ValidationError diff --git a/python/cql2.pyi b/python/cql2.pyi index 00db301..9994b7f 100644 --- a/python/cql2.pyi +++ b/python/cql2.pyi @@ -41,6 +41,18 @@ class Expr: >>> expr = Expr({"op":"=","args":[{"property":"landsat:scene_id"},"LC82030282019133LGN00"]}) """ + def validate(self) -> None: + """Validates this expression using json-schema. + + Raises: + ValidationError: Raised if the validation fails + + Examples: + >>> from cql2 import Expr + >>> expr = Expr("landsat:scene_id = 'LC82030282019133LGN00'") + >>> expr.validate() + """ + def to_json(self) -> dict[str, Any]: """Converts this cql2 expression to a cql2-json dictionary. @@ -81,3 +93,6 @@ class Expr: >>> q.params ['LC82030282019133LGN00'] """ + +class ValidationError(Exception): + """An error raised when cql2 json-schema validation fails.""" diff --git a/python/src/lib.rs b/python/src/lib.rs index e970a4d..461792c 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -1,9 +1,12 @@ use pyo3::{ + create_exception, exceptions::{PyException, PyIOError, PyValueError}, prelude::*, }; use std::path::PathBuf; +create_exception!(cql2, ValidationError, PyException); + /// Crate-specific error enum. enum Error { Cql2(::cql2::Error), @@ -46,6 +49,15 @@ impl Expr { } } + fn validate(&self) -> PyResult<()> { + let validator = ::cql2::Validator::new().map_err(Error::from)?; + if let Err(error) = validator.validate(&self.0.to_value().map_err(Error::from)?) { + Err(ValidationError::new_err(error.to_string())) + } else { + Ok(()) + } + } + /// Converts this expression to a cql2-json dictionary. fn to_json<'py>(&self, py: Python<'py>) -> Result> { pythonize::pythonize(py, &self.0).map_err(Error::from) @@ -104,8 +116,9 @@ impl From for Error { /// A Python module implemented in Rust. #[pymodule] -fn cql2(m: &Bound<'_, PyModule>) -> PyResult<()> { +fn cql2(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add("ValidationError", py.get_type_bound::())?; Ok(()) } diff --git a/python/tests/test_expr.py b/python/tests/test_expr.py index 7f4a073..4b7f949 100644 --- a/python/tests/test_expr.py +++ b/python/tests/test_expr.py @@ -1,6 +1,7 @@ from pathlib import Path -from cql2 import Expr +import pytest +from cql2 import Expr, ValidationError def test_from_path(fixtures: Path) -> None: @@ -26,3 +27,14 @@ def test_to_sql(example01_text: str) -> None: sql_query = Expr(example01_text).to_sql() assert sql_query.query == '("landsat:scene_id" = $1)' assert sql_query.params == ["LC82030282019133LGN00"] + + +def test_validate() -> None: + expr = Expr( + { + "op": "t_before", + "args": [{"property": "updated_at"}, {"timestamp": "invalid-timestamp"}], + } + ) + with pytest.raises(ValidationError): + expr.validate()