From 6eca85b497184630d05170b079da0b224238a259 Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:51:14 -0800 Subject: [PATCH] Improve crate documentation (#111) - Document QueryResults - Delete snippets folder - Document Value Signed-off-by: Anand Krishnamoorthi --- .github/workflows/rust.yml | 8 +- Cargo.toml | 1 + README.md | 30 +- docs/builtins.md | 239 ++++++++++++ snippets/2.rego | 23 -- src/builtins/debugging.rs | 9 +- src/builtins/encoding.rs | 80 +--- src/builtins/objects.rs | 75 ++++ src/engine.rs | 208 ++++++++-- src/interpreter.rs | 55 ++- src/lib.rs | 85 +++- src/number.rs | 20 + src/scheduler.rs | 19 +- src/value.rs | 778 ++++++++++++++++++++++++++++++++++--- tests/aci/main.rs | 4 +- tests/opa.rs | 8 +- tests/value/mod.rs | 11 +- 17 files changed, 1418 insertions(+), 235 deletions(-) create mode 100644 docs/builtins.md delete mode 100644 snippets/2.rego diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8f652985..fa46594a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -27,11 +27,13 @@ jobs: - name: Clippy run: cargo clippy --all-targets --no-deps -- -Dwarnings - name: Run tests - run: cargo test --verbose + run: cargo test -r --verbose - name: Build (MUSL) run: cargo build --verbose --all-targets --target x86_64-unknown-linux-musl - name: Run tests (MUSL) - run: cargo test --verbose --target x86_64-unknown-linux-musl + run: cargo test -r --verbose --target x86_64-unknown-linux-musl + - name: Run tests (ACI) + run: cargo test -r --test aci - name: Run tests (OPA Conformance) run: >- - cargo test --test opa -- $(tr '\n' ' ' < tests/opa.passing) + cargo test -r --test opa -- $(tr '\n' ' ' < tests/opa.passing) diff --git a/Cargo.toml b/Cargo.toml index 18c7e4ac..f37ade8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ uuid = { version = "1.6.1", features = ["v4", "fast-rng"], optional = true } jsonschema = { version = "0.17.1", default-features = false, optional = true } chrono = { version = "0.4.31", optional = true } chrono-tz = { version = "0.8.5", optional = true } +document-features = "0.2.8" [dev-dependencies] diff --git a/README.md b/README.md index c29ce8a9..493c7be8 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,11 @@ fn main() -> Result<()> { // Filename to be associated with the policy. "hello.rego".to_string(), - // Rego policy that just sets a message. - r#" - package test - message = "Hello, World!" - "#.to_string() + // Rego policy that just sets a message. + r#" + package test + message = "Hello, World!" + "#.to_string() )?; // Evaluate the policy, fetch the message and print it. @@ -39,6 +39,25 @@ fn main() -> Result<()> { } ``` +Regorus is designed with [Confidential Computing](https://confidentialcomputing.io/about/) in mind. In Confidential Computing environments, +it is important to be able to control exactly what is being run. Regorus allows enabling and disabling various components using cargo +features. By default all features are enabled. + +The default build of regorus example program is 6.4M: +```bash +$ cargo build -r --example regorus; strip target/release/examples/regorus; ls -lh target/release/examples/regorus +$ cargo build -r --example regorus; strip target/release/examples/regorus; ls -lh target/release/examples/regorus +-rwxr-xr-x 1 anand staff 6.4M Jan 19 11:23 target/release/examples/regorus* +``` + + +When all features except for `yaml` are disabled, the binary size drops down to 2.9M. +```bash +$ cargo build -r --example regorus --features "yaml" --no-default-features; strip target/release/examples/regorus; ls -lh target/release/examples/regorus +-rwxr-xr-x 1 anand staff 2.9M Jan 19 11:26 target/release/examples/regorus* +``` + + Regorus passes the [OPA v0.60.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few builtins. See [OPA Conformance](#opa-conformance) below. @@ -228,6 +247,7 @@ They are captured in the following [github issues](https://github.com/microsoft/ The grammar used by Regorus to parse Rego policies is described in [grammar.md](https://github.com/microsoft/regorus/blob/main/docs/grammar.md) in both [W3C EBNF](https://www.w3.org/Notation.html) and [RailRoad Diagram](https://en.wikipedia.org/wiki/Syntax_diagram) formats. + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a diff --git a/docs/builtins.md b/docs/builtins.md new file mode 100644 index 00000000..71e99029 --- /dev/null +++ b/docs/builtins.md @@ -0,0 +1,239 @@ +# Built-in Functions + + +This page lists all the supported Rego built-in functions and the cargo feature that is needed to enable each builtin. + +Those builtins that are not need for a specific use of the Regorus crate can be excluded from the binary by not specifying +the corresponding feature. This is useful in Confidential Computing scenarios where + - There needs to be control over what a policy execution can and cannot do. + - There needs to be control over exactly what goes into the [Trusted Computing Base](https://en.wikipedia.org/wiki/Trusted_computing_base). + +Currently many builtins are `baked-in`, i.e. there is no way to exclude them from the TCB. +In future, each builtin will be associated with a feature (many builtins could be associated with the same feature). + +- [Comparison](https://www.openpolicyagent.org/docs/latest/policy-reference/#comparison) + | Builtin | Feature | + |--------------------------------------------------------------------------------------------------|---------| + | [x == y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-equal) | _ | + | [x > y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-gt) | _ | + | [x >= y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-gte) | _ | + | [x < y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-lt) | _ | + | [x <= y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-lte) | _ | + | [x != y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-neq) | _ | + +- [Numbers](https://www.openpolicyagent.org/docs/latest/policy-reference/#numbers) + | Builtin | Feature | + |-----------------------------------------------------------------------------------------------------------------------|---------| + | [abs](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-abs) | _ | + | [ceil](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-ceil) | _ | + | [x / y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-div) | _ | + | [floor](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-floor) | _ | + | [x - y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-minus) | _ | + | [x * y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-mul) | _ | + | [numbers.range](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-numbersrange) | _ | + | [numbers.range_step](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-numbersrange_step) | _ | + | [x + y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-plus) | _ | + | [rand.intn](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-randintn) | _ | + | [x % y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-rem) | _ | + | [round](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-round) | _ | + + +- [Aggregates](https://www.openpolicyagent.org/docs/latest/policy-reference/#aggregates) + | Builtin | Feature | + |-----------------------------------------------------------------------------------------------------|---------| + | [count](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-count) | _ | + | [max](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-max) | _ | + | [min](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-min) | _ | + | [product](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-product) | _ | + | [sort](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-sort) | _ | + | [sum](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-sum) | _ | + +- [Arrays](https://www.openpolicyagent.org/docs/latest/policy-reference/#arrays-2) + | Builtin | Feature | + |-----------------------------------------------------------------------------------------------------------|---------| + | [array.concat](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayconcat) | _ | + | [array.reverse](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayreverse) | _ | + | [array.slice](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayslice) | _ | + +- [Sets](https://www.openpolicyagent.org/docs/latest/policy-reference/#sets-2) + | Builtin | Feature | + |---------------------------------------------------------------------------------------------------------|---------| + | [x & y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-sets-and) | _ | + | [intersection](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-sets-intersection) | _ | + | [x - y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-sets-minus) | _ | + | [x \| y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-sets-or) | _ | + | [union](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-sets-union) | _ | + +- [Objects](https://www.openpolicyagent.org/docs/latest/policy-reference/#object) + | Builtin | Feature | + |----------------------------------------------------------------------------------------------------------------------|--------------| + | [json.filter](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-jsonfilter) | _ | + | [json.match_schema](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-jsonmatch_schema) | `jsonschema` | + | [json.remove](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-jsonremove) | _ | + | [json.verify_schema](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-jsonverify_schema) | `jsonschema` | + | [object.filter](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectfilter) | _ | + | [object.get](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectget) | _ | + | [object.keys](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectkeys) | _ | + | [object.remove](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectremove) | _ | + | [object.subset](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectsubset) | _ | + | [object.union](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectunion) | _ | + | [object.union_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectunion_n) | _ | + +- [Strings](https://www.openpolicyagent.org/docs/latest/policy-reference/#strings) + | Builtin | Feature | + |-----------------------------------------------------------------------------------------------------------------------------------|---------| + | [concat](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-concat) | _ | + | [contains](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-contains) | _ | + | [endswith](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-endswith) | _ | + | [format_int](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-format_int) | _ | + | [indexof](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-indexof) | _ | + | [indexof_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-indexof_n) | _ | + | [lower](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-lower) | _ | + | [replace](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-replace) | _ | + | [split](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-split) | _ | + | [sprintf](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-sprintf) | _ | + | [startswith](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-startswith) | _ | + | [strings.any_prefix_match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsany_prefix_match) | _ | + | [strings.any_suffix_match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsany_suffix_match) | _ | + | [strings.render_template](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsrender_template) | _ | + | [strings.replace_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsreplace_n) | _ | + | [strings.reverse](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsreverse) | _ | + | [substring](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-substring) | _ | + | [trim](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim) | _ | + | [trim_left](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim_left) | _ | + | [trim_prefix](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim_prefix) | _ | + | [trim_right](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim_right) | _ | + | [trim_space](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim_space) | _ | + | [trim_suffix](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim_suffix) | _ | + | [upper](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-upper) | _ | + +- [Regex](https://www.openpolicyagent.org/docs/latest/policy-reference/#regex) + | Builtin | Feature | + |-------------------------------------------------------------------------------------------------------------------------------------------------|---------| + | [regex.find_all_string_submatch_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexfind_all_string_submatch_n) | `regex` | + | [regex.find_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexfind_n) | `regex` | + | [regex.globs_match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexglobs_match) | `regex` | + | [regex.is_valid](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexis_valid) | `regex` | + | [regex.match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexmatch) | `regex` | + | [regex.replace](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexreplace) | `regex` | + | [regex.split](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexsplit) | `regex` | + | [regex.template_match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regextemplate_match) | `regex` | + +- [Glob](https://www.openpolicyagent.org/docs/latest/policy-reference/#regex) + | Builtin | Feature | + |--------------------------------------------------------------------------------------------------------------|---------| + | [glob.match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-glob-globmatch) | `glob` | + | [glob.quote_meta](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-glob-globquote_meta) | `glob` | + +- [Bitwise](https://www.openpolicyagent.org/docs/latest/policy-reference/#regex) + | Builtin | Feature | + |------------------------------------------------------------------------------------------------------|---------| + | [bits.and](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitsand) | _ | + | [bits.lsh](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitslsh) | _ | + | [bits.negate](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitsnegate) | _ | + | [bits.or](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitsor) | _ | + | [bits.rsh](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitsrsh) | _ | + | [bits.xor](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitsxor) | _ | + +- [Conversions](https://www.openpolicyagent.org/docs/latest/policy-reference/#conversions) + | Builtin | Feature | + |-------|---------| + [to_number](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-conversions-to_number) | _ | +| +- [Units](https://www.openpolicyagent.org/docs/latest/policy-reference/#units) + | Builtin | Feature | + |-------------------------------------------------------------------------------------------------------------------|---------| + | [units.parse](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-units-unitsparse) | _ | + | [units.parse_bytes](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-units-unitsparse_bytes) | _ | + +- [Types](https://www.openpolicyagent.org/docs/latest/policy-reference/#types) + | Builtin | Feature | + |------------------------------------------------------------------------------------------------------|---------| + | [is_array](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_array) | _ | + | [is_boolean](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_boolean) | _ | + | [is_null](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_null) | _ | + | [is_number](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_number) | _ | + | [is_object](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_object) | _ | + | [is_set](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_set) | _ | + | [is_string](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_string) | _ | + | [type_name](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-type_name) | _ | + +- [Encoding](https://www.openpolicyagent.org/docs/latest/policy-reference/#encoding) + | Builtin | Feature | + |----------------------------------------------------------------------------------------------------------------------------------|-------------| + | [base64.is_valid](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-base64is_valid) | `base64` | + | [base64url.decode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-base64urldecode) | `base64` | + | [base64url.encode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-base64urlencode) | `base64url` | + | [base64url.encode_no_pad](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-base64urlencode_no_pad) | `base64url` | + | [hex.decode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-hexdecode) | `hex` | + | [hex.encode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-hexencode) | `hex` | + | [json.is_valid](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-jsonis_valid) | _ | + | [json.marshal](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-jsonmarshal) | _ | + | [json.unmarshal](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-jsonunmarshal) | _ | + | [urlquery.decode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-urlquerydecode) | `urlquery` | + | [urlquery.decode_object](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-urlquerydecode_object) | `urlquery` | + | [urlquery.encode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-urlqueryencode) | `urlquery` | + | [urlquery.encode_object](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-urlqueryencode_object) | `urlquery` | + | [yaml.is_valid](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-yamlis_valid) | `yaml` | + | [yaml.marshal](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-yamlmarshal) | `yaml` | + | [yaml.unmarshal](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-yamlunmarshal) | `yaml` | + +- [Time](https://www.openpolicyagent.org/docs/latest/policy-reference/#time) + | Builtin | Feature | + |----------------------------------------------------------------------------------------------------------------------------|---------| + | ([time.add_date](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeadd_date) | `time` | + | [time.add_date](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeadd_date) | `time` | + | [time.clock](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeclock) | `time` | + | [time.date](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timedate) | `time` | + | [time.diff](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timediff) | `time` | + | [time.format](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeformat) | `time` | + | [time.now_ns](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timenow_ns) | `time` | + | [time.parse_duration_ns](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeparse_duration_ns) | `time` | + | [time.parse_ns](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeparse_ns) | `time` | + | [time.parse_rfc3339_ns](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeparse_rfc3339_ns) | `time` | + | [time.weekday](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeweekday) | `time` | + +- [Cryptography](https://www.openpolicyagent.org/docs/latest/policy-reference/#crypto) + | Builtin | Feature | + |---------------------------------------------------------------------------------------------------------------------|----------| + | [crypto.hmac.equal](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptohmacequal) | `crypto` | + | [crypto.hmac.md5](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptohmacmd5) | `crypto` | + | [crypto.hmac.sha1](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptohmacsha1) | `crypto` | + | [crypto.hmac.sha256](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptohmacsha256) | `crypto` | + | [crypto.hmac.sha512](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptohmacsha512) | `crypto` | + | [crypto.md5](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptomd5) | `crypto` | + | [crypto.sha1](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptosha1) | `crypto` | + | [crypto.sha256](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptosha256) | `crypto` | + +- [Graphs](https://www.openpolicyagent.org/docs/latest/policy-reference/#graph) + | Builtin | Feature | + |---------------------------------------------------------------------------------------------------------------|---------| + | [graph.reachable](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-graph-graphreachable) | `graph` | + | [walk](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-graph-walk) | `graph` | + +- [UUID](https://www.openpolicyagent.org/docs/latest/policy-reference/#uuid) + | Builtin | Feature | + |--------------------------------------------------------------------------------------------------------|---------| + | [uuid.parse](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-uuid-uuidparse) | `uuid` | + | [uuid.rfc4122](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-uuid-uuidrfc4122) | `uuid` | + +- [Semantic Versions](https://www.openpolicyagent.org/docs/latest/policy-reference/#semver) + | Builtin | Feature | + |----------------------------------------------------------------------------------------------------------------|----------| + | [semver.compare](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-semver-semvercompare) | `semver` | + | [semver.is_valid](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-semver-semveris_valid) | `semver` | + +- [OPA](https://www.openpolicyagent.org/docs/latest/policy-reference/#opa + | Builtin | Feature | + |-----------------------------------------------------------------------------------------------------|---------| + | [opa.runtime](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-opa-oparuntime) | _ | + +- [Debugging](https://www.openpolicyagent.org/docs/latest/policy-reference/#opa) + | Builtin | Feature | + |---------------------------------------------------------------------------------|---------| + | [print(...)](https://www.openpolicyagent.org/docs/latest/policy-reference/#opa) | _ | + +- [Tracing](https://www.openpolicyagent.org/docs/latest/policy-reference/#tracing) + | Builtin | Feature | + |----------------------------------------------------------------------------------------------|---------| + | [trace](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-tracing-trace) | _ | diff --git a/snippets/2.rego b/snippets/2.rego deleted file mode 100644 index 5f30ffb4..00000000 --- a/snippets/2.rego +++ /dev/null @@ -1,23 +0,0 @@ -Cpackage play - -a := {4} - -mydoc(x) := path { - path := "data.play.a" -} - -x := [ y | - y := data.play.a | data.play.b with data.play.a as {5} with data.play.b as {6} -] - -r := [ m | m := data.play.p with data.play.p as 5 + 6; true ] - - -allow { - input.x - == 5 - - input.y == 5 - input.y - == 5 -} \ No newline at end of file diff --git a/src/builtins/debugging.rs b/src/builtins/debugging.rs index bb77b141..b005c288 100644 --- a/src/builtins/debugging.rs +++ b/src/builtins/debugging.rs @@ -28,11 +28,14 @@ fn print(span: &Span, _params: &[Ref], args: &[Value], _strict: bool) -> R let mut msg = String::default(); for a in args { match a { - Value::Undefined => msg += "", - _ => msg += format!("{a}").as_str(), + Value::Undefined => msg += " ", + Value::String(s) => msg += &format!(" {s}"), + _ => msg += &format!(" {a}"), }; } - span.message("print", msg.as_str()); + if !msg.is_empty() { + println!("{}", &msg[1..]); + } Ok(Value::Bool(true)) } diff --git a/src/builtins/encoding.rs b/src/builtins/encoding.rs index e2326b57..16a51d66 100644 --- a/src/builtins/encoding.rs +++ b/src/builtins/encoding.rs @@ -3,14 +3,16 @@ use crate::ast::{Expr, Ref}; use crate::builtins; +#[allow(unused)] use crate::builtins::utils::{ ensure_args_count, ensure_object, ensure_string, ensure_string_collection, }; use crate::lexer::Span; use crate::value::Value; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; +#[allow(unused)] use anyhow::{anyhow, bail, Context, Result}; pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { @@ -41,11 +43,6 @@ pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { m.insert("json.is_valid", (json_is_valid, 1)); m.insert("json.marshal", (json_marshal, 1)); m.insert("json.unmarshal", (json_unmarshal, 1)); - #[cfg(feature = "jsonschema")] - { - m.insert("json.match_schema", (json_match_schema, 2)); - m.insert("json.verify_schema", (json_verify_schema, 1)); - } #[cfg(feature = "yaml")] { @@ -240,7 +237,7 @@ fn urlquery_decode_object( Err(_) => bail!(params[0].span().error("not a valid url query")), }; - let mut map = BTreeMap::new(); + let mut map = std::collections::BTreeMap::new(); for (k, v) in url.query_pairs() { let key = Value::String(k.clone().into()); let value = Value::String(v.clone().into()); @@ -382,72 +379,3 @@ fn json_unmarshal( let json_str = ensure_string(name, ¶ms[0], &args[0])?; Value::from_json_str(&json_str).with_context(|| span.error("could not deserialize json.")) } - -#[cfg(feature = "jsonschema")] -fn compile_json_schema(param: &Ref, arg: &Value) -> Result { - let schema_str = match arg { - Value::String(schema_str) => schema_str.as_ref().to_string(), - _ => arg.to_json_str()?, - }; - - if let Ok(schema) = serde_json::from_str(&schema_str) { - match jsonschema::JSONSchema::compile(&schema) { - Ok(schema) => return Ok(schema), - Err(e) => bail!(e.to_string()), - } - } - bail!(param.span().error("not a valid json schema")) -} - -#[cfg(feature = "jsonschema")] -fn json_verify_schema( - span: &Span, - params: &[Ref], - args: &[Value], - strict: bool, -) -> Result { - let name = "json.verify_schema"; - ensure_args_count(span, name, params, args, 1)?; - - Ok(Value::from_array( - match compile_json_schema(¶ms[0], &args[0]) { - Ok(_) => [Value::Bool(true), Value::Null], - Err(e) if strict => bail!(params[0] - .span() - .error(format!("invalid schema: {e}").as_str())), - Err(e) => [Value::Bool(false), Value::String(e.to_string().into())], - } - .to_vec(), - )) -} - -#[cfg(feature = "jsonschema")] -fn json_match_schema( - span: &Span, - params: &[Ref], - args: &[Value], - strict: bool, -) -> Result { - let name = "json.match_schema"; - ensure_args_count(span, name, params, args, 2)?; - - // The following is expected to succeed. - let document: serde_json::Value = serde_json::from_str(&args[0].to_json_str()?)?; - - Ok(Value::from_array( - match compile_json_schema(¶ms[1], &args[1]) { - Ok(schema) => match schema.validate(&document) { - Ok(_) => [Value::Bool(true), Value::Null], - Err(e) => [ - Value::Bool(false), - Value::from_array(e.map(|e| Value::String(e.to_string().into())).collect()), - ], - }, - Err(e) if strict => bail!(params[1] - .span() - .error(format!("invalid schema: {e}").as_str())), - Err(e) => [Value::Bool(false), Value::String(e.to_string().into())], - } - .to_vec(), - )) -} diff --git a/src/builtins/objects.rs b/src/builtins/objects.rs index be14206e..8cf8053e 100644 --- a/src/builtins/objects.rs +++ b/src/builtins/objects.rs @@ -23,6 +23,12 @@ pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { m.insert("object.subset", (subset, 2)); m.insert("object.union", (object_union, 2)); m.insert("object.union_n", (object_union_n, 1)); + + #[cfg(feature = "jsonschema")] + { + m.insert("json.match_schema", (json_match_schema, 2)); + m.insert("json.verify_schema", (json_verify_schema, 1)); + } } fn json_filter_impl(v: &Value, filter: &Value) -> Value { @@ -382,3 +388,72 @@ fn object_union_n( Ok(u) } + +#[cfg(feature = "jsonschema")] +fn compile_json_schema(param: &Ref, arg: &Value) -> Result { + let schema_str = match arg { + Value::String(schema_str) => schema_str.as_ref().to_string(), + _ => arg.to_json_str()?, + }; + + if let Ok(schema) = serde_json::from_str(&schema_str) { + match jsonschema::JSONSchema::compile(&schema) { + Ok(schema) => return Ok(schema), + Err(e) => bail!(e.to_string()), + } + } + bail!(param.span().error("not a valid json schema")) +} + +#[cfg(feature = "jsonschema")] +fn json_verify_schema( + span: &Span, + params: &[Ref], + args: &[Value], + strict: bool, +) -> Result { + let name = "json.verify_schema"; + ensure_args_count(span, name, params, args, 1)?; + + Ok(Value::from_array( + match compile_json_schema(¶ms[0], &args[0]) { + Ok(_) => [Value::Bool(true), Value::Null], + Err(e) if strict => bail!(params[0] + .span() + .error(format!("invalid schema: {e}").as_str())), + Err(e) => [Value::Bool(false), Value::String(e.to_string().into())], + } + .to_vec(), + )) +} + +#[cfg(feature = "jsonschema")] +fn json_match_schema( + span: &Span, + params: &[Ref], + args: &[Value], + strict: bool, +) -> Result { + let name = "json.match_schema"; + ensure_args_count(span, name, params, args, 2)?; + + // The following is expected to succeed. + let document: serde_json::Value = serde_json::from_str(&args[0].to_json_str()?)?; + + Ok(Value::from_array( + match compile_json_schema(¶ms[1], &args[1]) { + Ok(schema) => match schema.validate(&document) { + Ok(_) => [Value::Bool(true), Value::Null], + Err(e) => [ + Value::Bool(false), + Value::from_array(e.map(|e| Value::String(e.to_string().into())).collect()), + ], + }, + Err(e) if strict => bail!(params[1] + .span() + .error(format!("invalid schema: {e}").as_str())), + Err(e) => [Value::Bool(false), Value::String(e.to_string().into())], + } + .to_vec(), + )) +} diff --git a/src/engine.rs b/src/engine.rs index e41fbcd0..784ec235 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -16,6 +16,7 @@ use std::path::Path; use anyhow::Result; /// The Rego evaluation engine. +/// #[derive(Clone)] pub struct Engine { modules: Vec>, @@ -31,6 +32,7 @@ impl Default for Engine { } impl Engine { + /// Create an instance of [Engine]. pub fn new() -> Self { Self { modules: vec![], @@ -39,6 +41,29 @@ impl Engine { } } + /// Add a policy. + /// + /// The policy file will be parsed and converted to AST representation. + /// Multiple policy files may be added to the engine. + /// + /// * `path`: A filename to be associated with the policy. + /// * `rego`: The rego policy code. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let mut engine = Engine::new(); + /// + /// engine.add_policy( + /// "test.rego".to_string(), + /// r#" + /// package test + /// allow = input.user == "root" + /// "#.to_string())?; + /// # Ok(()) + /// # } + /// ``` + /// pub fn add_policy(&mut self, path: String, rego: String) -> Result<()> { let source = Source::new(path, rego); let mut parser = Parser::new(&source)?; @@ -48,6 +73,22 @@ impl Engine { Ok(()) } + /// Add a policy from a given file. + /// + /// The policy file will be parsed and converted to AST representation. + /// Multiple policy files may be added to the engine. + /// + /// * `path`: Path to the policy file (.rego). + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let mut engine = Engine::new(); + /// + /// engine.add_policy_from_file("tests/aci/framework.rego")?; + /// # Ok(()) + /// # } + /// ``` pub fn add_policy_from_file>(&mut self, path: P) -> Result<()> { let source = Source::from_file(path)?; let mut parser = Parser::new(&source)?; @@ -56,28 +97,163 @@ impl Engine { Ok(()) } + /// Set the input document. + /// + /// * `input`: Input documented. Typically this [Value] is constructed from JSON or YAML. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let mut engine = Engine::new(); + /// + /// let input = Value::from_json_str(r#" + /// { + /// "role" : "admin", + /// "action": "delete" + /// }"#)?; + /// + /// engine.set_input(input); + /// # Ok(()) + /// # } + /// ``` pub fn set_input(&mut self, input: Value) { self.interpreter.set_input(input); } + /// Clear the data document. + /// + /// The data document will be reset to an empty object. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let mut engine = Engine::new(); + /// + /// engine.clear_data(); + /// + /// // Evaluate data. + /// let results = engine.eval_query("data".to_string(), false)?; + /// + /// // Assert that it is empty object. + /// assert_eq!(results.result.len(), 1); + /// assert_eq!(results.result[0].expressions.len(), 1); + /// assert_eq!(results.result[0].expressions[0].value, Value::new_object()); + /// # Ok(()) + /// # } + /// ``` pub fn clear_data(&mut self) { self.interpreter.set_data(Value::new_object()); self.prepared = false; } + /// Add data document. + /// + /// The specified data document is merged into existing data document. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let mut engine = Engine::new(); + /// + /// // Only objects can be added. + /// assert!(engine.add_data(Value::from_json_str("[]")?).is_err()); + /// + /// // Merge { "x" : 1, "y" : {} } + /// assert!(engine.add_data(Value::from_json_str(r#"{ "x" : 1, "y" : {}}"#)?).is_ok()); + /// + /// // Merge { "z" : 2 } + /// assert!(engine.add_data(Value::from_json_str(r#"{ "z" : 2 }"#)?).is_ok()); + /// + /// // Merge { "z" : 3 }. Conflict error. + /// assert!(engine.add_data(Value::from_json_str(r#"{ "z" : 3 }"#)?).is_err()); + /// + /// assert_eq!( + /// engine.eval_query("data".to_string(), false)?.result[0].expressions[0].value, + /// Value::from_json_str(r#"{ "x": 1, "y": {}, "z": 2}"#)? + /// ); + /// # Ok(()) + /// # } + /// ``` pub fn add_data(&mut self, data: Value) -> Result<()> { self.prepared = false; self.interpreter.get_data_mut().merge(data) } + /// Set whether builtins should raise errors strictly or not. + /// + /// Regorus differs from OPA in that by default builtins will + /// raise errors instead of returning Undefined. + /// + /// ---- + /// **_NOTE:_** Currently not all builtins honor this flag and will always strictly raise errors. + /// ---- + pub fn set_strict_builtin_errors(&mut self, b: bool) { + self.interpreter.set_strict_builtin_errors(b) + } + + #[doc(hidden)] pub fn get_modules(&mut self) -> &Vec> { &self.modules } - pub fn set_strict_builtin_errors(&mut self, b: bool) { - self.interpreter.set_strict_builtin_errors(b) + /// Evaluate a Rego query. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let mut engine = Engine::new(); + /// + /// // Add policies + /// engine.add_policy_from_file("tests/aci/framework.rego")?; + /// engine.add_policy_from_file("tests/aci/api.rego")?; + /// engine.add_policy_from_file("tests/aci/policy.rego")?; + /// + /// // Add data document (if any). + /// // If multiple data documents can be added, they will be merged together. + /// engine.add_data(Value::from_json_file("tests/aci/data.json")?)?; + /// + /// // At this point the policies and data have been loaded. + /// // Either the same engine can be used to make multiple queries or the engine + /// // can be cloned to avoid having the reload the policies and data. + /// let _clone = engine.clone(); + /// + /// // Evaluate a query. + /// // Load input and make query. + /// engine.set_input(Value::new_object()); + /// let results = engine.eval_query("data.framework.mount_overlay.allowed".to_string(), false)?; + /// assert!(results.result.is_empty()); + /// + /// // Evaluate query with different inputs. + /// engine.set_input(Value::from_json_file("tests/aci/input.json")?); + /// let results = engine.eval_query("data.framework.mount_overlay.allowed".to_string(), false)?; + /// assert_eq!(results.result[0].expressions[0].value, Value::from(true)); + /// # Ok(()) + /// # } + pub fn eval_query(&mut self, query: String, enable_tracing: bool) -> Result { + self.eval_modules(enable_tracing)?; + + let query_module = { + let source = Source::new( + "".to_owned(), + "package __internal_query_module".to_owned(), + ); + Ref::new(Parser::new(&source)?.parse()?) + }; + + // Parse the query. + let query_source = Source::new("".to_string(), query); + let mut parser = Parser::new(&query_source)?; + let query_node = parser.parse_user_query()?; + let query_schedule = Analyzer::new().analyze_query_snippet(&self.modules, &query_node)?; + self.interpreter.eval_user_query( + &query_module, + &query_node, + &query_schedule, + enable_tracing, + ) } + #[doc(hidden)] fn prepare_for_eval(&mut self, enable_tracing: bool) -> Result<()> { self.interpreter.set_traces(enable_tracing); @@ -110,6 +286,7 @@ impl Engine { Ok(()) } + #[doc(hidden)] pub fn eval_rule( &mut self, module: &Ref, @@ -124,6 +301,7 @@ impl Engine { Ok(self.interpreter.get_data_mut().clone()) } + #[doc(hidden)] pub fn eval_modules(&mut self, enable_tracing: bool) -> Result { self.prepare_for_eval(enable_tracing)?; self.interpreter.clean_internal_evaluation_state(); @@ -167,30 +345,4 @@ impl Engine { self.interpreter.create_rule_prefixes()?; Ok(self.interpreter.get_data_mut().clone()) } - - pub fn eval_query(&mut self, query: String, enable_tracing: bool) -> Result { - self.eval_modules(false)?; - - let query_module = { - let source = Source::new( - "".to_owned(), - "package __internal_query_module".to_owned(), - ); - Ref::new(Parser::new(&source)?.parse()?) - }; - - // Parse the query. - let query_source = Source::new("".to_string(), query); - let mut parser = Parser::new(&query_source)?; - let query_node = parser.parse_user_query()?; - let query_schedule = Analyzer::new().analyze_query_snippet(&self.modules, &query_node)?; - - let results = self.interpreter.eval_user_query( - &query_module, - &query_node, - &query_schedule, - enable_tracing, - )?; - Ok(results) - } } diff --git a/src/interpreter.rs b/src/interpreter.rs index ace9b6ad..8b8e154f 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -235,6 +235,7 @@ impl Interpreter { self.loop_var_values.clear(); self.scopes = vec![Scope::new()]; self.contexts = vec![]; + self.rule_values.clear(); } fn current_module(&self) -> Result> { @@ -347,7 +348,7 @@ impl Interpreter { && get_root_var(refr)?.text() == "data" { let index = index.to_string(); - v = obj[&index].clone(); + v = obj[index].clone(); } return Ok(Self::get_value_chained(v, &path[..])); } @@ -1310,6 +1311,13 @@ impl Interpreter { r } + fn clear_scope(scope: &mut Scope) { + // Set each value to undefined. This is equivalent to removing the key. + for (_, v) in scope.iter_mut() { + *v = Value::Undefined; + } + } + fn eval_stmts_in_loop(&mut self, stmts: &[&LiteralStmt], loops: &[LoopExpr]) -> Result { if loops.is_empty() { if !stmts.is_empty() { @@ -1373,9 +1381,8 @@ impl Interpreter { } } - // Save the current scope and restore it after evaluating the statements so - // that the effects of the current loop iteration are cleared. - let scope_saved = self.current_scope()?.clone(); + // Create a new scope. + self.scopes.push(Scope::default()); let query_result = self.get_current_context()?.result.clone(); match loop_expr_value { @@ -1401,12 +1408,13 @@ impl Interpreter { result = self.eval_stmts_in_loop(stmts, &loops[1..])? || result; } - self.loop_var_values.remove(&loop_expr.expr()); - *self.current_scope_mut()? = scope_saved.clone(); + Self::clear_scope(self.current_scope_mut()?); if let Some(ctx) = self.contexts.last_mut() { ctx.result = query_result.clone(); } } + + self.loop_var_values.remove(&loop_expr.expr()); } Value::Set(items) => { for v in items.iter() { @@ -1424,12 +1432,12 @@ impl Interpreter { result = self.eval_stmts_in_loop(stmts, &loops[1..])? || result; } - self.loop_var_values.remove(&loop_expr.expr()); - *self.current_scope_mut()? = scope_saved.clone(); + Self::clear_scope(self.current_scope_mut()?); if let Some(ctx) = self.contexts.last_mut() { ctx.result = query_result.clone(); } } + self.loop_var_values.remove(&loop_expr.expr()); } Value::Object(obj) => { for (k, v) in obj.iter() { @@ -1445,12 +1453,13 @@ impl Interpreter { if exec { result = self.eval_stmts_in_loop(stmts, &loops[1..])? || result; } - self.loop_var_values.remove(&loop_expr.expr()); - *self.current_scope_mut()? = scope_saved.clone(); + + Self::clear_scope(self.current_scope_mut()?); if let Some(ctx) = self.contexts.last_mut() { ctx.result = query_result.clone(); } } + self.loop_var_values.remove(&loop_expr.expr()); } Value::Undefined => { result = false; @@ -1461,6 +1470,8 @@ impl Interpreter { } } + self.scopes.pop(); + // Return true if at least on iteration returned true Ok(result) } @@ -1692,7 +1703,7 @@ impl Interpreter { if result .expressions .iter() - .all(|v| v.value != Value::Undefined) + .all(|v| v.value != Value::Undefined && v.value != Value::Bool(false)) && !result.expressions.is_empty() { ctx.results.result.push(result); @@ -1811,7 +1822,7 @@ impl Interpreter { if result .expressions .iter() - .all(|v| v.value != Value::Undefined) + .all(|v| v.value != Value::Undefined && v.value != Value::Bool(false)) && !result.expressions.is_empty() { ctx.results.result.push(result); @@ -2027,10 +2038,12 @@ impl Interpreter { // Handle trace function. // TODO: with modifier. - if let (Some(traces), Value::String(msg)) = (&mut self.traces, &v) { - traces.push(msg.clone()); - return Ok(Value::Bool(true)); - }; + if name == "trace" { + if let (Some(traces), Value::String(msg)) = (&mut self.traces, &v) { + traces.push(msg.clone()); + return Ok(Value::Bool(true)); + } + } if let Some(name) = cache { self.builtins_cache.insert((name, args), v.clone()); @@ -2374,6 +2387,14 @@ impl Interpreter { self.eval_rule(&module, rule)?; } } + + let prev_module = self.set_current_module(Some(module.clone()))?; + for rule in &module.policy { + if !self.processed.contains(rule) { + self.eval_default_rule(rule)?; + } + } + self.set_current_module(prev_module)?; } } Ok(()) @@ -3186,7 +3207,7 @@ impl Interpreter { self.set_current_module(prev_module)?; if let Some(r) = results.result.last() { - if r.bindings.is_empty_object() + if matches!(&r.bindings, Value::Object(obj) if obj.is_empty()) && r.expressions.iter().any(|e| e.value == Value::Bool(false)) { results = QueryResults::default(); diff --git a/src/lib.rs b/src/lib.rs index 3944e78b..03824a76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -139,7 +139,7 @@ pub struct Expression { /// # } /// ``` /// -/// If any expression evaluates to false, then no results are produces. +/// If any expression evaluates to false, then no results are produced. /// ``` /// # use regorus::*; /// # fn main() -> anyhow::Result<()> { @@ -172,13 +172,96 @@ impl Default for QueryResult { } } +/// Results of evaluating a Rego query. +/// +/// Generates the same `json` representation as `opa eval`. +/// +/// Queries typically produce a single result. +/// ``` +/// # use regorus::*; +/// # fn main() -> anyhow::Result<()> { +/// // Create engine and evaluate "true; true; false". +/// let results = Engine::new().eval_query("1 + 1".to_string(), false)?; +/// +/// assert!(results.result.len() == 1); +/// assert_eq!(results.result[0].expressions[0].value, Value::from(2u64)); +/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "1 + 1"); +/// # Ok(()) +/// # } +/// ``` +/// +/// If any expression evaluates to false, then no results are produced. +/// ``` +/// # use regorus::*; +/// # fn main() -> anyhow::Result<()> { +/// // Create engine and evaluate "true; true; false". +/// let results = Engine::new().eval_query("true; true; false".to_string(), false)?; +/// +/// assert!(results.result.is_empty()); +/// # Ok(()) +/// # } +/// ``` +/// +/// Queries containing loops produce multiple results. +/// ``` +/// # use regorus::*; +/// # fn main() -> anyhow::Result<()> { +/// let results = Engine::new().eval_query("x = [1, 2, 3][_]".to_string(), false)?; +/// +/// // Three results are produced, one of each value of x. +/// assert_eq!(results.result.len(), 3); +/// +/// // Assert expressions and bindings of results. +/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true)); +/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "x = [1, 2, 3][_]"); +/// assert_eq!(results.result[0].bindings[&Value::from("x")], Value::from(1u64)); +/// +/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true)); +/// assert_eq!(results.result[1].expressions[0].text.as_ref(), "x = [1, 2, 3][_]"); +/// assert_eq!(results.result[1].bindings[&Value::from("x")], Value::from(2u64)); +/// +/// assert_eq!(results.result[2].expressions[0].value, Value::Bool(true)); +/// assert_eq!(results.result[2].expressions[0].text.as_ref(), "x = [1, 2, 3][_]"); +/// assert_eq!(results.result[2].bindings[&Value::from("x")], Value::from(3u64)); +/// # Ok(()) +/// # } +/// ``` +/// +/// Loop iterations that evaluate to false or undefined don't produce results. +/// ``` +/// # use regorus::*; +/// # fn main() -> anyhow::Result<()> { +/// let results = Engine::new().eval_query("x = [1, 2, 3][_]; x >= 2".to_string(), false)?; +/// +/// // Two results are produced, one for x = 2 and another for x = 3. +/// assert_eq!(results.result.len(), 2); +/// +/// // Assert expressions and bindings of results. +/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true)); +/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "x = [1, 2, 3][_]"); +/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true)); +/// assert_eq!(results.result[0].expressions[1].text.as_ref(), "x >= 2"); +/// assert_eq!(results.result[0].bindings[&Value::from("x")], Value::from(2u64)); +/// +/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true)); +/// assert_eq!(results.result[1].expressions[0].text.as_ref(), "x = [1, 2, 3][_]"); +/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true)); +/// assert_eq!(results.result[1].expressions[1].text.as_ref(), "x >= 2"); +/// assert_eq!(results.result[1].bindings[&Value::from("x")], Value::from(3u64)); +/// # Ok(()) +/// # } +/// ``` +/// +/// See [QueryResult] for examples of different kinds of results. #[derive(Debug, Clone, Default, Serialize)] pub struct QueryResults { + /// Collection of results of evaluting a query. #[serde(skip_serializing_if = "Vec::is_empty")] pub result: Vec, } /// Items in `unstable` are likely to change. +#[doc(hidden)] pub mod unstable { pub use crate::ast::*; pub use crate::lexer::*; diff --git a/src/number.rs b/src/number.rs index 385854c3..8b7eac4f 100644 --- a/src/number.rs +++ b/src/number.rs @@ -132,6 +132,26 @@ impl From for Number { } impl Number { + pub fn as_u128(&self) -> Option { + match self { + Big(b) if b.is_integer() => match u128::try_from(&b.d) { + Ok(v) => Some(v), + _ => None, + }, + _ => None, + } + } + + pub fn as_i128(&self) -> Option { + match self { + Big(b) if b.is_integer() => match i128::try_from(&b.d) { + Ok(v) => Some(v), + _ => None, + }, + _ => None, + } + } + pub fn as_u64(&self) -> Option { match self { Big(b) if b.is_integer() => match u64::try_from(&b.d) { diff --git a/src/scheduler.rs b/src/scheduler.rs index d5ce0697..77c7be3e 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -3,7 +3,6 @@ use crate::ast::Expr::*; use crate::ast::*; -use crate::builtins; use crate::lexer::*; use crate::utils::*; @@ -629,6 +628,7 @@ impl Analyzer { let mut used_vars = vec![]; let mut comprs = vec![]; let full_expr = expr; + std::convert::identity(&full_expr); traverse(expr, &mut |e| match e.as_ref() { Var(v) if !matches!(v.text(), "_" | "input" | "data") => { let name = v.source_str(); @@ -645,15 +645,18 @@ impl Analyzer { first_use.entry(name).or_insert(v.clone()); } } else if !scope.inputs.contains(&name) { - match get_path_string(full_expr, None) { - Ok(path) - if builtins::BUILTINS.contains_key(path.as_str()) - || builtins::deprecated::DEPRECATED.contains_key(path.as_str()) => { + #[cfg(feature = "deprecated")] + { + if let Ok(path) = get_path_string(full_expr, None) { + if crate::builtins::BUILTINS.contains_key(path.as_str()) + || crate::builtins::deprecated::DEPRECATED + .contains_key(path.as_str()) + { + return Ok(false); + } } - _ => bail!(v.error( - format!("use of undefined variable `{name}` is unsafe").as_str() - )), } + bail!(v.error(format!("use of undefined variable `{name}` is unsafe").as_str())); } Ok(false) } diff --git a/src/value.rs b/src/value.rs index 35f8b441..250365dd 100644 --- a/src/value.rs +++ b/src/value.rs @@ -5,7 +5,9 @@ use crate::number::Number; use core::fmt; use std::collections::{BTreeMap, BTreeSet}; +use std::convert::AsRef; use std::ops; +use std::path::Path; use std::rc::Rc; use std::str::FromStr; @@ -14,27 +16,49 @@ use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor}; use serde::ser::{SerializeMap, Serializer}; use serde::{Deserialize, Serialize}; -// We cannot use serde_json::Value because Rego has set type and object's key can be -// other rego values. -// BTree is more efficient than a hash table. Another alternative is a sorted vector. +/// A value in a Rego document. +/// +/// Value is similar to a [`serde_json::value::Value`], but has the following additional +/// capabilities: +/// - [`Value::Set`] variant to represent sets. +/// - [`Value::Undefined`] variant to represent absence of value. +// - [`Value::Object`] keys can be other values, not just strings. +/// - [`Value::Number`] has at least 100 digits of precision for computations. +/// +/// Value can be efficiently cloned due to the use of reference counting. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum Value { - // Json data types. serde will automatically map json to these variants. + /// JSON null. Null, + + /// JSON boolean. Bool(bool), + + /// JSON number. + /// At least 100 digits of precision. Number(Number), + + /// JSON string. String(Rc), + + /// JSON array. Array(Rc>), - // Extra rego data type + /// A set of values. + /// No JSON equivalent. + /// Sets are serialized as arrays in JSON. Set(Rc>), + /// An object. + /// Unlike JSON, keys can be any value, not just string. Object(Rc>), - // Indicate that a value is undefined + /// Undefined value. + /// Used to indicate the absence of a value. Undefined, } +#[doc(hidden)] impl Serialize for Value { fn serialize(&self, serializer: S) -> Result where @@ -178,6 +202,7 @@ impl<'de> Visitor<'de> for ValueVisitor { } } +#[doc(hidden)] impl<'de> Deserialize<'de> for Value { fn deserialize(deserializer: D) -> Result where @@ -188,6 +213,18 @@ impl<'de> Deserialize<'de> for Value { } impl fmt::Display for Value { + /// Display a value. + /// + /// A value is displayed by serializing it to JSON using serde_json::to_string. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from("hello"); + /// assert_eq!(format!("{v}"), "\"hello\""); + /// # Ok(()) + /// # } + /// ``` fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match serde_json::to_string(self) { Ok(s) => write!(f, "{s}"), @@ -197,38 +234,176 @@ impl fmt::Display for Value { } impl Value { + /// Create an empty [`Value::Array`] + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let obj = Value::new_array(); + /// assert_eq!(obj.as_array().expect("not an array").len(), 0); + /// # Ok(()) + /// # } + /// ``` + pub fn new_array() -> Value { + Value::from(vec![]) + } + + /// Create an empty [`Value::Object`] + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let obj = Value::new_object(); + /// assert_eq!(obj.as_object().expect("not an object").len(), 0); + /// # Ok(()) + /// # } + /// ``` pub fn new_object() -> Value { Value::from(BTreeMap::new()) } + /// Create an empty [`Value::Set`] + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let obj = Value::new_set(); + /// assert_eq!(obj.as_set().expect("not a set").len(), 0); + /// # Ok(()) + /// # } + /// ``` pub fn new_set() -> Value { Value::from(BTreeSet::new()) } +} - pub fn new_array() -> Value { - Value::from(vec![]) - } - +impl Value { + /// Deserialize a [`Value`] from JSON. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let json = r#" + /// [ + /// null, true, false, + /// "hello", 12345, + /// { "name" : "regorus" } + /// ]"#; + /// + /// // Deserialize json. + /// let value = Value::from_json_str(json)?; + /// + /// // Assert outer array. + /// let array = value.as_array().expect("not an array"); + /// + /// // Assert elements. + /// assert_eq!(array[0], Value::Null); + /// assert_eq!(array[1], Value::from(true)); + /// assert_eq!(array[2], Value::from(false)); + /// assert_eq!(array[3], Value::from("hello")); + /// assert_eq!(array[4], Value::from(12345u64)); + /// let obj = array[5].as_object().expect("not an object"); + /// assert_eq!(obj.len(), 1); + /// assert_eq!(obj[&Value::from("name")], Value::from("regorus")); + /// # Ok(()) + /// # } + /// ``` pub fn from_json_str(json: &str) -> Result { Ok(serde_json::from_str(json)?) } - pub fn to_json_str(&self) -> Result { - Ok(serde_json::to_string_pretty(self)?) - } - - pub fn from_json_file(path: &String) -> Result { - match std::fs::read_to_string(path) { + /// Deserialize a [`Value`] from a file containing JSON. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let value = Value::from_json_file("tests/aci/input.json")?; + /// + /// // Convert the value back to json. + /// let json_str = value.to_json_str()?; + /// + /// assert_eq!(json_str.trim(), std::fs::read_to_string("tests/aci/input.json")?.trim()); + /// # Ok(()) + /// # } + /// ``` + pub fn from_json_file>(path: P) -> Result { + match std::fs::read_to_string(&path) { Ok(c) => Self::from_json_str(c.as_str()), - Err(e) => bail!("Failed to read {path}. {e}"), + Err(e) => bail!("Failed to read {}. {e}", path.as_ref().display()), } } + /// Serialize a value to JSON. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let value = Value::from_json_file("tests/aci/input.json")?; + /// + /// // Convert the value back to json. + /// let json_str = value.to_json_str()?; + /// + /// assert_eq!(json_str.trim(), std::fs::read_to_string("tests/aci/input.json")?.trim()); + /// # Ok(()) + /// # } + /// ``` + /// + /// Sets are serialized as arrays. + /// ``` + /// # use regorus::*; + /// # use std::collections::BTreeSet; + /// # fn main() -> anyhow::Result<()> { + /// let mut set = BTreeSet::new(); + /// set.insert(Value::from("Hello")); + /// set.insert(Value::from(1u64)); + /// + /// let set_value = Value::from(set); + /// + /// assert_eq!( + /// set_value.to_json_str()?, + /// r#" + ///[ + /// 1, + /// "Hello" + ///]"#.trim()); + /// # Ok(()) + /// # } + /// ``` + /// + /// Non string keys of objects are serialized to json first and the serialized string representation + /// is emitted as the key. + /// ``` + /// # use regorus::*; + /// # use std::collections::BTreeMap; + /// # fn main() -> anyhow::Result<()> { + /// let mut obj = BTreeMap::new(); + /// obj.insert(Value::from("Hello"), Value::from("World")); + /// obj.insert(Value::from([Value::from(1u64)].to_vec()), Value::Null); + /// + /// let obj_value = Value::from(obj); + /// + /// assert_eq!( + /// obj_value.to_json_str()?, + /// r#" + ///{ + /// "Hello": "World", + /// "[1]": null + ///}"#.trim()); + /// # Ok(()) + /// # } + /// ``` + pub fn to_json_str(&self) -> Result { + Ok(serde_json::to_string_pretty(self)?) + } + + /// Deserialize a value from YAML. + /// Note: Deserialization from YAML does not support arbitrary precision numbers. #[cfg(feature = "yaml")] pub fn from_yaml_str(yaml: &str) -> Result { Ok(serde_yaml::from_str(yaml)?) } + /// Deserialize a value from a file containing YAML. + /// Note: Deserialization from YAML does not support arbitrary precision numbers. #[cfg(feature = "yaml")] pub fn from_yaml_file(path: &String) -> Result { match std::fs::read_to_string(path) { @@ -239,59 +414,214 @@ impl Value { } impl From for Value { + /// Create a [`Value::Bool`] from `bool`. + /// ``` + /// # use regorus::*; + /// # use std::collections::BTreeSet; + /// # fn main() -> anyhow::Result<()> { + /// assert_eq!(Value::from(true), Value::Bool(true)); + /// # Ok(()) + /// # } fn from(b: bool) -> Self { Value::Bool(b) } } impl From for Value { + /// Create a [`Value::String`] from `string`. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// assert_eq!(Value::from("Hello".to_string()), Value::String("Hello".into())); + /// # Ok(()) + /// # } fn from(s: String) -> Self { Value::String(s.into()) } } impl From<&str> for Value { + /// Create a [`Value::String`] from `&str`. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// assert_eq!(Value::from("Hello"), Value::String("Hello".into())); + /// # Ok(()) + /// # } fn from(s: &str) -> Self { Value::String(s.into()) } } impl From for Value { + /// Create a [`Value::Number`] from `u128`. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// assert_eq!( + /// Value::from(340_282_366_920_938_463_463_374_607_431_768_211_455u128), + /// Value::from_json_str("340282366920938463463374607431768211455")?); + /// # Ok(()) + /// # } fn from(n: u128) -> Self { Value::Number(Number::from(n)) } } impl From for Value { + /// Create a [`Value::Number`] from `i128`. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// assert_eq!( + /// Value::from(-170141183460469231731687303715884105728i128), + /// Value::from_json_str("-170141183460469231731687303715884105728")?); + /// # Ok(()) + /// # } fn from(n: i128) -> Self { Value::Number(Number::from(n)) } } impl From for Value { + /// Create a [`Value::Number`] from `u64`. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// assert_eq!( + /// Value::from(0u64), + /// Value::from_json_str("0")?); + /// # Ok(()) + /// # } fn from(n: u64) -> Self { Value::Number(Number::from(n)) } } impl From for Value { + /// Create a [`Value::Number`] from `i64`. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// assert_eq!( + /// Value::from(0i64), + /// Value::from_json_str("0")?); + /// # Ok(()) + /// # } fn from(n: i64) -> Self { Value::Number(Number::from(n)) } } +impl From for Value { + /// Create a [`Value::Number`] from `u32`. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// assert_eq!( + /// Value::from(0u32), + /// Value::from_json_str("0")?); + /// # Ok(()) + /// # } + fn from(n: u32) -> Self { + Value::Number(Number::from(n as u64)) + } +} + +impl From for Value { + /// Create a [`Value::Number`] from `i32`. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// assert_eq!( + /// Value::from(0i32), + /// Value::from_json_str("0")?); + /// # Ok(()) + /// # } + fn from(n: i32) -> Self { + Value::Number(Number::from(n as i64)) + } +} + impl From for Value { + /// Create a [`Value::Number`] from `f64`. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// assert_eq!( + /// Value::from(3.141592653589793), + /// Value::from_json_str("3.141592653589793")?); + /// # Ok(()) + /// # } + /// ``` + /// + /// Note, f64 can store only around 15 digits of precision whereas [`Value::Number`] + /// can store arbitrary precision. Adding an extra digit to the f64 literal in the above + /// example causes loss of precision and the Value created from f64 does not match the + /// Value parsed from json string (which is more precise). + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// // The last digit is lost in f64. + /// assert_ne!( + /// Value::from(3.1415926535897932), + /// Value::from_json_str("3.141592653589793232")?); + /// + /// // The value, in this case is equal to parsing the json number with last digit omitted. + /// assert_ne!( + /// Value::from(3.1415926535897932), + /// Value::from_json_str("3.14159265358979323")?); + /// # Ok(()) + /// # } + /// ``` + /// + /// If precision is important, it is better to construct numeric values from strings instead + /// of f64 when possible. + /// See [Value::from_numeric_string] fn from(n: f64) -> Self { Value::Number(Number::from(n)) } } +impl Value { + /// Create a [`Value::Number`] from a string containing numeric representation of a number. + /// + /// This is the preferred way for creating arbitrary precision numbers. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from_numeric_string("3.14159265358979323846264338327950288419716939937510")?; + /// + /// assert_eq!( + /// v.to_json_str()?, + /// "3.1415926535897932384626433832795028841971693993751"); + /// # Ok(()) + /// # } + /// ``` + pub fn from_numeric_string(s: &str) -> Result { + Ok(Value::Number( + Number::from_str(s).map_err(|_| anyhow!("not a valid numeric string"))?, + )) + } +} + impl From for Value { + /// Create a [`Value::Number`] from `usize`. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// assert_eq!( + /// Value::from(0usize), + /// Value::from_json_str("0")?); + /// # Ok(()) + /// # } fn from(n: usize) -> Self { Value::Number(Number::from(n)) } } +#[doc(hidden)] impl From for Value { fn from(n: Number) -> Self { Value::Number(n) @@ -299,48 +629,92 @@ impl From for Value { } impl From> for Value { + /// Create a [`Value::Array`] from a [`Vec`]. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let strings = [ "Hello", "World" ]; + /// + /// let v = Value::from(strings.iter().map(|s| Value::from(*s)).collect::>()); + /// assert_eq!(v[0], Value::from(strings[0])); + /// assert_eq!(v[1], Value::from(strings[1])); + /// # Ok(()) + /// # } fn from(a: Vec) -> Self { Value::Array(Rc::new(a)) } } impl From> for Value { + /// Create a [`Value::Set`] from a [`BTreeSet`]. + /// ``` + /// # use regorus::*; + /// # use std::collections::BTreeSet; + /// # fn main() -> anyhow::Result<()> { + /// let strings = [ "Hello", "World" ]; + /// let v = Value::from(strings + /// .iter() + /// .map(|s| Value::from(*s)) + /// .collect::>()); + /// + /// let mut iter = v.as_set()?.iter(); + /// assert_eq!(iter.next(), Some(&Value::from(strings[0]))); + /// assert_eq!(iter.next(), Some(&Value::from(strings[1]))); + /// # Ok(()) + /// # } fn from(s: BTreeSet) -> Self { Value::Set(Rc::new(s)) } } impl From> for Value { + /// Create a [`Value::Object`] from a [`BTreeMap`]. + /// ``` + /// # use regorus::*; + /// # use std::collections::BTreeMap; + /// # fn main() -> anyhow::Result<()> { + /// let strings = [ ("Hello", "World") ]; + /// let v = Value::from(strings + /// .iter() + /// .map(|(k,v)| (Value::from(*k), Value::from(*v))) + /// .collect::>()); + /// + /// let mut iter = v.as_object()?.iter(); + /// assert_eq!(iter.next(), Some((&Value::from(strings[0].0), &Value::from(strings[0].1)))); + /// # Ok(()) + /// # } fn from(s: BTreeMap) -> Self { Value::Object(Rc::new(s)) } } impl Value { - pub fn from_array(a: Vec) -> Value { + pub(crate) fn from_array(a: Vec) -> Value { Value::from(a) } - pub fn from_set(s: BTreeSet) -> Value { + pub(crate) fn from_set(s: BTreeSet) -> Value { Value::from(s) } - pub fn from_map(m: BTreeMap) -> Value { + pub(crate) fn from_map(m: BTreeMap) -> Value { Value::from(m) } - pub fn is_null(&self) -> bool { - matches!(self, Value::Null) - } - - pub fn is_undefined(&self) -> bool { - matches!(self, Value::Null) - } - - pub fn is_empty_object(&self) -> bool { + pub(crate) fn is_empty_object(&self) -> bool { self == &Value::new_object() } +} +impl Value { + /// Cast value to [`& bool`] if [`Value::Bool`]. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from(true); + /// assert_eq!(v.as_bool()?, &true); + /// # Ok(()) + /// # } pub fn as_bool(&self) -> Result<&bool> { match self { Value::Bool(b) => Ok(b), @@ -348,6 +722,14 @@ impl Value { } } + /// Cast value to [`&mut bool`] if [`Value::Bool`]. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let mut v = Value::from(true); + /// *v.as_bool_mut()? = false; + /// # Ok(()) + /// # } pub fn as_bool_mut(&mut self) -> Result<&mut bool> { match self { Value::Bool(b) => Ok(b), @@ -355,6 +737,148 @@ impl Value { } } + /// Cast value to [`& u128`] if [`Value::Number`]. + /// + /// Error is raised if the value is not a number or if the numeric value + /// does not fit in a u128. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from(10); + /// assert_eq!(v.as_u128()?, 10u128); + /// + /// let v = Value::from(-10); + /// assert!(v.as_u128().is_err()); + /// # Ok(()) + /// # } + pub fn as_u128(&self) -> Result { + match self { + Value::Number(b) => { + if let Some(n) = b.as_u128() { + return Ok(n); + } + bail!("not a u128"); + } + _ => Err(anyhow!("not a u128")), + } + } + + /// Cast value to [`& i128`] if [`Value::Number`]. + /// + /// Error is raised if the value is not a number or if the numeric value + /// does not fit in a i128. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from(-10); + /// assert_eq!(v.as_i128()?, -10i128); + /// + /// let v = Value::from_numeric_string("11111111111111111111111111111111111111111111111111")?; + /// assert!(v.as_i128().is_err()); + /// # Ok(()) + /// # } + pub fn as_i128(&self) -> Result { + match self { + Value::Number(b) => { + if let Some(n) = b.as_i128() { + return Ok(n); + } + bail!("not a i128"); + } + _ => Err(anyhow!("not a i128")), + } + } + + /// Cast value to [`& u64`] if [`Value::Number`]. + /// + /// Error is raised if the value is not a number or if the numeric value + /// does not fit in a u64. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from(10); + /// assert_eq!(v.as_u64()?, 10u64); + /// + /// let v = Value::from(-10); + /// assert!(v.as_u64().is_err()); + /// # Ok(()) + /// # } + pub fn as_u64(&self) -> Result { + match self { + Value::Number(b) => { + if let Some(n) = b.as_u64() { + return Ok(n); + } + bail!("not a u64"); + } + _ => Err(anyhow!("not a u64")), + } + } + + /// Cast value to [`& i64`] if [`Value::Number`]. + /// + /// Error is raised if the value is not a number or if the numeric value + /// does not fit in a i64. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from(-10); + /// assert_eq!(v.as_i64()?, -10i64); + /// + /// let v = Value::from(340_282_366_920_938_463_463_374_607_431_768_211_455u128); + /// assert!(v.as_i64().is_err()); + /// # Ok(()) + /// # } + pub fn as_i64(&self) -> Result { + match self { + Value::Number(b) => { + if let Some(n) = b.as_i64() { + return Ok(n); + } + bail!("not an i64"); + } + _ => Err(anyhow!("not an i64")), + } + } + + /// Cast value to [`& f64`] if [`Value::Number`]. + /// Error is raised if the value is not a number or if the numeric value + /// does not fit in a i64. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from(-10); + /// assert_eq!(v.as_f64()?, -10f64); + /// + /// let v = Value::from(340_282_366_920_938_463_463_374_607_431_768_211_455u128); + /// assert!(v.as_i64().is_err()); + /// # Ok(()) + /// # } + pub fn as_f64(&self) -> Result { + match self { + Value::Number(b) => { + if let Some(n) = b.as_f64() { + return Ok(n); + } + bail!("not a f64"); + } + _ => Err(anyhow!("not a f64")), + } + } + + /// Cast value to [`& Rc`] if [`Value::String`]. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from("Hello"); + /// assert_eq!(v.as_string()?.as_ref(), "Hello"); + /// # Ok(()) + /// # } pub fn as_string(&self) -> Result<&Rc> { match self { Value::String(s) => Ok(s), @@ -362,6 +886,14 @@ impl Value { } } + /// Cast value to [`&mut Rc`] if [`Value::String`]. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let mut v = Value::from("Hello"); + /// *v.as_string_mut()? = "World".into(); + /// # Ok(()) + /// # } pub fn as_string_mut(&mut self) -> Result<&mut Rc> { match self { Value::String(s) => Ok(s), @@ -369,6 +901,7 @@ impl Value { } } + #[doc(hidden)] pub fn as_number(&self) -> Result<&Number> { match self { Value::Number(n) => Ok(n), @@ -376,6 +909,7 @@ impl Value { } } + #[doc(hidden)] pub fn as_number_mut(&mut self) -> Result<&mut Number> { match self { Value::Number(n) => Ok(n), @@ -383,6 +917,14 @@ impl Value { } } + /// Cast value to [`& Vec`] if [`Value::Array`]. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from([Value::from("Hello")].to_vec()); + /// assert_eq!(v.as_array()?[0], Value::from("Hello")); + /// # Ok(()) + /// # } pub fn as_array(&self) -> Result<&Vec> { match self { Value::Array(a) => Ok(a), @@ -390,6 +932,14 @@ impl Value { } } + /// Cast value to [`&mut Vec`] if [`Value::Array`]. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let mut v = Value::from([Value::from("Hello")].to_vec()); + /// v.as_array_mut()?.push(Value::from("World")); + /// # Ok(()) + /// # } pub fn as_array_mut(&mut self) -> Result<&mut Vec> { match self { Value::Array(a) => Ok(Rc::make_mut(a)), @@ -397,6 +947,20 @@ impl Value { } } + /// Cast value to [`& BTreeSet`] if [`Value::Set`]. + /// ``` + /// # use regorus::*; + /// # use std::collections::BTreeSet; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from( + /// [Value::from("Hello")] + /// .iter() + /// .cloned() + /// .collect::>(), + /// ); + /// assert_eq!(v.as_set()?.first(), Some(&Value::from("Hello"))); + /// # Ok(()) + /// # } pub fn as_set(&self) -> Result<&BTreeSet> { match self { Value::Set(s) => Ok(s), @@ -404,6 +968,20 @@ impl Value { } } + /// Cast value to [`&mut BTreeSet`] if [`Value::Set`]. + /// ``` + /// # use regorus::*; + /// # use std::collections::BTreeSet; + /// # fn main() -> anyhow::Result<()> { + /// let mut v = Value::from( + /// [Value::from("Hello")] + /// .iter() + /// .cloned() + /// .collect::>(), + /// ); + /// v.as_set_mut()?.insert(Value::from("World")); + /// # Ok(()) + /// # } pub fn as_set_mut(&mut self) -> Result<&mut BTreeSet> { match self { Value::Set(s) => Ok(Rc::make_mut(s)), @@ -411,6 +989,23 @@ impl Value { } } + /// Cast value to [`& BTreeMap`] if [`Value::Object`]. + /// ``` + /// # use regorus::*; + /// # use std::collections::BTreeMap; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from( + /// [(Value::from("Hello"), Value::from("World"))] + /// .iter() + /// .cloned() + /// .collect::>(), + /// ); + /// assert_eq!( + /// v.as_object()?.iter().next(), + /// Some((&Value::from("Hello"), &Value::from("World"))), + /// ); + /// # Ok(()) + /// # } pub fn as_object(&self) -> Result<&BTreeMap> { match self { Value::Object(m) => Ok(m), @@ -418,6 +1013,20 @@ impl Value { } } + /// Cast value to [`&mut BTreeMap`] if [`Value::Object`]. + /// ``` + /// # use regorus::*; + /// # use std::collections::BTreeMap; + /// # fn main() -> anyhow::Result<()> { + /// let mut v = Value::from( + /// [(Value::from("Hello"), Value::from("World"))] + /// .iter() + /// .cloned() + /// .collect::>(), + /// ); + /// v.as_object_mut()?.insert(Value::from("Good"), Value::from("Bye")); + /// # Ok(()) + /// # } pub fn as_object_mut(&mut self) -> Result<&mut BTreeMap> { match self { Value::Object(m) => Ok(Rc::make_mut(m)), @@ -427,7 +1036,7 @@ impl Value { } impl Value { - pub fn make_or_get_value_mut<'a>(&'a mut self, paths: &[&str]) -> Result<&'a mut Value> { + pub(crate) fn make_or_get_value_mut<'a>(&'a mut self, paths: &[&str]) -> Result<&'a mut Value> { if paths.is_empty() { return Ok(self); } @@ -457,7 +1066,7 @@ impl Value { } } - pub fn merge(&mut self, mut new: Value) -> Result<()> { + pub(crate) fn merge(&mut self, mut new: Value) -> Result<()> { if self == &new { return Ok(()); } @@ -486,38 +1095,59 @@ impl Value { Ok(()) } } -impl ops::Index for Value { - type Output = Value; - - fn index(&self, index: usize) -> &Self::Output { - match self.as_array() { - Ok(a) if index < a.len() => &a[index], - _ => &Value::Undefined, - } - } -} - -impl ops::Index<&str> for Value { - type Output = Value; - - fn index(&self, key: &str) -> &Self::Output { - &self[&Value::String(key.into())] - } -} - -impl ops::Index<&String> for Value { - type Output = Value; - - fn index(&self, key: &String) -> &Self::Output { - &self[&Value::String(key.clone().into())] - } -} impl ops::Index<&Value> for Value { type Output = Value; + /// Index a [`Value`] using a [`Value`]. + /// + /// [`Value::Undefined`] is returned + /// - If the index not valid for the collection. + /// - If the value being indexed is not an array, set or object. + /// + /// Sets can be indexed only by elements within the set. + /// + /// ``` + /// # use regorus::*; + /// # use std::collections::BTreeMap; + /// # fn main() -> anyhow::Result<()> { + /// + /// let arr = Value::from([Value::from("Hello")].to_vec()); + /// // Index an array. + /// assert_eq!(arr[&Value::from(0)].as_string()?.as_ref(), "Hello"); + /// assert_eq!(arr[&Value::from(10)], Value::Undefined); + /// + /// let mut set = Value::new_set(); + /// set.as_set_mut()?.insert(Value::from(100)); + /// set.as_set_mut()?.insert(Value::from("Hello")); + /// + /// // Index a set. + /// let item = Value::from("Hello"); + /// assert_eq!(&set[&item], &item); + /// assert_eq!(&set[&Value::from(10)], &Value::Undefined); + /// + /// let mut obj = Value::new_object(); + /// obj.as_object_mut()?.insert(Value::from("Hello"), Value::from("World")); + /// obj.as_object_mut()?.insert(Value::new_array(), Value::from("bye")); + /// + /// // Index an object. + /// assert_eq!(&obj[Value::from("Hello")].as_string()?.as_ref(), &"World"); + /// assert_eq!(&obj[Value::from("hllo")], &Value::Undefined); + /// // Index using non-string key. + /// assert_eq!(&obj[&Value::new_array()].as_string()?.as_ref(), &"bye"); + /// + /// // Index a non-collection. + /// assert_eq!(&Value::Null[&Value::from(1)], &Value::Undefined); + /// # Ok(()) + /// # } + /// ``` + /// + /// This is the preferred way of indexing a value. + /// Since constructing a value may be a costly operation (e.g. Value::String), + /// the caller can construct the index value once and use it many times. + ///` fn index(&self, key: &Value) -> &Self::Output { - match (self, &key) { + match (self, key) { (Value::Object(o), _) => match &o.get(key) { Some(v) => v, _ => &Value::Undefined, @@ -534,3 +1164,35 @@ impl ops::Index<&Value> for Value { } } } + +impl ops::Index for Value +where + Value: From, +{ + type Output = Value; + + /// Index a [`Value`]. + /// + /// + /// A [`Value`] is constructed from the index which is then used for indexing. + /// + /// ``` + /// # use regorus::*; + /// # use std::collections::BTreeMap; + /// # fn main() -> anyhow::Result<()> { + /// let v = Value::from( + /// [(Value::from("Hello"), Value::from("World")), + /// (Value::from(1), Value::from(2))] + /// .iter() + /// .cloned() + /// .collect::>(), + /// ); + /// + /// assert_eq!(&v["Hello"].as_string()?.as_ref(), &"World"); + /// assert_eq!(&v[1].as_u64()?, &2u64); + /// # Ok(()) + /// # } + fn index(&self, key: T) -> &Self::Output { + &self[&Value::from(key)] + } +} diff --git a/tests/aci/main.rs b/tests/aci/main.rs index dac562a4..da84fd9c 100644 --- a/tests/aci/main.rs +++ b/tests/aci/main.rs @@ -45,7 +45,7 @@ fn eval_test_case(dir: &Path, case: &TestCase) -> Result { let mut values = vec![]; for qr in query_results.result { - values.push(if !qr.bindings.is_empty_object() { + values.push(if !qr.bindings.as_object()?.is_empty() { qr.bindings.clone() } else if let Some(v) = qr.expressions.last() { v.value.clone() @@ -53,7 +53,7 @@ fn eval_test_case(dir: &Path, case: &TestCase) -> Result { Value::Undefined }); } - let result = Value::from_array(values); + let result = Value::from(values); // Make result json compatible. (E.g: avoid sets). Value::from_json_str(&result.to_string()) } diff --git a/tests/opa.rs b/tests/opa.rs index 90757c76..bef1d7e7 100644 --- a/tests/opa.rs +++ b/tests/opa.rs @@ -84,7 +84,7 @@ fn eval_test_case(case: &TestCase) -> Result { let mut values = vec![]; for qr in query_results.result { - values.push(if !qr.bindings.is_empty_object() { + values.push(if !qr.bindings.as_object()?.is_empty() { if case.sort_bindings == Some(true) { let mut v = qr.bindings.clone(); let bindings = v.as_object_mut()?; @@ -105,15 +105,15 @@ fn eval_test_case(case: &TestCase) -> Result { }); } - let result = Value::from_array(values); + let result = Value::from(values); // Make result json compatible. (E.g: avoid sets). Value::from_json_str(&result.to_string()) } fn json_schema_tests_check(actual: &Value, expected: &Value) -> bool { // Fetch `x` binding. - let actual = &actual[0][&Value::String("x".into())]; - let expected = &expected[0][&Value::String("x".into())]; + let actual = &actual[0]["x"]; + let expected = &expected[0]["x"]; match (actual, expected) { (Value::Array(actual), Value::Array(expected)) diff --git a/tests/value/mod.rs b/tests/value/mod.rs index 7e449903..26f02285 100644 --- a/tests/value/mod.rs +++ b/tests/value/mod.rs @@ -15,7 +15,7 @@ fn non_string_key() -> Result<()> { obj.as_object_mut()? .insert(Value::from(std::f64::consts::PI), Value::Null); obj.as_object_mut()?.insert( - Value::from_array(vec![ + Value::from(vec![ Value::Bool(true), Value::Null, Value::from(std::f64::consts::PI), @@ -115,14 +115,14 @@ fn value_as_index() -> Result<()> { fn string_as_index() -> Result<()> { let obj = Value::from_json_str(r#"{ "a" : 5, "b" : 6 }"#)?; assert_eq!(&obj["a"], &Value::from(5.0)); - assert_eq!(&obj[&"b".to_owned()], &Value::from(6.0)); + assert_eq!(&obj["b".to_owned()], &Value::from(6.0)); Ok(()) } #[test] fn usize_as_index() -> Result<()> { - assert_eq!(&Value::from_json_str("[1, 2, 3]")?[0], &Value::from(1.0)); - assert_eq!(&Value::from_json_str("[1, 2, 3]")?[5], &Value::Undefined); + assert_eq!(&Value::from_json_str("[1, 2, 3]")?[0u64], &Value::from(1.0)); + assert_eq!(&Value::from_json_str("[1, 2, 3]")?[5u64], &Value::Undefined); Ok(()) } @@ -135,9 +135,6 @@ fn api() -> Result<()> { assert_eq!(v["a"], Value::from(3.145)); assert_eq!(v.as_object()?.len(), 1); - // Null - assert!(Value::Null.is_null()); - let v = Value::new_set(); assert_eq!(v.as_set()?.len(), 0);