diff --git a/Cargo.toml b/Cargo.toml index 406178c..ff5cbf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,3 +125,9 @@ required-features = ["full-opa"] name="aci" harness=false test=false + +[package.metadata.docs.rs] +# To build locally: +# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps +all-features = true +rustdoc-args = ["--cfg", "docsrs"] \ No newline at end of file diff --git a/README.md b/README.md index b4f813f..37f2e4a 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,8 @@ $ regorus eval -d examples/example.rego -i examples/input.json data.example --co ``` It produces the following coverage report which shows that all lines are executed except the line that sets `allow` to true. -![coverage.png](https://github.com/microsoft/regorus/blob/main/docs/coverage.png) + +![coverage.png](https://github.com/microsoft/regorus/blob/main/docs/coverage.png?raw=true) See [Engine::get_coverage_report](https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_coverage_report) for details. Policy coverage information is useful for debugging your policy as well as to write tests for your policy so that all diff --git a/src/engine.rs b/src/engine.rs index f0c5c80..15c89f6 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -265,12 +265,85 @@ impl Engine { ) } + /// Evaluate a Rego query that produces a boolean value. + /// + /// + /// This function should be preferred over [`Engine::eval_query`] if just a `true`/`false` + /// value is desired instead of [`QueryResults`]. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// # let mut engine = Engine::new(); + /// + /// let enable_tracing = false; + /// assert_eq!(engine.eval_bool_query("1 > 2".to_string(), enable_tracing)?, false); + /// assert_eq!(engine.eval_bool_query("1 < 2".to_string(), enable_tracing)?, true); + /// + /// // Non boolean queries will raise an error. + /// assert!(engine.eval_bool_query("1+1".to_string(), enable_tracing).is_err()); + /// + /// // Queries producing multiple values will raise an error. + /// assert!(engine.eval_bool_query("true; true".to_string(), enable_tracing).is_err()); + /// + /// // Queries producing no values will raise an error. + /// assert!(engine.eval_bool_query("true; false; true".to_string(), enable_tracing).is_err()); + /// # Ok(()) + /// # } pub fn eval_bool_query(&mut self, query: String, enable_tracing: bool) -> Result { let results = self.eval_query(query, enable_tracing)?; - if results.result.len() != 1 || results.result[0].expressions.len() != 1 { - bail!("query did not produce exactly one value"); + match results.result.len() { + 0 => bail!("query did not produce any values"), + 1 if results.result[0].expressions.len() == 1 => { + results.result[0].expressions[0].value.as_bool().copied() + } + _ => bail!("query produced more than one value"), } - results.result[0].expressions[0].value.as_bool().copied() + } + + /// Evaluate an `allow` query. + /// + /// This is a wrapper over [`Engine::eval_bool_query`] that returns true only if the + /// boolean query succeed and produced a `true` value. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// # let mut engine = Engine::new(); + /// + /// let enable_tracing = false; + /// assert_eq!(engine.eval_allow_query("1 > 2".to_string(), enable_tracing), false); + /// assert_eq!(engine.eval_allow_query("1 < 2".to_string(), enable_tracing), true); + /// + /// assert_eq!(engine.eval_allow_query("1+1".to_string(), enable_tracing), false); + /// assert_eq!(engine.eval_allow_query("true; true".to_string(), enable_tracing), false); + /// assert_eq!(engine.eval_allow_query("true; false; true".to_string(), enable_tracing), false); + /// # Ok(()) + /// # } + pub fn eval_allow_query(&mut self, query: String, enable_tracing: bool) -> bool { + matches!(self.eval_bool_query(query, enable_tracing), Ok(true)) + } + + /// Evaluate a `deny` query. + /// + /// This is a wrapper over [`Engine::eval_bool_query`] that returns false only if the + /// boolean query succeed and produced a `false` value. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// # let mut engine = Engine::new(); + /// + /// let enable_tracing = false; + /// assert_eq!(engine.eval_deny_query("1 > 2".to_string(), enable_tracing), false); + /// assert_eq!(engine.eval_deny_query("1 < 2".to_string(), enable_tracing), true); + /// + /// assert_eq!(engine.eval_deny_query("1+1".to_string(), enable_tracing), true); + /// assert_eq!(engine.eval_deny_query("true; true".to_string(), enable_tracing), true); + /// assert_eq!(engine.eval_deny_query("true; false; true".to_string(), enable_tracing), true); + /// # Ok(()) + /// # } + pub fn eval_deny_query(&mut self, query: String, enable_tracing: bool) -> bool { + !matches!(self.eval_bool_query(query, enable_tracing), Ok(false)) } #[doc(hidden)] @@ -455,16 +528,62 @@ impl Engine { } #[cfg(feature = "coverage")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "coverage")))] + /// Get the coverage report. + /// + /// ```rust + /// # use regorus::*; + /// # use anyhow::{bail, Result}; + /// # fn main() -> Result<()> { + /// let mut engine = Engine::new(); + /// + /// engine.add_policy( + /// "policy.rego".to_string(), + /// r#" + /// package test # Line 2 + /// + /// x = y { # Line 4 + /// input.a > 2 # Line 5 + /// y = 5 # Line 6 + /// } + /// "#.to_string() + /// )?; + /// + /// // Enable coverage. + /// engine.set_enable_coverage(true); + /// + /// engine.eval_query("data".to_string(), false)?; + /// + /// let report = engine.get_coverage_report()?; + /// assert_eq!(report.files[0].path, "policy.rego"); + /// + /// // Only line 5 is evaluated. + /// assert_eq!(report.files[0].covered.iter().cloned().collect::>(), vec![5]); + /// + /// // Line 4 and 6 are not evaluated. + /// assert_eq!(report.files[0].not_covered.iter().cloned().collect::>(), vec![4, 6]); + /// # Ok(()) + /// # } + /// ``` + /// + /// See also [`crate::coverage::Report::to_colored_string`]. pub fn get_coverage_report(&self) -> Result { self.interpreter.get_coverage_report() } #[cfg(feature = "coverage")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "coverage")))] + /// Enable/disable policy coverage. + /// + /// If `enable` is different from the current value, then any existing coverage + /// information will be cleared. pub fn set_enable_coverage(&mut self, enable: bool) { self.interpreter.set_enable_coverage(enable) } #[cfg(feature = "coverage")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "coverage")))] + /// Clear the gathered policy coverage data. pub fn clear_coverage_data(&mut self) { self.interpreter.clear_coverage_data() } diff --git a/src/lib.rs b/src/lib.rs index bf75435..490b3ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ // Use README.md as crate documentation. #![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] +#![cfg_attr(docsrs, feature(doc_cfg))] use serde::Serialize; @@ -340,21 +341,39 @@ impl std::fmt::Debug for dyn Extension { } #[cfg(feature = "coverage")] +#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))] pub mod coverage { #[derive(Default, serde::Serialize, serde::Deserialize)] + /// Coverage information about a rego policy file. pub struct File { + /// Path of the policy file. pub path: String, + + /// The rego policy. pub code: String, + + /// Lines that were evaluated. pub covered: std::collections::BTreeSet, + + /// Lines that were not evaluated. pub not_covered: std::collections::BTreeSet, } #[derive(Default, serde::Serialize, serde::Deserialize)] + /// Policy coverage report. pub struct Report { + /// Coverage information for files. pub files: Vec, } impl Report { + /// Produce an ANSI color encoded version of the report. + /// + /// Covered lines are green. + /// Lines that are not covered are red. + /// + /// + pub fn to_colored_string(&self) -> anyhow::Result { use std::io::Write; let mut s = Vec::new();