diff --git a/README.md b/README.md index 2b9091b..029861f 100644 --- a/README.md +++ b/README.md @@ -57,15 +57,15 @@ variables are not known at compile time. ### Evaluation -To evaluate a compiled expression, you need to create an activation, which +To evaluate a compiled expression, you need to provide bindings for +variables and then call `eval()`. + +you need to create an activation, which provides bindings for variables, and then call `eval()`. ```python -# Provide variable values in a dictionary. -activation = cel_env.Activation({"x": 7, "y": 4}) - -# Evaluate the expression. -result = expr.eval(activation) +# Provide variable values in a dictionary and evaluate the expression. +result = expr.eval(data={"x": 7, "y": 4}) # The result is a `CelValue` object, which contains the result's CEL type and # value. @@ -82,9 +82,32 @@ Result type: BOOL Result value: True ``` +### Using an Activation + +The `eval()` function can also be invoked with an `Activation` object that +holds variable bindings and a pointer to an `Arena` (see below). +This is particularly useful when multiple expressions need to be evaluated +with the same set of variable values, such as multiple policies +on the same server request. + +```python +expr1 = cel_env.compile("user.role in ['admin', 'owner']") +expr2 = cel_env.compile("user.organization == 'myorg'") + +# Provide variable values as an Activation. +activation = cel_env.Activation({"user": user}) + +# Evaluate the expression. +result1 = expr1.eval(activation) + +# Evaluate another expression using the same variable bindings +result2 = expr2.eval(activation) +``` + ### Using an Arena -An `Activation` can also take an `Arena` for memory management during +The `eval()` function as well as an `Activation` can also take an `Arena` +for memory management during evaluation. This is a memory optimization technique that allows temporary C++ objects created during the evaluation to be released as a group. The same `Arena` can be shared across multiple activations; just keep in mind that none diff --git a/py_cel_expression.cc b/py_cel_expression.cc index 2e2b2e8..c1d9bc3 100644 --- a/py_cel_expression.cc +++ b/py_cel_expression.cc @@ -18,9 +18,12 @@ #include #include +#include #include +#include #include #include +#include #include "cel/expr/checked.pb.h" #include "cel/expr/syntax.pb.h" @@ -43,14 +46,15 @@ #include "py_cel_activation.h" #include "py_cel_arena.h" #include "py_cel_env_internal.h" +#include "py_cel_function.h" #include "py_cel_type.h" #include "py_cel_value.h" #include "py_error_status.h" #include "status_macros.h" #include +#include #include "pybind11_abseil/status_casters.h" - namespace cel_python { using ::cel::expr::CheckedExpr; @@ -67,7 +71,37 @@ void PyCelExpression::DefinePythonBindings(py::module& m) { // decoder from std::string to Python `str`. return py::bytes(self.PyCelExpression::Serialize()); }) - .def("eval", &PyCelExpression::Eval, py::arg("activation")); + .def( + "eval", + [](PyCelExpression& self, + std::optional> activation, + const std::optional>& + data, + const std::optional>>& + functions, + const std::shared_ptr& arena = + nullptr) -> absl::StatusOr { + if (activation) { + if (data || functions || arena) { + return absl::InvalidArgumentError( + "Cannot provide both activation and any other arguments."); + } + return self.Eval(**activation); + } + std::unordered_map data_ptrs; + if (data) { + for (auto const& [key, val] : *data) { + data_ptrs[key] = val.ptr(); + } + } + return self.Eval(PyCelActivation( + self.env_, data_ptrs, + functions.value_or( + std::vector>{}), + arena ? arena : NewArena())); + }, + py::arg("activation") = py::none(), py::arg("data") = py::none(), + py::arg("functions") = py::none(), py::arg("arena") = nullptr); } absl::StatusOr PyCelExpression::Compile( diff --git a/py_cel_test.py b/py_cel_test.py index b164abf..86a861a 100644 --- a/py_cel_test.py +++ b/py_cel_test.py @@ -718,6 +718,21 @@ def testActivationWithArena(self): gc.collect() self.assertEqual(cel._InternalArena._get_instance_count(), 0) + def testImplicitActivation(self): + expr = self.env.compile("'Hello, ' + var_str") + res = expr.eval(data={"var_str": "World!"}) + self.assertEqual(res.value(), "Hello, World!") + + def testActivationAndOtherArgs(self): + expr = self.env.compile("'Hello, ' + var_str") + with self.assertRaises(Exception) as e: + expr.eval( + self.env.Activation(data={"var_str": "World!"}), + data={"var_str": "World!"}, + ) + self.assertIn("Cannot provide both activation and any other arguments", + str(e.exception)) + def testCompilationErrorHandling(self): # Check parser error. with self.assertRaises(Exception) as e: