mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Compare commits
13 Commits
regorus-v0
...
regorus-v0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fa2847e6f | ||
|
|
82c86437cb | ||
|
|
05e91da06e | ||
|
|
8c69dd491b | ||
|
|
6a167143cb | ||
|
|
d2049d07f3 | ||
|
|
e326f3c629 | ||
|
|
3c7674e7c2 | ||
|
|
947c9490fa | ||
|
|
0ebcb568cc | ||
|
|
e86801bdd9 | ||
|
|
3d98c3b12e | ||
|
|
330a6dff72 |
4
.github/workflows/pr.yml
vendored
4
.github/workflows/pr.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
run: cargo test -r --test aci
|
||||
- name: Run tests (OPA Conformance)
|
||||
run: >-
|
||||
cargo test -r --test opa --features opa-testutil -- $(tr '\n' ' ' < tests/opa.passing)
|
||||
cargo test -r --test opa --features opa-testutil,serde_json/arbitrary_precision -- $(tr '\n' ' ' < tests/opa.passing)
|
||||
- name: Build (MUSL)
|
||||
run: cargo build --verbose --all-targets --target x86_64-unknown-linux-musl
|
||||
- name: Run tests (MUSL)
|
||||
@@ -41,4 +41,4 @@ jobs:
|
||||
run: cargo test -r --test aci --target x86_64-unknown-linux-musl
|
||||
- name: Run tests (MUSL OPA Conformance)
|
||||
run: >-
|
||||
cargo test -r --test opa --features opa-testutil --target x86_64-unknown-linux-musl -- $(tr '\n' ' ' < tests/opa.passing)
|
||||
cargo test -r --test opa --features opa-testutil,serde_json/arbitrary_precision --target x86_64-unknown-linux-musl -- $(tr '\n' ' ' < tests/opa.passing)
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.3](https://github.com/microsoft/regorus/compare/regorus-v0.1.2...regorus-v0.1.3) - 2024-04-11
|
||||
|
||||
### Other
|
||||
- Add a note in example to prefer eval_rule over eval_query ([#204](https://github.com/microsoft/regorus/pull/204))
|
||||
- Do not enable serde_json/arbitrary_precision by default ([#203](https://github.com/microsoft/regorus/pull/203))
|
||||
- Rewrite so that code compiles with chrono_tz 0.8.5 and 0.9.0 ([#201](https://github.com/microsoft/regorus/pull/201))
|
||||
- update ruby bindings ([#200](https://github.com/microsoft/regorus/pull/200))
|
||||
- Store Value instances in AST for strings, numbers and idents ([#197](https://github.com/microsoft/regorus/pull/197))
|
||||
- :Value> and From<serde_yaml::Value> ([#196](https://github.com/microsoft/regorus/pull/196))
|
||||
- Build dependency on git only if opa.runtime feature is enabled. ([#194](https://github.com/microsoft/regorus/pull/194))
|
||||
- Update to opa v0.63.0 ([#192](https://github.com/microsoft/regorus/pull/192))
|
||||
- Update pyo3 requirement from 0.20.2 to 0.21.0 ([#190](https://github.com/microsoft/regorus/pull/190))
|
||||
- Ruby bindings for existing FFI methods, plus eval_rule() ([#188](https://github.com/microsoft/regorus/pull/188))
|
||||
- Evaluate rules directly instead of queries ([#186](https://github.com/microsoft/regorus/pull/186))
|
||||
- Remove cruft. ([#184](https://github.com/microsoft/regorus/pull/184))
|
||||
|
||||
## [0.1.2](https://github.com/microsoft/regorus/compare/regorus-v0.1.1...regorus-v0.1.2) - 2024-03-22
|
||||
|
||||
### Other
|
||||
|
||||
@@ -5,12 +5,13 @@ members = [
|
||||
"bindings/python",
|
||||
"bindings/wasm",
|
||||
"bindings/java",
|
||||
"bindings/ruby/ext/regorusrb",
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "regorus"
|
||||
description = "A fast, lightweight Rego (OPA policy language) interpreter"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
license-file = "LICENSE"
|
||||
repository = "https://github.com/microsoft/regorus"
|
||||
@@ -70,10 +71,8 @@ opa-testutil = []
|
||||
[dependencies]
|
||||
anyhow = {version = "1.0.66", features = ["backtrace"] }
|
||||
serde = {version = "1.0.150", features = ["derive", "rc"] }
|
||||
serde_json = {version = "1.0.89", features = ["arbitrary_precision"] }
|
||||
serde_json = "1.0.89"
|
||||
serde_yaml = {version = "0.9.16", optional = true }
|
||||
log = "0.4.17"
|
||||
env_logger="0.11.1"
|
||||
lazy_static = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
num = "0.4.1"
|
||||
|
||||
17
README.md
17
README.md
@@ -9,11 +9,11 @@
|
||||
Regorus is also
|
||||
- *cross-platform* - Written in platform-agnostic Rust.
|
||||
- *current* - We strive to keep Regorus up to date with latest OPA release. Regorus supports `import rego.v1`.
|
||||
- *compliant* - Regorus is mostly compliant with the latest [OPA release v0.62.0](https://github.com/open-policy-agent/opa/releases/tag/v0.62.0). See [OPA Conformance](#opa-conformance) for details. Note that while we behaviorally produce the same results, we don't yet support all the builtins.
|
||||
- *compliant* - Regorus is mostly compliant with the latest [OPA release v0.63.0](https://github.com/open-policy-agent/opa/releases/tag/v0.63.0). See [OPA Conformance](#opa-conformance) for details. Note that while we behaviorally produce the same results, we don't yet support all the builtins.
|
||||
- *extensible* - Extend the Rego language by implementing custom stateful builtins in Rust.
|
||||
See [add_extension](https://github.com/microsoft/regorus/blob/fc68bf9c8bea36427dae9401a7d1f6ada771f7ab/src/engine.rs#L352).
|
||||
Support for extensibility using other languages coming soon.
|
||||
- *polyglot* - In addition to Rust, Regorus can be used from *C*, *C++*, *C#*, *Golang*, *Java*, *Javascript* and *Python*.
|
||||
- *polyglot* - In addition to Rust, Regorus can be used from *C*, *C++*, *C#*, *Golang*, *Java*, *Javascript*, *Python*, and *Ruby*.
|
||||
This is made possible by the excellent FFI tools available in the Rust ecosystem. See [bindings](#bindings) for information on how to use Regorus from different languages.
|
||||
|
||||
To try out a *Javascript(WASM)* compiled version of Regorus from your browser, visit [Regorus Playground](https://anakrish.github.io/regorus-playground/).
|
||||
@@ -69,7 +69,7 @@ $ cargo build -r --example regorus --features "yaml" --no-default-features; stri
|
||||
-rwxr-xr-x 1 anand staff 2.9M Jan 19 11:26 target/release/examples/regorus*
|
||||
```
|
||||
|
||||
Regorus passes the [OPA v0.61.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few
|
||||
Regorus passes the [OPA v0.63.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few
|
||||
builtins. See [OPA Conformance](#opa-conformance) below.
|
||||
|
||||
## Bindings
|
||||
@@ -90,6 +90,8 @@ Regorus can be used from a variety of languages:
|
||||
- *Javascript*: Regorus is compiled to WASM using [wasmpack](https://github.com/rustwasm/wasm-pack).
|
||||
See [bindings/wasm](https://github.com/microsoft/regorus/tree/main/bindings/wasm) for an example of using Regorus from nodejs.
|
||||
To try out a *Javascript(WASM)* compiled version of Regorus from your browser, visit [Regorus Playground](https://anakrish.github.io/regorus-playground/).
|
||||
- *Ruby*: Ruby bindings are developed using [magnus](https://github.com/matsadler/magnus).
|
||||
See [bindings/ruby](https://github.com/microsoft/regorus/tree/main/bindings/ruby).
|
||||
|
||||
To avoid operational overhead, we currently don't publish these bindings to various repositories.
|
||||
It is straight-forward to build these bindings yourself.
|
||||
@@ -179,7 +181,7 @@ It produces the following coverage report which shows that all lines are execute
|
||||

|
||||
|
||||
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
|
||||
Policy coverage information is useful for debugging your policy as well as to write tests for your policy so that all
|
||||
lines of the policy are exercised by the tests.
|
||||
|
||||
## ACI Policies
|
||||
@@ -243,14 +245,13 @@ Benchmark 1: opa eval -b tests/aci -d tests/aci/data.json -i tests/aci/input.jso
|
||||
```
|
||||
## OPA Conformance
|
||||
|
||||
Regorus has been verified to be compliant with [OPA v0.61.0](https://github.com/open-policy-agent/opa/releases/tag/v0.61.0)
|
||||
using a [test driver](https://github.com/microsoft/regorus/blob/main/tests/opa.rs) that loads and runs the OPA testsuite using Regorus, and verifies that expected outputs
|
||||
are produced.
|
||||
Regorus has been verified to be compliant with [OPA v0.63.0](https://github.com/open-policy-agent/opa/releases/tag/v0.63.0)
|
||||
using a [test driver](https://github.com/microsoft/regorus/blob/main/tests/opa.rs) that loads and runs the OPA testsuite using Regorus, and verifies that expected outputs are produced.
|
||||
|
||||
The test driver can be invoked by running:
|
||||
|
||||
```bash
|
||||
$ cargo test -r --test opa
|
||||
$ cargo test -r --test opa --features opa-testutil,serde_json/arbitrary_precision
|
||||
```
|
||||
|
||||
Currently, Regorus passes all the non-builtin specific tests.
|
||||
|
||||
@@ -14,7 +14,7 @@ crate-type = ["cdylib"]
|
||||
[dependencies]
|
||||
anyhow = "1.0.79"
|
||||
ordered-float = "4.2.0"
|
||||
pyo3 = {version = "0.20.2", features = ["anyhow", "extension-module"] }
|
||||
pyo3 = {version = "0.21.0", features = ["anyhow", "extension-module"] }
|
||||
regorus = { path = "../.." }
|
||||
serde_json = "1.0.112"
|
||||
|
||||
|
||||
14
bindings/ruby/.gitignore
vendored
Normal file
14
bindings/ruby/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/.bundle/
|
||||
/.yardoc
|
||||
/_yardoc/
|
||||
/coverage/
|
||||
/doc/
|
||||
/pkg/
|
||||
/spec/reports/
|
||||
/tmp/
|
||||
*.bundle
|
||||
*.so
|
||||
*.o
|
||||
*.a
|
||||
mkmf.log
|
||||
target/
|
||||
29
bindings/ruby/.rubocop.yml
Normal file
29
bindings/ruby/.rubocop.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
require:
|
||||
- rubocop-minitest
|
||||
- rubocop-rake
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 3.0
|
||||
NewCops: enable
|
||||
|
||||
Layout/LineLength:
|
||||
Max: 180
|
||||
|
||||
Lint/EmptyClass:
|
||||
Enabled: false
|
||||
|
||||
Metrics/ClassLength:
|
||||
Exclude:
|
||||
- 'test/**/*.rb'
|
||||
|
||||
Metrics/MethodLength:
|
||||
Enabled: false
|
||||
|
||||
Style/StringLiterals:
|
||||
EnforcedStyle: double_quotes
|
||||
|
||||
Style/StringLiteralsInInterpolation:
|
||||
EnforcedStyle: double_quotes
|
||||
|
||||
Style/WordArray:
|
||||
Enabled: false
|
||||
1
bindings/ruby/.tool-versions
Normal file
1
bindings/ruby/.tool-versions
Normal file
@@ -0,0 +1 @@
|
||||
ruby 3.3.0
|
||||
5
bindings/ruby/CHANGELOG.md
Normal file
5
bindings/ruby/CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2024-03-29
|
||||
|
||||
- Initial release
|
||||
3
bindings/ruby/Cargo.toml
Normal file
3
bindings/ruby/Cargo.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[workspace]
|
||||
members = ["ext/regorusrb"]
|
||||
resolver = "2"
|
||||
16
bindings/ruby/Gemfile
Normal file
16
bindings/ruby/Gemfile
Normal file
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
# Specify your gem's dependencies in regorusrb.gemspec
|
||||
gemspec
|
||||
|
||||
# These gems are required for local development and testing,
|
||||
# but won't be included in the published gem
|
||||
gem "minitest", "~> 5.16"
|
||||
gem "rake", "~> 13.0"
|
||||
gem "rake-compiler"
|
||||
gem "rake-compiler-dock"
|
||||
gem "rubocop", "~> 1.62", require: false
|
||||
gem "rubocop-minitest", require: false
|
||||
gem "rubocop-rake", require: false
|
||||
63
bindings/ruby/Gemfile.lock
Normal file
63
bindings/ruby/Gemfile.lock
Normal file
@@ -0,0 +1,63 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
regorusrb (0.1.0)
|
||||
rb_sys (~> 0.9.91)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ast (2.4.2)
|
||||
json (2.7.2)
|
||||
language_server-protocol (3.17.0.3)
|
||||
minitest (5.22.3)
|
||||
parallel (1.24.0)
|
||||
parser (3.3.0.5)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
racc (1.7.3)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rake-compiler (1.2.7)
|
||||
rake
|
||||
rake-compiler-dock (1.4.0)
|
||||
rb_sys (0.9.91)
|
||||
regexp_parser (2.9.0)
|
||||
rexml (3.2.6)
|
||||
rubocop (1.63.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.31.2)
|
||||
parser (>= 3.3.0.4)
|
||||
rubocop-minitest (0.35.0)
|
||||
rubocop (>= 1.61, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rake (0.6.0)
|
||||
rubocop (~> 1.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
unicode-display_width (2.5.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
minitest (~> 5.16)
|
||||
rake (~> 13.0)
|
||||
rake-compiler
|
||||
rake-compiler-dock
|
||||
regorusrb!
|
||||
rubocop (~> 1.62)
|
||||
rubocop-minitest
|
||||
rubocop-rake
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.7
|
||||
21
bindings/ruby/LICENSE.txt
Normal file
21
bindings/ruby/LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
99
bindings/ruby/README.md
Normal file
99
bindings/ruby/README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Regorusrb
|
||||
|
||||
**Regorus** is
|
||||
|
||||
- *Rego*-*Rus(t)* - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/)
|
||||
interpreter written in Rust.
|
||||
- *Rigorous* - A rigorous enforcer of well-defined Rego semantics.
|
||||
|
||||
## Installation
|
||||
|
||||
Regorus can be used in Ruby by configuring bundler to build from the remote git source.
|
||||
|
||||
If using [Bundler](https://bundler.io/) to manage gems (recommended), edit your gemfile to include the following
|
||||
`
|
||||
gem "regorusrb", git: "https://github.com/microsoft/regorus/", glob: "bindings/ruby/*.gemspec"
|
||||
`
|
||||
|
||||
or manually install checkout the source and build the gem
|
||||
|
||||
`
|
||||
git clone https://github.com/microsoft/regorus/
|
||||
cd regorus/bindings/ruby
|
||||
rake && rake build # should eventually output 'regorusrb 0.1.0 built to pkg/regorusrb-0.1.0.gem.'
|
||||
gem install --local ./pkg/regorusrb-0.1.0.gem
|
||||
`
|
||||
|
||||
It is not yet available in rubygems.
|
||||
|
||||
See [Repository](https://github.com/microsoft/regorus).
|
||||
|
||||
To build this gem locally without bundler,
|
||||
|
||||
`rake build`
|
||||
|
||||
then to install the gem and build the native extensions
|
||||
|
||||
`gem install --local ./pkg/regorusrb-0.1.0.gem`
|
||||
|
||||
## Usage
|
||||
|
||||
```ruby
|
||||
require "regorus"
|
||||
|
||||
engine = Regorus::Engine.new
|
||||
|
||||
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')
|
||||
|
||||
|
||||
# can be strings or symbols
|
||||
data = {
|
||||
metadata: {
|
||||
devices: {
|
||||
"/run/layers/p0-layer0": "1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766",
|
||||
"/run/layers/p0-layer1": "e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c",
|
||||
"/run/layers/p0-layer2": "eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79",
|
||||
"/run/layers/p0-layer3": "41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156",
|
||||
"/run/layers/p0-layer4": "4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c",
|
||||
"/run/layers/p0-layer5": "fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
engine.add_data(data)
|
||||
input = {
|
||||
"containerID": "container0",
|
||||
"layerPaths": [
|
||||
"/run/layers/p0-layer0",
|
||||
"/run/layers/p0-layer1",
|
||||
"/run/layers/p0-layer2",
|
||||
"/run/layers/p0-layer3",
|
||||
"/run/layers/p0-layer4",
|
||||
"/run/layers/p0-layer5"
|
||||
],
|
||||
"target": "/run/gcs/c/container0/rootfs"
|
||||
}
|
||||
|
||||
engine.set_input(input)
|
||||
|
||||
# Evaluate a specife rule
|
||||
rule_results = engine.eval_rule('data.framework.mount_overlay')
|
||||
puts rule_results # { "allowed" => true, "metadata" => [...]}
|
||||
|
||||
# Or evalute a full policy document
|
||||
query_results = engine.eval_query('data.framework')
|
||||
puts query_results[:result][0]
|
||||
|
||||
# Query results can can also be returned as JSON strings instead of Ruby Hash structure
|
||||
results_json = engine.eval_query_as_json('data.framework.mount_overlay=x')
|
||||
puts results_json
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
||||
|
||||
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
||||
|
||||
29
bindings/ruby/Rakefile
Normal file
29
bindings/ruby/Rakefile
Normal file
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "bundler/gem_tasks"
|
||||
require "minitest/test_task"
|
||||
require "rake/extensiontask"
|
||||
require "rubocop/rake_task"
|
||||
require "rb_sys/extensiontask"
|
||||
|
||||
Minitest::TestTask.create
|
||||
|
||||
RuboCop::RakeTask.new
|
||||
|
||||
desc "build the .gem file, including native extensions, according to the .gemspec"
|
||||
task build: :compile
|
||||
|
||||
GEMSPEC = Gem::Specification.load("regorusrb.gemspec")
|
||||
|
||||
RbSys::ExtensionTask.new("regorusrb", GEMSPEC) do |ext|
|
||||
ext.lib_dir = "lib/regorus"
|
||||
ext.cross_compile = true
|
||||
ext.cross_platform = %w[x86-mingw32 x64-mingw-ucrt x64-mingw32 x86-linux x86_64-linux x86_64-darwin arm64-darwin]
|
||||
end
|
||||
|
||||
task default: %i[compile test rubocop]
|
||||
|
||||
desc "Build native extension for a given platform (i.e. rake 'native[x86_64-linux]')"
|
||||
task :native, [:platform] do |_t, platform:|
|
||||
sh "bundle", "exec", "rb-sys-dock", "--platform", platform, "--build"
|
||||
end
|
||||
11
bindings/ruby/bin/console
Executable file
11
bindings/ruby/bin/console
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "bundler/setup"
|
||||
require "regorus"
|
||||
|
||||
# You can add fixtures and/or initialization code here to make experimenting
|
||||
# with your gem easier. You can also use a different console, if you like.
|
||||
|
||||
require "irb"
|
||||
IRB.start(__FILE__)
|
||||
8
bindings/ruby/bin/setup
Executable file
8
bindings/ruby/bin/setup
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
set -vx
|
||||
|
||||
bundle install
|
||||
|
||||
# Do any other automated setup that you need to do here
|
||||
16
bindings/ruby/ext/regorusrb/Cargo.toml
Normal file
16
bindings/ruby/ext/regorusrb/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "regorusrb"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Ruby bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
magnus = { version = "0.6.3" }
|
||||
regorus = { git = "https://github.com/microsoft/regorus" }
|
||||
serde_json = "1.0.115"
|
||||
serde_magnus = "0.8.1"
|
||||
8
bindings/ruby/ext/regorusrb/extconf.rb
Normal file
8
bindings/ruby/ext/regorusrb/extconf.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "mkmf"
|
||||
require "rb_sys/mkmf"
|
||||
|
||||
create_rust_makefile("regorus/regorusrb") do |r|
|
||||
r.auto_install_rust_toolchain = true
|
||||
end
|
||||
241
bindings/ruby/ext/regorusrb/src/lib.rs
Normal file
241
bindings/ruby/ext/regorusrb/src/lib.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
use magnus::{exception::runtime_error, method, module, prelude::*, Error, Ruby};
|
||||
use regorus::Engine as RegorusEngine;
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
// `Value` exists under magnus, regorus, and serde_json, so be explicit
|
||||
|
||||
#[derive(Default)]
|
||||
#[magnus::wrap(class = "Regorus::Engine")]
|
||||
pub struct Engine {
|
||||
engine: RefCell<RegorusEngine>,
|
||||
}
|
||||
|
||||
impl Clone for Engine {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
engine: self.engine.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
fn initialize(&self) {
|
||||
let engine = RegorusEngine::new();
|
||||
*self.engine.borrow_mut() = engine;
|
||||
}
|
||||
|
||||
fn compare(&self, other: &Self) -> Result<i32, Error> {
|
||||
let self_ptr: *const _ = &*self.engine.borrow();
|
||||
let other_ptr: *const _ = &*other.engine.borrow();
|
||||
match self_ptr.partial_cmp(&other_ptr) {
|
||||
Some(Ordering::Less) => Ok(-1),
|
||||
Some(Ordering::Equal) => Ok(0),
|
||||
Some(Ordering::Greater) => Ok(1),
|
||||
None => Err(Error::new(runtime_error(), "Comparison failed")),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_policy(&self, path: String, rego: String) -> Result<(), Error> {
|
||||
self.engine
|
||||
.borrow_mut()
|
||||
.add_policy(path, rego)
|
||||
.map_err(|e| Error::new(runtime_error(), format!("Failed to add policy: {}", e)))
|
||||
}
|
||||
|
||||
fn add_policy_from_file(&self, path: String) -> Result<(), Error> {
|
||||
self.engine
|
||||
.borrow_mut()
|
||||
.add_policy_from_file(path)
|
||||
.map_err(|e| Error::new(runtime_error(), format!("Failed to add policy: {}", e)))
|
||||
}
|
||||
|
||||
fn add_data(&self, ruby_hash: magnus::RHash) -> Result<(), Error> {
|
||||
let data_value: regorus::Value = serde_magnus::deserialize(ruby_hash).map_err(|e| {
|
||||
Error::new(
|
||||
runtime_error(),
|
||||
format!("Failed to deserialize Ruby value: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
self.engine
|
||||
.borrow_mut()
|
||||
.add_data(data_value)
|
||||
.map_err(|e| Error::new(runtime_error(), format!("Failed to add data: {}", e)))
|
||||
}
|
||||
|
||||
fn add_data_json(&self, json_string: String) -> Result<(), Error> {
|
||||
self.engine
|
||||
.borrow_mut()
|
||||
.add_data_json(&json_string)
|
||||
.map_err(|e| Error::new(runtime_error(), format!("Failed to add data json: {}", e)))
|
||||
}
|
||||
|
||||
fn add_data_from_json_file(&self, path: String) -> Result<(), Error> {
|
||||
let json_data = regorus::Value::from_json_file(&path).map_err(|e| {
|
||||
Error::new(
|
||||
runtime_error(),
|
||||
format!("Failed to parse JSON data file: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
self.engine.borrow_mut().add_data(json_data).map_err(|e| {
|
||||
Error::new(
|
||||
runtime_error(),
|
||||
format!("Failed to add data from file: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn clear_data(&self) -> Result<(), Error> {
|
||||
self.engine.borrow_mut().clear_data();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_input(&self, ruby_hash: magnus::RHash) -> Result<(), Error> {
|
||||
let input_value: regorus::Value = serde_magnus::deserialize(ruby_hash).map_err(|e| {
|
||||
Error::new(
|
||||
runtime_error(),
|
||||
format!("Failed to deserialize Ruby value: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
self.engine.borrow_mut().set_input(input_value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_input_json(&self, json_string: String) -> Result<(), Error> {
|
||||
self.engine
|
||||
.borrow_mut()
|
||||
.set_input_json(&json_string)
|
||||
.map_err(|e| Error::new(runtime_error(), format!("Failed to set input JSON: {}", e)))
|
||||
}
|
||||
|
||||
fn add_input_from_json_file(&self, path: String) -> Result<(), Error> {
|
||||
let json_data = regorus::Value::from_json_file(&path).map_err(|e| {
|
||||
Error::new(
|
||||
runtime_error(),
|
||||
format!("Failed to parse JSON input file: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
self.engine.borrow_mut().set_input(json_data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn eval_query(&self, query: String) -> Result<magnus::Value, Error> {
|
||||
let results = self
|
||||
.engine
|
||||
.borrow_mut()
|
||||
.eval_query(query, false)
|
||||
.map_err(|e| Error::new(runtime_error(), format!("Failed to evaluate query: {}", e)))?;
|
||||
|
||||
serde_magnus::serialize(&results).map_err(|e| {
|
||||
Error::new(
|
||||
runtime_error(),
|
||||
format!("Failed to serailzie query results: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn eval_query_as_json(&self, query: String) -> Result<String, Error> {
|
||||
let results = self
|
||||
.engine
|
||||
.borrow_mut()
|
||||
.eval_query(query, false)
|
||||
.map_err(|e| {
|
||||
Error::new(
|
||||
runtime_error(),
|
||||
format!("Failed to evaluate query as json: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
serde_json::to_string(&results).map_err(|e| {
|
||||
Error::new(
|
||||
runtime_error(),
|
||||
format!("Failed to serialize query results: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn eval_rule(&self, query: String) -> Result<Option<magnus::Value>, Error> {
|
||||
let result =
|
||||
self.engine.borrow_mut().eval_rule(query).map_err(|e| {
|
||||
Error::new(runtime_error(), format!("Failed to evaluate rule: {}", e))
|
||||
})?;
|
||||
|
||||
match result {
|
||||
regorus::Value::Undefined => Ok(None), // Convert undefined to Ruby's nil
|
||||
_ => serde_magnus::serialize(&result) // Serialize other results normally
|
||||
.map(Some)
|
||||
.map_err(|e| {
|
||||
magnus::Error::new(
|
||||
runtime_error(),
|
||||
format!("Failed to serialize the rule evaluation result: {}", e),
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_bool_query(&self, query: String) -> Result<bool, Error> {
|
||||
self.engine
|
||||
.borrow_mut()
|
||||
.eval_bool_query(query, false)
|
||||
.map_err(|e| Error::new(runtime_error(), format!("Failed to evaluate query: {}", e)))
|
||||
}
|
||||
|
||||
fn eval_allow_query(&self, query: String) -> Result<bool, Error> {
|
||||
Ok(self.engine.borrow_mut().eval_allow_query(query, false))
|
||||
}
|
||||
|
||||
fn eval_deny_query(&self, query: String) -> Result<bool, Error> {
|
||||
Ok(self.engine.borrow_mut().eval_deny_query(query, false))
|
||||
}
|
||||
}
|
||||
|
||||
#[magnus::init]
|
||||
fn init(ruby: &Ruby) -> Result<(), Error> {
|
||||
let regorus_module = ruby.define_module("Regorus")?;
|
||||
let engine_class = regorus_module.define_class("Engine", ruby.class_object())?;
|
||||
|
||||
// ruby object methods
|
||||
engine_class.define_alloc_func::<Engine>();
|
||||
engine_class.define_method("initialize", method!(Engine::initialize, 0))?;
|
||||
engine_class.define_method("clone", method!(Engine::clone, 0))?;
|
||||
engine_class.define_method("<=>", method!(Engine::compare, 1))?;
|
||||
// defines <, <=, >, >=, and == based on <=>
|
||||
engine_class.include_module(module::comparable())?;
|
||||
|
||||
// policy operations
|
||||
engine_class.define_method("add_policy", method!(Engine::add_policy, 2))?;
|
||||
engine_class.define_method(
|
||||
"add_policy_from_file",
|
||||
method!(Engine::add_policy_from_file, 1),
|
||||
)?;
|
||||
|
||||
// data operations
|
||||
engine_class.define_method("add_data", method!(Engine::add_data, 1))?;
|
||||
engine_class.define_method("add_data_json", method!(Engine::add_data_json, 1))?;
|
||||
engine_class.define_method(
|
||||
"add_data_from_json_file",
|
||||
method!(Engine::add_data_from_json_file, 1),
|
||||
)?;
|
||||
engine_class.define_method("clear_data", method!(Engine::clear_data, 0))?;
|
||||
|
||||
// input operations
|
||||
engine_class.define_method("set_input", method!(Engine::set_input, 1))?;
|
||||
engine_class.define_method("set_input_json", method!(Engine::set_input_json, 1))?;
|
||||
engine_class.define_method(
|
||||
"add_input_from_json_file",
|
||||
method!(Engine::add_input_from_json_file, 1),
|
||||
)?;
|
||||
|
||||
// query operations
|
||||
engine_class.define_method("eval_query", method!(Engine::eval_query, 1))?;
|
||||
engine_class.define_method("eval_query_as_json", method!(Engine::eval_query_as_json, 1))?;
|
||||
engine_class.define_method("eval_rule", method!(Engine::eval_rule, 1))?;
|
||||
engine_class.define_method("eval_bool_query", method!(Engine::eval_bool_query, 1))?;
|
||||
engine_class.define_method("eval_allow_query", method!(Engine::eval_allow_query, 1))?;
|
||||
engine_class.define_method("eval_deny_query", method!(Engine::eval_deny_query, 1))?;
|
||||
Ok(())
|
||||
}
|
||||
8
bindings/ruby/lib/regorus.rb
Normal file
8
bindings/ruby/lib/regorus.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "regorus/version"
|
||||
require_relative "regorus/regorusrb"
|
||||
|
||||
module Regorus
|
||||
class Engine; end
|
||||
end
|
||||
5
bindings/ruby/lib/regorus/version.rb
Normal file
5
bindings/ruby/lib/regorus/version.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Regorus
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
30
bindings/ruby/regorusrb.gemspec
Normal file
30
bindings/ruby/regorusrb.gemspec
Normal file
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "lib/regorus/version"
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "regorusrb"
|
||||
spec.version = Regorus::VERSION
|
||||
spec.authors = ["David Marshall"]
|
||||
|
||||
spec.summary = "Ruby bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
|
||||
spec.homepage = "https://github.com/microsoft/regorus/blob/main/bindings/ruby"
|
||||
spec.license = "MIT"
|
||||
spec.required_ruby_version = ">= 3.0.0"
|
||||
spec.required_rubygems_version = ">= 3.3.11"
|
||||
|
||||
spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
||||
|
||||
spec.metadata["homepage_uri"] = spec.homepage
|
||||
spec.metadata["source_code_uri"] = spec.homepage
|
||||
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/bindings/ruby/CHANGELOG.md"
|
||||
spec.metadata["rubygems_mfa_required"] = "true"
|
||||
|
||||
spec.files = Dir["lib/*.rb", "lib/regorus/*.rb", "ext/**/*.{rs,rb,lock,toml}", "Cargo.{lock,toml}", "LICENSE.txt", "README.md"]
|
||||
|
||||
spec.bindir = "exe"
|
||||
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
||||
spec.require_paths = ["lib"]
|
||||
spec.extensions = ["ext/regorusrb/extconf.rb"]
|
||||
spec.add_dependency "rb_sys", "~> 0.9.91"
|
||||
end
|
||||
4
bindings/ruby/sig/regorusrb.rbs
Normal file
4
bindings/ruby/sig/regorusrb.rbs
Normal file
@@ -0,0 +1,4 @@
|
||||
module Regorus
|
||||
VERSION: String
|
||||
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
|
||||
end
|
||||
6
bindings/ruby/test/test_helper.rb
Normal file
6
bindings/ruby/test/test_helper.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
||||
require "regorus"
|
||||
|
||||
require "minitest/autorun"
|
||||
204
bindings/ruby/test/test_regorus.rb
Normal file
204
bindings/ruby/test/test_regorus.rb
Normal file
@@ -0,0 +1,204 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
require "json"
|
||||
|
||||
class TestRegorus < Minitest::Test
|
||||
ALICE = "Alice"
|
||||
BOB = "Bob"
|
||||
CARLOS = "Carlos"
|
||||
|
||||
def setup
|
||||
@engine = ::Regorus::Engine.new
|
||||
@engine.add_policy("regorus_test.rego", example_policy)
|
||||
@engine.add_data(example_data)
|
||||
end
|
||||
|
||||
def example_policy
|
||||
<<~REGO
|
||||
package regorus_test
|
||||
is_manager {
|
||||
input.name == data.managers[_]
|
||||
}
|
||||
|
||||
is_employee {
|
||||
input.name == data.employees[_]
|
||||
}
|
||||
|
||||
# Set a default value for to return false instead of nil
|
||||
default is_manager_bool = false
|
||||
default is_employee_bool = false
|
||||
|
||||
is_manager_bool {
|
||||
is_manager
|
||||
}
|
||||
|
||||
is_employee_bool {
|
||||
is_employee
|
||||
}
|
||||
REGO
|
||||
end
|
||||
|
||||
def example_data
|
||||
{
|
||||
"managers" => [ALICE],
|
||||
"employees" => [ALICE, BOB]
|
||||
}
|
||||
end
|
||||
|
||||
def input_for(name)
|
||||
{ "name" => name }
|
||||
end
|
||||
|
||||
def test_version_number_presence
|
||||
refute_nil ::Regorus::VERSION
|
||||
end
|
||||
|
||||
def test_engine_creation
|
||||
assert_instance_of ::Regorus::Engine, ::Regorus::Engine.new
|
||||
end
|
||||
|
||||
def test_policy_addition
|
||||
assert_silent { @engine.add_policy("example.rego", example_policy) }
|
||||
end
|
||||
|
||||
def test_object_creation_with_new
|
||||
refute_same ::Regorus::Engine.new, ::Regorus::Engine.new
|
||||
end
|
||||
|
||||
def test_data_addition
|
||||
assert_silent { @engine.add_data(example_data) }
|
||||
end
|
||||
|
||||
def test_data_addition_as_json
|
||||
assert_silent { @engine.add_data_json(example_data.to_json) }
|
||||
end
|
||||
|
||||
def test_query_evaluation_for_alice
|
||||
@engine.set_input(input_for(ALICE))
|
||||
|
||||
assert_equal alice_results, @engine.eval_query("data.regorus_test")
|
||||
end
|
||||
|
||||
def test_query_evaluation_for_bob
|
||||
@engine.set_input(input_for(BOB))
|
||||
|
||||
assert_equal bob_results, @engine.eval_query("data.regorus_test")
|
||||
end
|
||||
|
||||
def test_query_evaluation_as_json
|
||||
@engine.set_input(input_for(ALICE))
|
||||
|
||||
assert_equal alice_results.to_json, @engine.eval_query_as_json("data.regorus_test")
|
||||
end
|
||||
|
||||
def test_rule_evaluation_for_alice
|
||||
@engine.set_input(input_for(ALICE))
|
||||
|
||||
assert @engine.eval_rule("data.regorus_test.is_employee")
|
||||
assert @engine.eval_rule("data.regorus_test.is_employee_bool")
|
||||
assert @engine.eval_rule("data.regorus_test.is_manager")
|
||||
assert @engine.eval_rule("data.regorus_test.is_manager_bool")
|
||||
end
|
||||
|
||||
def test_rule_evaluation_for_bob
|
||||
@engine.set_input(input_for(BOB))
|
||||
|
||||
assert @engine.eval_rule("data.regorus_test.is_employee")
|
||||
assert @engine.eval_rule("data.regorus_test.is_employee_bool")
|
||||
assert_nil @engine.eval_rule("data.regorus_test.is_manager")
|
||||
refute @engine.eval_rule("data.regorus_test.is_manager_bool")
|
||||
end
|
||||
|
||||
def test_rule_evaluation_for_carlos
|
||||
@engine.set_input(input_for(CARLOS))
|
||||
|
||||
assert_nil @engine.eval_rule("data.regorus_test.is_employee")
|
||||
refute @engine.eval_rule("data.regorus_test.is_employee_bool")
|
||||
assert_nil @engine.eval_rule("data.regorus_test.is_manager")
|
||||
refute @engine.eval_rule("data.regorus_test.is_manager_bool")
|
||||
end
|
||||
|
||||
def test_eval_bool_query
|
||||
assert @engine.eval_bool_query("1 < 2")
|
||||
refute @engine.eval_bool_query("1 > 2")
|
||||
assert_raises(RuntimeError) { @engine.eval_bool_query("1 + 1") }
|
||||
assert_raises(RuntimeError) { @engine.eval_bool_query("true; true") }
|
||||
assert_raises(RuntimeError) { @engine.eval_bool_query("true; false; true") }
|
||||
end
|
||||
|
||||
def test_eval_allow_query
|
||||
assert @engine.eval_allow_query("1 < 2")
|
||||
refute @engine.eval_allow_query("1 > 2")
|
||||
refute @engine.eval_allow_query("1 + 1")
|
||||
refute @engine.eval_allow_query("true; true")
|
||||
refute @engine.eval_allow_query("true; false; true")
|
||||
end
|
||||
|
||||
def test_eval_deny_query
|
||||
assert @engine.eval_deny_query("1 < 2")
|
||||
refute @engine.eval_deny_query("1 > 2")
|
||||
assert @engine.eval_deny_query("1 + 1")
|
||||
assert @engine.eval_deny_query("true; true")
|
||||
assert @engine.eval_deny_query("true; false; true")
|
||||
end
|
||||
|
||||
def test_missing_rules_handling
|
||||
@engine.set_input(input_for(ALICE))
|
||||
assert_raises(RuntimeError) { @engine.eval_rule("data.regorus_test.not_a_rule") }
|
||||
end
|
||||
|
||||
def test_engine_cloning
|
||||
cloned_engine = @engine.clone
|
||||
|
||||
assert_instance_of ::Regorus::Engine, cloned_engine
|
||||
refute_same @engine, cloned_engine
|
||||
end
|
||||
|
||||
def alice_results
|
||||
{
|
||||
result: [
|
||||
{
|
||||
expressions: [
|
||||
{
|
||||
value: {
|
||||
"is_employee" => true,
|
||||
"is_employee_bool" => true,
|
||||
"is_manager" => true,
|
||||
"is_manager_bool" => true
|
||||
},
|
||||
text: "data.regorus_test",
|
||||
location: {
|
||||
row: 1,
|
||||
col: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def bob_results
|
||||
{
|
||||
result: [
|
||||
{
|
||||
expressions: [
|
||||
{
|
||||
value: {
|
||||
"is_employee" => true,
|
||||
"is_employee_bool" => true,
|
||||
"is_manager_bool" => false
|
||||
},
|
||||
text: "data.regorus_test",
|
||||
location: {
|
||||
row: 1,
|
||||
col: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
16
build.rs
16
build.rs
@@ -3,7 +3,6 @@
|
||||
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Copy hooks to appropriate location so that git will run them.
|
||||
@@ -14,12 +13,15 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
// Supply information as compile-time environment variables.
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.unwrap();
|
||||
let git_hash = String::from_utf8(output.stdout).unwrap();
|
||||
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
|
||||
#[cfg(feature = "opa-runtime")]
|
||||
{
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.expect("`git rev-parse HEAD` failed.");
|
||||
let git_hash = String::from_utf8(output.stdout).unwrap();
|
||||
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -70,7 +70,14 @@ fn rego_eval(
|
||||
engine.set_input(input);
|
||||
}
|
||||
|
||||
// Evaluate query.
|
||||
// Note: The `eval_query` function is used below since it produces output
|
||||
// in the same format as OPA. It also allows evaluating arbitrary statements
|
||||
// as queries.
|
||||
//
|
||||
// Most applications will want to use `eval_rule` instead.
|
||||
// It is faster since it does not have to parse the query string.
|
||||
// It also returns the value of the rule directly and thus is easier
|
||||
// to use.
|
||||
let results = engine.eval_query(query, enable_tracing)?;
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&results)?);
|
||||
@@ -184,10 +191,6 @@ struct Cli {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
use clap::Parser;
|
||||
env_logger::builder()
|
||||
.format_level(false)
|
||||
.format_timestamp(None)
|
||||
.init();
|
||||
|
||||
// Parse and dispatch command.
|
||||
let cli = Cli::parse();
|
||||
|
||||
@@ -17,5 +17,5 @@ if [ -f Cargo.toml ]; then
|
||||
cargo test -r --test aci
|
||||
|
||||
# Ensure that OPA conformance tests don't regress.
|
||||
cargo test -r --features opa-testutil --test opa -- $(tr '\n' ' ' < tests/opa.passing)
|
||||
cargo test -r --features opa-testutil,serde_json/arbitrary_precision --test opa -- $(tr '\n' ' ' < tests/opa.passing)
|
||||
fi
|
||||
|
||||
14
src/ast.rs
14
src/ast.rs
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use crate::lexer::*;
|
||||
use crate::value::Value;
|
||||
use crate::Rc;
|
||||
|
||||
use std::ops::Deref;
|
||||
@@ -98,13 +99,13 @@ pub type Ref<T> = NodeRef<T>;
|
||||
#[derive(Debug)]
|
||||
pub enum Expr {
|
||||
// Simple items that only have a span as content.
|
||||
String(Span),
|
||||
RawString(Span),
|
||||
Number(Span),
|
||||
String((Span, Value)),
|
||||
RawString((Span, Value)),
|
||||
Number((Span, Value)),
|
||||
True(Span),
|
||||
False(Span),
|
||||
Null(Span),
|
||||
Var(Span),
|
||||
Var((Span, Value)),
|
||||
|
||||
// array
|
||||
Array {
|
||||
@@ -158,7 +159,7 @@ pub enum Expr {
|
||||
RefDot {
|
||||
span: Span,
|
||||
refr: Ref<Expr>,
|
||||
field: Span,
|
||||
field: (Span, Value),
|
||||
},
|
||||
|
||||
RefBrack {
|
||||
@@ -207,7 +208,8 @@ impl Expr {
|
||||
pub fn span(&self) -> &Span {
|
||||
use Expr::*;
|
||||
match self {
|
||||
String(s) | RawString(s) | Number(s) | True(s) | False(s) | Null(s) | Var(s) => s,
|
||||
String(s) | RawString(s) | Number(s) | Var(s) => &s.0,
|
||||
True(s) | False(s) | Null(s) => s,
|
||||
Array { span, .. }
|
||||
| Set { span, .. }
|
||||
| Object { span, .. }
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::value::Value;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use chrono::{
|
||||
DateTime, Datelike, Days, FixedOffset, Local, Months, SecondsFormat, TimeZone, Timelike, Utc,
|
||||
@@ -248,7 +248,10 @@ fn parse_epoch(
|
||||
"UTC" | "" => Utc.timestamp_nanos(ns).fixed_offset(),
|
||||
"Local" => Local.timestamp_nanos(ns).fixed_offset(),
|
||||
_ => {
|
||||
let tz: Tz = tz.parse().map_err(|err: String| anyhow!(err))?;
|
||||
let tz: Tz = match tz.parse() {
|
||||
Ok(tz) => tz,
|
||||
Err(e) => bail!(e),
|
||||
};
|
||||
tz.timestamp_nanos(ns).fixed_offset()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -208,6 +208,52 @@ impl Engine {
|
||||
&self.modules
|
||||
}
|
||||
|
||||
/// Evaluate rule(s) at given path.
|
||||
///
|
||||
/// [`Engine::eval_rule`] is often faster than [`Engine::eval_query`] and should be preferred if
|
||||
/// OPA style [`QueryResults`] are not needed.
|
||||
///
|
||||
/// ```
|
||||
/// # use regorus::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// let mut engine = Engine::new();
|
||||
///
|
||||
/// // Add policy
|
||||
/// engine.add_policy(
|
||||
/// "policy.rego".to_string(),
|
||||
/// r#"
|
||||
/// package example
|
||||
/// import rego.v1
|
||||
///
|
||||
/// x = [1, 2]
|
||||
///
|
||||
/// y := 5 if input.a > 2
|
||||
/// "#.to_string())?;
|
||||
///
|
||||
/// // Evaluate rule.
|
||||
/// let v = engine.eval_rule("data.example.x".to_string())?;
|
||||
/// assert_eq!(v, Value::from(vec![Value::from(1), Value::from(2)]));
|
||||
///
|
||||
/// // y evaluates to undefined.
|
||||
/// let v = engine.eval_rule("data.example.y".to_string())?;
|
||||
/// assert_eq!(v, Value::Undefined);
|
||||
///
|
||||
/// // Evaluating a non-existent rule is an error.
|
||||
/// let r = engine.eval_rule("data.exaample.x".to_string());
|
||||
/// assert!(r.is_err());
|
||||
///
|
||||
/// // Path must be valid rule paths.
|
||||
/// assert!( engine.eval_rule("data".to_string()).is_err());
|
||||
/// assert!( engine.eval_rule("data.example".to_string()).is_err());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn eval_rule(&mut self, path: String) -> Result<Value> {
|
||||
self.prepare_for_eval(false)?;
|
||||
self.interpreter.clean_internal_evaluation_state();
|
||||
self.interpreter.eval_rule_in_path(path)
|
||||
}
|
||||
|
||||
/// Evaluate a Rego query.
|
||||
///
|
||||
/// ```
|
||||
@@ -419,7 +465,7 @@ impl Engine {
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn eval_rule(
|
||||
pub fn eval_rule_in_module(
|
||||
&mut self,
|
||||
module: &Ref<Module>,
|
||||
rule: &Ref<Rule>,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
use crate::ast::*;
|
||||
use crate::builtins::{self, BuiltinFcn};
|
||||
use crate::lexer::*;
|
||||
use crate::number::*;
|
||||
use crate::parser::Parser;
|
||||
use crate::scheduler::*;
|
||||
use crate::utils::*;
|
||||
@@ -13,11 +12,9 @@ use crate::Rc;
|
||||
use crate::{Expression, Extension, Location, QueryResult, QueryResults};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use log::info;
|
||||
use std::collections::btree_map::Entry as BTreeMapEntry;
|
||||
use std::collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap};
|
||||
use std::collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use std::ops::Bound::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
type Scope = BTreeMap<SourceStr, Value>;
|
||||
|
||||
@@ -76,6 +73,7 @@ pub struct Interpreter {
|
||||
|
||||
gather_prints: bool,
|
||||
prints: Vec<String>,
|
||||
rule_paths: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Default for Interpreter {
|
||||
@@ -196,6 +194,7 @@ impl Interpreter {
|
||||
|
||||
gather_prints: false,
|
||||
prints: Vec::default(),
|
||||
rule_paths: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +235,6 @@ impl Interpreter {
|
||||
|
||||
pub fn set_input(&mut self, input: Value) {
|
||||
self.input = input;
|
||||
info!("input: {:#?}", self.input);
|
||||
}
|
||||
|
||||
pub fn init_with_document(&mut self) -> Result<()> {
|
||||
@@ -323,18 +321,18 @@ impl Interpreter {
|
||||
// Stop path collection upon encountering the leading variable.
|
||||
Expr::Var(v) => {
|
||||
path.reverse();
|
||||
return self.lookup_var(v, &path[..], false);
|
||||
return self.lookup_var(&v.0, &path[..], false);
|
||||
}
|
||||
// Accumulate chained . field accesses.
|
||||
Expr::RefDot { refr, field, .. } => {
|
||||
expr = refr;
|
||||
path.push(field.text());
|
||||
path.push(field.0.text());
|
||||
}
|
||||
Expr::RefBrack { refr, index, .. } => match index.as_ref() {
|
||||
// refr["field"] is the same as refr.field
|
||||
Expr::String(s) => {
|
||||
expr = refr;
|
||||
path.push(s.text());
|
||||
path.push(s.0.text());
|
||||
}
|
||||
// Handle other forms of refr.
|
||||
// Note, we have the choice to evaluate a non-string index
|
||||
@@ -416,8 +414,8 @@ impl Interpreter {
|
||||
// Then hoist the current bracket operation.
|
||||
let mut indices = Vec::with_capacity(1);
|
||||
let _ = traverse(index, &mut |e| match e.as_ref() {
|
||||
Var(ident) if self.is_loop_index_var(&ident.source_str()) => {
|
||||
indices.push(ident.source_str());
|
||||
Var(ident) if self.is_loop_index_var(&ident.0.source_str()) => {
|
||||
indices.push(ident.0.source_str());
|
||||
Ok(false)
|
||||
}
|
||||
Array { .. } | Object { .. } => Ok(true),
|
||||
@@ -586,16 +584,16 @@ impl Interpreter {
|
||||
AssignOp::Eq => {
|
||||
match (lhs.as_ref(), rhs.as_ref()) {
|
||||
(_, Expr::Var(var))
|
||||
if var.source_str().text() != "input"
|
||||
&& self.lookup_var(var, &[], true)? == Value::Undefined =>
|
||||
if var.0.source_str().text() != "input"
|
||||
&& self.lookup_var(&var.0, &[], true)? == Value::Undefined =>
|
||||
{
|
||||
(var.source_str(), self.eval_expr(lhs)?)
|
||||
(var.0.source_str(), self.eval_expr(lhs)?)
|
||||
}
|
||||
(Expr::Var(var), _)
|
||||
if var.source_str().text() != "input"
|
||||
&& self.lookup_var(var, &[], true)? == Value::Undefined =>
|
||||
if var.0.source_str().text() != "input"
|
||||
&& self.lookup_var(&var.0, &[], true)? == Value::Undefined =>
|
||||
{
|
||||
(var.source_str(), self.eval_expr(rhs)?)
|
||||
(var.0.source_str(), self.eval_expr(rhs)?)
|
||||
}
|
||||
(
|
||||
Expr::Array {
|
||||
@@ -696,8 +694,8 @@ impl Interpreter {
|
||||
return Ok(rhs_value);
|
||||
}
|
||||
|
||||
let name = if let Expr::Var(span) = lhs.as_ref() {
|
||||
span.source_str()
|
||||
let name = if let Expr::Var(s) = lhs.as_ref() {
|
||||
s.0.source_str()
|
||||
} else {
|
||||
let mut cache = BTreeMap::new();
|
||||
let mut type_match = BTreeSet::new();
|
||||
@@ -731,11 +729,6 @@ impl Interpreter {
|
||||
// TODO: optimize this
|
||||
self.variables_assignment(&name, &value)?;
|
||||
|
||||
info!(
|
||||
"eval_assign_expr before, op: {:?}, lhs: {:?}, rhs: {:?}",
|
||||
op, lhs, rhs
|
||||
);
|
||||
|
||||
Ok(Value::Bool(true))
|
||||
}
|
||||
|
||||
@@ -834,16 +827,16 @@ impl Interpreter {
|
||||
let raise_error = is_last && type_match.get(expr).is_none();
|
||||
|
||||
match (expr.as_ref(), value) {
|
||||
(Expr::Var(ident), _) if ident.text() == "_" => Ok(true),
|
||||
(Expr::Var(ident), _) if ident.0.text() == "_" => Ok(true),
|
||||
(Expr::Var(ident), _)
|
||||
if check_existing_value
|
||||
&& self.lookup_local_var(&ident.source_str()) == Some(value.clone()) =>
|
||||
&& self.lookup_local_var(&ident.0.source_str()) == Some(value.clone()) =>
|
||||
{
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
(Expr::Var(ident), _) => {
|
||||
self.add_variable(&ident.source_str(), value.clone())?;
|
||||
self.add_variable(&ident.0.source_str(), value.clone())?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -1323,13 +1316,6 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_stmt(&mut self, stmt: &LiteralStmt, stmts: &[&LiteralStmt]) -> Result<bool> {
|
||||
debug_new_group!(
|
||||
"eval_stmt {}:{} {}",
|
||||
stmt.span.line,
|
||||
stmt.span.col,
|
||||
stmt.span.text()
|
||||
);
|
||||
|
||||
let (saved_state, skip_exec) = self.apply_with_modifiers(stmt)?;
|
||||
let r = if !skip_exec {
|
||||
self.eval_stmt_impl(stmt, stmts)
|
||||
@@ -1401,7 +1387,7 @@ impl Interpreter {
|
||||
// then evaluate statements only if the index applies to this collection.
|
||||
let loop_expr_index = loop_expr.index();
|
||||
if let Some(Expr::Var(index_var)) = loop_expr_index.as_ref().map(|r| r.as_ref()) {
|
||||
if let Some(idx) = self.lookup_local_var(&index_var.source_str()) {
|
||||
if let Some(idx) = self.lookup_local_var(&index_var.0.source_str()) {
|
||||
if loop_expr_value[&idx] != Value::Undefined {
|
||||
result = self.eval_stmts_in_loop(stmts, &loops[1..])? || result;
|
||||
return Ok(result);
|
||||
@@ -1514,7 +1500,7 @@ impl Interpreter {
|
||||
loop {
|
||||
match expr.as_ref() {
|
||||
Expr::Var(v) => {
|
||||
comps.push(Value::String(v.text().into()));
|
||||
comps.push(Value::String(v.0.text().into()));
|
||||
break;
|
||||
}
|
||||
Expr::RefBrack { refr, index, .. } => {
|
||||
@@ -1522,7 +1508,7 @@ impl Interpreter {
|
||||
expr = refr;
|
||||
}
|
||||
Expr::RefDot { refr, field, .. } => {
|
||||
comps.push(Value::String(field.text().into()));
|
||||
comps.push(Value::String(field.0.text().into()));
|
||||
expr = refr;
|
||||
}
|
||||
_ => {
|
||||
@@ -2404,12 +2390,12 @@ impl Interpreter {
|
||||
if let Some(ea) = extra_arg {
|
||||
match ea.as_ref() {
|
||||
Expr::Var(var)
|
||||
if allow_return_arg && self.lookup_local_var(&var.source_str()).is_none() =>
|
||||
if allow_return_arg && self.lookup_local_var(&var.0.source_str()).is_none() =>
|
||||
{
|
||||
let value =
|
||||
self.eval_call_impl(span, expr, fcn, ¶ms[..params.len() - 1])?;
|
||||
if var.text() != "_" {
|
||||
self.add_variable(&var.source_str(), value)?;
|
||||
if var.0.text() != "_" {
|
||||
self.add_variable(&var.0.source_str(), value)?;
|
||||
}
|
||||
Ok(Value::Bool(true))
|
||||
}
|
||||
@@ -2543,7 +2529,6 @@ impl Interpreter {
|
||||
|
||||
fn lookup_var(&mut self, span: &Span, fields: &[&str], no_error: bool) -> Result<Value> {
|
||||
let name = span.source_str();
|
||||
debug_new_group!("lookup_var: name={name}, fields={fields:?}, no_error={no_error}");
|
||||
|
||||
// Return local variable/argument.
|
||||
if let Some(v) = self.lookup_local_var(&name) {
|
||||
@@ -2648,13 +2633,6 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn eval_expr(&mut self, expr: &ExprRef) -> Result<Value> {
|
||||
debug_new_group!(
|
||||
"eval_expr: {}:{} {}",
|
||||
expr.span().line,
|
||||
expr.span().col,
|
||||
expr.span().text()
|
||||
);
|
||||
|
||||
#[cfg(feature = "coverage")]
|
||||
if self.enable_coverage {
|
||||
let span = expr.span();
|
||||
@@ -2684,24 +2662,10 @@ impl Interpreter {
|
||||
Expr::Null(_) => Ok(Value::Null),
|
||||
Expr::True(_) => Ok(Value::Bool(true)),
|
||||
Expr::False(_) => Ok(Value::Bool(false)),
|
||||
Expr::Number(span) => {
|
||||
let v = match Number::from_str(span.text()) {
|
||||
Ok(v) => Ok(Value::Number(v)),
|
||||
Err(_) => Err(span
|
||||
.source
|
||||
.error(span.line, span.col, "could not parse number")),
|
||||
};
|
||||
v
|
||||
}
|
||||
Expr::Number((_, v)) => Ok(v.clone()),
|
||||
// TODO: Handle string vs rawstring
|
||||
Expr::String(span) => {
|
||||
match serde_json::from_str::<Value>(format!("\"{}\"", span.text()).as_str()) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(e) => bail!(span.error(format!("invalid string literal. {e}").as_str())),
|
||||
}
|
||||
}
|
||||
Expr::RawString(span) => Ok(Value::String(span.text().to_string().into())),
|
||||
|
||||
Expr::String((_, v)) => Ok(v.clone()),
|
||||
Expr::RawString((_, v)) => Ok(v.clone()),
|
||||
// TODO: Handle undefined variables
|
||||
Expr::Var(_) => self.eval_chained_ref_dot_or_brack(expr),
|
||||
Expr::RefDot { .. } => self.eval_chained_ref_dot_or_brack(expr),
|
||||
@@ -2929,17 +2893,17 @@ impl Interpreter {
|
||||
while let Some(e) = expr {
|
||||
match e {
|
||||
Expr::RefDot { refr, field, .. } => {
|
||||
comps.push(field.text());
|
||||
comps.push(field.0.text());
|
||||
expr = Some(refr);
|
||||
}
|
||||
Expr::RefBrack { refr, index, .. } if matches!(index.as_ref(), Expr::String(_)) => {
|
||||
if let Expr::String(s) = index.as_ref() {
|
||||
comps.push(s.text());
|
||||
comps.push(s.0.text());
|
||||
expr = Some(refr);
|
||||
}
|
||||
}
|
||||
Expr::Var(v) => {
|
||||
comps.push(v.text());
|
||||
comps.push(v.0.text());
|
||||
expr = None;
|
||||
}
|
||||
_ => bail!(e.span().error("invalid ref expression")),
|
||||
@@ -3005,7 +2969,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
// The following may evaluate to undefined.
|
||||
Var(span) => ("var", span),
|
||||
Var((span, _)) => ("var", span),
|
||||
Call { span, .. } => ("call", span),
|
||||
UnaryExpr { span, .. } => ("unaryexpr", span),
|
||||
RefDot { span, .. } => ("ref", span),
|
||||
@@ -3379,19 +3343,19 @@ impl Interpreter {
|
||||
loop {
|
||||
refr = match refr.as_ref() {
|
||||
Expr::Var(v) => {
|
||||
components.push(v.text().into());
|
||||
components.push(v.0.text().into());
|
||||
break;
|
||||
}
|
||||
Expr::RefBrack { refr, index, .. } => {
|
||||
if let Expr::String(s) = index.as_ref() {
|
||||
components.push(s.text().into());
|
||||
components.push(s.0.text().into());
|
||||
} else {
|
||||
components.clear();
|
||||
}
|
||||
refr
|
||||
}
|
||||
Expr::RefDot { refr, field, .. } => {
|
||||
components.push(field.text().into());
|
||||
components.push(field.0.text().into());
|
||||
refr
|
||||
}
|
||||
_ => break,
|
||||
@@ -3402,12 +3366,8 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
pub fn create_rule_prefixes(&mut self) -> Result<()> {
|
||||
debug_new_group!("create_rule_prefixes");
|
||||
debug!("data before: {}", self.data);
|
||||
|
||||
for module in self.modules.clone() {
|
||||
let module_path = Self::get_rule_path_components(&module.package.refr)?;
|
||||
debug!("processing module {module_path:?}");
|
||||
|
||||
for rule in &module.policy {
|
||||
let rule_refr = Self::get_rule_refr(rule);
|
||||
@@ -3443,7 +3403,7 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("data after: {}", self.data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3452,6 +3412,9 @@ impl Interpreter {
|
||||
let comps: Vec<&str> = comps.iter().map(|s| s.text()).collect();
|
||||
for c in 0..comps.len() {
|
||||
let path = self.current_module_path.clone() + "." + &comps[0..c + 1].join(".");
|
||||
if c + 1 == comps.len() {
|
||||
self.rule_paths.insert(path.clone());
|
||||
}
|
||||
|
||||
match self.rules.entry(path) {
|
||||
Entry::Occupied(o) => {
|
||||
@@ -3476,6 +3439,10 @@ impl Interpreter {
|
||||
let comps: Vec<&str> = comps.iter().map(|s| s.text()).collect();
|
||||
for (idx, c) in (0..comps.len()).enumerate() {
|
||||
let path = self.current_module_path.clone() + "." + &comps[0..c + 1].join(".");
|
||||
if c + 1 == comps.len() {
|
||||
self.rule_paths.insert(path.clone());
|
||||
}
|
||||
|
||||
match self.default_rules.entry(path) {
|
||||
Entry::Occupied(o) => {
|
||||
if idx + 1 == comps.len() {
|
||||
@@ -3509,12 +3476,12 @@ impl Interpreter {
|
||||
let target = match &import.r#as {
|
||||
Some(s) => s.text(),
|
||||
_ => match import.refr.as_ref() {
|
||||
Expr::RefDot { field, .. } => field.text(),
|
||||
Expr::RefDot { field, .. } => field.0.text(),
|
||||
Expr::RefBrack { index, .. } => match index.as_ref() {
|
||||
Expr::String(s) => s.text(),
|
||||
Expr::String(s) => s.0.text(),
|
||||
_ => "",
|
||||
},
|
||||
Expr::Var(v) if v.text() == "input" => {
|
||||
Expr::Var(v) if v.0.text() == "input" => {
|
||||
// Warn redundant import of input. Ignore it.
|
||||
eprintln!(
|
||||
"{}",
|
||||
@@ -3733,4 +3700,14 @@ impl Interpreter {
|
||||
pub fn take_prints(&mut self) -> Result<Vec<String>> {
|
||||
Ok(std::mem::take(&mut self.prints))
|
||||
}
|
||||
|
||||
pub fn eval_rule_in_path(&mut self, path: String) -> Result<Value> {
|
||||
if !self.rule_paths.contains(&path) {
|
||||
bail!("not a valid rule path");
|
||||
}
|
||||
self.ensure_rule_evaluated(path.clone())?;
|
||||
let parts: Vec<&str> = path.split('.').collect();
|
||||
|
||||
Ok(Self::get_value_chained(self.data.clone(), &parts[1..]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
|
||||
use crate::ast::*;
|
||||
use crate::lexer::*;
|
||||
use crate::number::*;
|
||||
use crate::value::*;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
|
||||
@@ -102,14 +106,14 @@ impl<'source> Parser<'source> {
|
||||
match refr.as_ref() {
|
||||
Expr::RefDot { refr, field, .. } => {
|
||||
Self::get_path_ref_components_into(refr, comps)?;
|
||||
comps.push(field.clone());
|
||||
comps.push(field.0.clone());
|
||||
}
|
||||
Expr::RefBrack { refr, index, .. } => {
|
||||
Self::get_path_ref_components_into(refr, comps)?;
|
||||
Self::get_path_ref_components_into(index, comps)?;
|
||||
}
|
||||
Expr::Var(v) => comps.push(v.clone()),
|
||||
Expr::String(s) => comps.push(s.clone()),
|
||||
Expr::Var(v) => comps.push(v.0.clone()),
|
||||
Expr::String(s) => comps.push(s.0.clone()),
|
||||
_ => bail!("internal error: not a simple ref"),
|
||||
}
|
||||
Ok(())
|
||||
@@ -231,17 +235,38 @@ impl<'source> Parser<'source> {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_number(span: Span) -> Result<Expr> {
|
||||
match Number::from_str(span.text()) {
|
||||
Ok(v) => Ok(Expr::Number((span, Value::Number(v)))),
|
||||
Err(_) => bail!(span.error("could not parse number")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_scalar_or_var(&mut self) -> Result<Expr> {
|
||||
let span = self.tok.1.clone();
|
||||
let node = match &self.tok.0 {
|
||||
TokenKind::Number => Expr::Number(span),
|
||||
TokenKind::String => Expr::String(span),
|
||||
TokenKind::RawString => Expr::RawString(span),
|
||||
TokenKind::Number => Self::read_number(span)?,
|
||||
TokenKind::String => {
|
||||
let v = match serde_json::from_str::<Value>(format!("\"{}\"", span.text()).as_str())
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(e) => bail!(span.error(format!("invalid string literal. {e}").as_str())),
|
||||
};
|
||||
Expr::String((span, v))
|
||||
}
|
||||
TokenKind::RawString => {
|
||||
let v = Value::from(span.text().to_string());
|
||||
Expr::RawString((span, v))
|
||||
}
|
||||
TokenKind::Ident => match self.token_text() {
|
||||
"null" => Expr::Null(span),
|
||||
"true" => Expr::True(span),
|
||||
"false" => Expr::False(span),
|
||||
_ => return Ok(Expr::Var(self.parse_var()?)),
|
||||
_ => {
|
||||
let ident = self.parse_var()?;
|
||||
let v = Value::from(ident.text());
|
||||
return Ok(Expr::Var((ident, v)));
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(self.source.error(
|
||||
@@ -529,10 +554,11 @@ impl<'source> Parser<'source> {
|
||||
)
|
||||
);
|
||||
}
|
||||
let fieldv = Value::from(field.text());
|
||||
term = Expr::RefDot {
|
||||
span,
|
||||
refr: Ref::new(term),
|
||||
field,
|
||||
field: (field, fieldv),
|
||||
};
|
||||
}
|
||||
"[" => {
|
||||
@@ -632,7 +658,7 @@ impl<'source> Parser<'source> {
|
||||
rhs_span.col += 1;
|
||||
|
||||
self.next_token()?;
|
||||
Expr::Number(rhs_span)
|
||||
Self::read_number(rhs_span)?
|
||||
} else {
|
||||
self.next_token()?;
|
||||
self.parse_mul_div_mod_expr()?
|
||||
@@ -786,10 +812,10 @@ impl<'source> Parser<'source> {
|
||||
"=" => AssignOp::Eq,
|
||||
":=" if self.rego_v1 => {
|
||||
if let Expr::Var(v) = &expr {
|
||||
if v.text() == "input" {
|
||||
if v.0.text() == "input" {
|
||||
bail!(span.error("input cannot be shadowed"));
|
||||
}
|
||||
if v.text() == "data" {
|
||||
if v.0.text() == "data" {
|
||||
bail!(span.error("data cannot be shadowed"));
|
||||
}
|
||||
}
|
||||
@@ -1055,11 +1081,16 @@ impl<'source> Parser<'source> {
|
||||
}))
|
||||
}
|
||||
|
||||
fn span_and_value(s: Span) -> (Span, Value) {
|
||||
let v = Value::from(s.text());
|
||||
(s, v)
|
||||
}
|
||||
|
||||
fn parse_path_ref(&mut self) -> Result<Expr> {
|
||||
let start = self.tok.1.start;
|
||||
let var = self.parse_var()?;
|
||||
|
||||
let mut refr = Expr::Var(var);
|
||||
let mut refr = Expr::Var(Self::span_and_value(var));
|
||||
loop {
|
||||
let mut span = self.tok.1.clone();
|
||||
let sep_pos = span.start;
|
||||
@@ -1095,13 +1126,13 @@ impl<'source> Parser<'source> {
|
||||
refr = Expr::RefDot {
|
||||
span,
|
||||
refr: Ref::new(refr),
|
||||
field,
|
||||
field: Self::span_and_value(field),
|
||||
};
|
||||
}
|
||||
"[" => {
|
||||
self.next_token()?;
|
||||
let index = match &self.tok.0 {
|
||||
TokenKind::String => Expr::String(self.tok.1.clone()),
|
||||
TokenKind::String => Expr::String(Self::span_and_value(self.tok.1.clone())),
|
||||
_ => {
|
||||
return Err(self.source.error(
|
||||
self.tok.1.line,
|
||||
@@ -1140,7 +1171,7 @@ impl<'source> Parser<'source> {
|
||||
bail!(span.error("data cannot be shadowed"));
|
||||
}
|
||||
}
|
||||
Expr::Var(v)
|
||||
Expr::Var(Self::span_and_value(v))
|
||||
} else {
|
||||
return Err(self.source.error(
|
||||
span.line,
|
||||
@@ -1184,7 +1215,7 @@ impl<'source> Parser<'source> {
|
||||
term = Expr::RefDot {
|
||||
span,
|
||||
refr: Ref::new(term),
|
||||
field,
|
||||
field: Self::span_and_value(field),
|
||||
};
|
||||
}
|
||||
"[" => {
|
||||
@@ -1488,7 +1519,10 @@ impl<'source> Parser<'source> {
|
||||
Ok(Rule::Default {
|
||||
span,
|
||||
refr: rule_ref,
|
||||
args: args.into_iter().map(|a| Ref::new(Expr::Var(a))).collect(),
|
||||
args: args
|
||||
.into_iter()
|
||||
.map(|a| Ref::new(Expr::Var(Self::span_and_value(a))))
|
||||
.collect(),
|
||||
op,
|
||||
value,
|
||||
})
|
||||
|
||||
@@ -299,23 +299,23 @@ fn gather_assigned_vars(
|
||||
) -> Result<()> {
|
||||
traverse(expr, &mut |e| match e.as_ref() {
|
||||
// Ignore _, input, data.
|
||||
Var(v) if matches!(v.text(), "_" | "input" | "data") => Ok(false),
|
||||
Var(v) if matches!(v.0.text(), "_" | "input" | "data") => Ok(false),
|
||||
|
||||
// Record local var that can shadow input var.
|
||||
Var(v) if can_shadow => {
|
||||
scope.locals.insert(v.source_str(), v.clone());
|
||||
scope.locals.insert(v.0.source_str(), v.0.clone());
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// Record input vars.
|
||||
Var(v) if var_exists(v, parent_scopes) => {
|
||||
scope.inputs.insert(v.source_str());
|
||||
Var(v) if var_exists(&v.0, parent_scopes) => {
|
||||
scope.inputs.insert(v.0.source_str());
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// Record local var.
|
||||
Var(v) => {
|
||||
scope.unscoped.insert(v.source_str());
|
||||
scope.unscoped.insert(v.0.source_str());
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
@@ -327,8 +327,10 @@ fn gather_assigned_vars(
|
||||
|
||||
fn gather_input_vars(expr: &Ref<Expr>, parent_scopes: &[Scope], scope: &mut Scope) -> Result<()> {
|
||||
traverse(expr, &mut |e| match e.as_ref() {
|
||||
Var(v) if !scope.unscoped.contains(&v.source_str()) && var_exists(v, parent_scopes) => {
|
||||
scope.inputs.insert(v.source_str());
|
||||
Var(v)
|
||||
if !scope.unscoped.contains(&v.0.source_str()) && var_exists(&v.0, parent_scopes) =>
|
||||
{
|
||||
scope.inputs.insert(v.0.source_str());
|
||||
Ok(false)
|
||||
}
|
||||
_ => Ok(true),
|
||||
@@ -537,7 +539,7 @@ impl Analyzer {
|
||||
for a in args.iter() {
|
||||
traverse(a, &mut |e| {
|
||||
if let Var(v) = e.as_ref() {
|
||||
scope.unscoped.insert(v.source_str());
|
||||
scope.unscoped.insert(v.0.source_str());
|
||||
}
|
||||
Ok(true)
|
||||
})?;
|
||||
@@ -630,10 +632,10 @@ impl Analyzer {
|
||||
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();
|
||||
Var(v) if !matches!(v.0.text(), "_" | "input" | "data") => {
|
||||
let name = v.0.source_str();
|
||||
let is_extra_arg = match assigned_vars {
|
||||
Some(vars) => vars.contains(&v.source_str()),
|
||||
Some(vars) => vars.contains(&v.0.source_str()),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -642,7 +644,7 @@ impl Analyzer {
|
||||
{
|
||||
if !is_extra_arg {
|
||||
used_vars.push(name.clone());
|
||||
first_use.entry(name).or_insert(v.clone());
|
||||
first_use.entry(name).or_insert(v.0.clone());
|
||||
}
|
||||
} else if !scope.inputs.contains(&name) {
|
||||
#[cfg(feature = "deprecated")]
|
||||
@@ -656,7 +658,9 @@ impl Analyzer {
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!(v.error(format!("use of undefined variable `{name}` is unsafe").as_str()));
|
||||
bail!(v
|
||||
.0
|
||||
.error(format!("use of undefined variable `{name}` is unsafe").as_str()));
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
@@ -664,7 +668,7 @@ impl Analyzer {
|
||||
RefBrack { refr, index, .. } => {
|
||||
traverse(index, &mut |e| match e.as_ref() {
|
||||
Var(v) => {
|
||||
let var = v.source_str();
|
||||
let var = v.0.source_str();
|
||||
if scope.locals.contains_key(&var) || scope.unscoped.contains(&var) {
|
||||
let (rb_used_vars, rb_comprs) =
|
||||
Self::gather_used_vars_comprs_index_vars(
|
||||
@@ -758,10 +762,10 @@ impl Analyzer {
|
||||
let mut vars = vec![];
|
||||
traverse(expr, &mut |e| match e.as_ref() {
|
||||
Var(v) => {
|
||||
let var = v.source_str();
|
||||
let var = v.0.source_str();
|
||||
if scope.locals.contains_key(&var) {
|
||||
if check_first_use {
|
||||
Self::check_first_use(v, first_use)?;
|
||||
Self::check_first_use(&v.0, first_use)?;
|
||||
}
|
||||
vars.push(var);
|
||||
} else if scope.unscoped.contains(&var) {
|
||||
@@ -947,8 +951,8 @@ impl Analyzer {
|
||||
non_vars: &mut Vec<Ref<Expr>>,
|
||||
) -> Result<()> {
|
||||
traverse(expr, &mut |e| match e.as_ref() {
|
||||
Var(v) if scope.locals.contains_key(&v.source_str()) => {
|
||||
vars.push(v.source_str());
|
||||
Var(v) if scope.locals.contains_key(&v.0.source_str()) => {
|
||||
vars.push(v.0.source_str());
|
||||
Ok(false)
|
||||
}
|
||||
// TODO: Object key/value
|
||||
|
||||
80
src/utils.rs
80
src/utils.rs
@@ -8,95 +8,23 @@ use crate::lexer::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
macro_rules! debug {
|
||||
($($arg:tt)+) => {
|
||||
{
|
||||
if log::log_enabled!(log::Level::Debug) {
|
||||
print!("{}:{}:", file!(), line!());
|
||||
crate::utils::NESTING.with(|f| {
|
||||
print!("{}", " ".repeat(*f.borrow() as usize));
|
||||
});
|
||||
println!($($arg)+);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
macro_rules! debug {
|
||||
($($arg:tt)+) => {};
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) use debug;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[allow(unused)]
|
||||
macro_rules! debug_new_group {
|
||||
($($arg:tt)+) => {
|
||||
debug!($($arg)+);
|
||||
let _group = DebugNesting::new();
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
macro_rules! debug_new_group {
|
||||
($($arg:tt)+) => {};
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) use debug_new_group;
|
||||
|
||||
#[allow(unused)]
|
||||
pub struct DebugNesting {}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
thread_local!(pub static NESTING: std::cell::RefCell<u32> = std::cell::RefCell::new(1));
|
||||
|
||||
impl DebugNesting {
|
||||
#[cfg(debug_assertions)]
|
||||
#[allow(unused)]
|
||||
pub fn new() -> DebugNesting {
|
||||
NESTING.with(|f| {
|
||||
*f.borrow_mut() += 1;
|
||||
});
|
||||
DebugNesting {}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl Drop for DebugNesting {
|
||||
#[cfg(debug_assertions)]
|
||||
fn drop(&mut self) {
|
||||
NESTING.with(|f| {
|
||||
*f.borrow_mut() -= 1;
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn drop(&mut self) {}
|
||||
}
|
||||
|
||||
pub fn get_path_string(refr: &Expr, document: Option<&str>) -> Result<String> {
|
||||
let mut comps: Vec<&str> = vec![];
|
||||
let mut expr = Some(refr);
|
||||
while expr.is_some() {
|
||||
match expr {
|
||||
Some(Expr::RefDot { refr, field, .. }) => {
|
||||
comps.push(field.text());
|
||||
comps.push(field.0.text());
|
||||
expr = Some(refr);
|
||||
}
|
||||
Some(Expr::RefBrack { refr, index, .. }) => {
|
||||
if let Expr::String(s) = index.as_ref() {
|
||||
comps.push(s.text());
|
||||
comps.push(s.0.text());
|
||||
}
|
||||
expr = Some(refr);
|
||||
}
|
||||
Some(Expr::Var(v)) => {
|
||||
comps.push(v.text());
|
||||
comps.push(v.0.text());
|
||||
expr = None;
|
||||
}
|
||||
_ => bail!("internal error: not a simple ref {expr:?}"),
|
||||
@@ -193,7 +121,7 @@ pub fn get_root_var(mut expr: &Expr) -> Result<SourceStr> {
|
||||
let empty = expr.span().source_str().clone_empty();
|
||||
loop {
|
||||
match expr {
|
||||
Expr::Var(v) => return Ok(v.source_str()),
|
||||
Expr::Var(v) => return Ok(v.0.source_str()),
|
||||
Expr::RefDot { refr, .. } | Expr::RefBrack { refr, .. } => expr = refr,
|
||||
_ => return Ok(empty),
|
||||
}
|
||||
|
||||
71
src/value.rs
71
src/value.rs
@@ -460,8 +460,8 @@ impl From<u128> for Value {
|
||||
/// # 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")?);
|
||||
/// Value::from(340_282_366_920_938_463_463_374_607_431_768_211_455u128).as_u128()?,
|
||||
/// 340_282_366_920_938_463_463_374_607_431_768_211_455u128);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
fn from(n: u128) -> Self {
|
||||
@@ -475,8 +475,8 @@ impl From<i128> for Value {
|
||||
/// # use regorus::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// assert_eq!(
|
||||
/// Value::from(-170141183460469231731687303715884105728i128),
|
||||
/// Value::from_json_str("-170141183460469231731687303715884105728")?);
|
||||
/// Value::from(-170141183460469231731687303715884105728i128).as_i128()?,
|
||||
/// -170141183460469231731687303715884105728i128);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
fn from(n: i128) -> Self {
|
||||
@@ -551,7 +551,7 @@ impl From<f64> for Value {
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// assert_eq!(
|
||||
/// Value::from(3.141592653589793),
|
||||
/// Value::from_json_str("3.141592653589793")?);
|
||||
/// Value::from_numeric_string("3.141592653589793")?);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -559,19 +559,19 @@ impl From<f64> for Value {
|
||||
/// 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).
|
||||
/// Value parsed from numeric 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")?);
|
||||
/// Value::from_numeric_string("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")?);
|
||||
/// Value::from_numeric_string("3.14159265358979323")?);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -584,6 +584,55 @@ impl From<f64> for Value {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Value> for Value {
|
||||
/// Create a [`Value`] from [`serde_json::Value`].
|
||||
///
|
||||
/// Returns [`Value::Undefined`] in case of error.
|
||||
/// ```
|
||||
/// # use regorus::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// let json_v = serde_json::json!({ "x":10, "y": 20 });
|
||||
/// let v = Value::from(json_v);
|
||||
///
|
||||
/// assert_eq!(v["x"].as_u64()?, 10);
|
||||
/// assert_eq!(v["y"].as_u64()?, 20);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
fn from(v: serde_json::Value) -> Self {
|
||||
match serde_json::from_value(v) {
|
||||
Ok(v) => v,
|
||||
_ => Value::Undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "yaml")]
|
||||
impl From<serde_yaml::Value> for Value {
|
||||
/// Create a [`Value`] from [`serde_yaml::Value`].
|
||||
///
|
||||
/// Returns [`Value::Undefined`] in case of error.
|
||||
/// ```
|
||||
/// # use regorus::*;
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// let yaml = "
|
||||
/// x: 10
|
||||
/// y: 20
|
||||
/// ";
|
||||
/// let yaml_v : serde_yaml::Value = serde_yaml::from_str(&yaml).unwrap();
|
||||
/// let v = Value::from(yaml_v);
|
||||
///
|
||||
/// assert_eq!(v["x"].as_u64()?, 10);
|
||||
/// assert_eq!(v["y"].as_u64()?, 20);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
fn from(v: serde_yaml::Value) -> Self {
|
||||
match serde_yaml::from_value(v) {
|
||||
Ok(v) => v,
|
||||
_ => Value::Undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// Create a [`Value::Number`] from a string containing numeric representation of a number.
|
||||
///
|
||||
@@ -594,9 +643,9 @@ impl Value {
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// let v = Value::from_numeric_string("3.14159265358979323846264338327950288419716939937510")?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// v.to_json_str()?,
|
||||
/// "3.1415926535897932384626433832795028841971693993751");
|
||||
/// println!("{}", v.to_json_str()?);
|
||||
/// // Prints 3.1415926535897932384626433832795028841971693993751 if serde_json/arbitrary_precision feature is enabled.
|
||||
/// // Prints 3.141592653589793 if serde_json/arbitrary_precision is not enabled.
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
|
||||
@@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
const OPA_REPO: &str = "https://github.com/open-policy-agent/opa";
|
||||
const OPA_BRANCH: &str = "v0.62.0";
|
||||
const OPA_BRANCH: &str = "v0.63.0";
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
|
||||
@@ -102,13 +102,13 @@ fn match_expr_impl(e: &Expr, v: &Value) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
match e {
|
||||
Expr::String(s) => match_span(s, &v["string"]),
|
||||
Expr::RawString(s) => match_span(s, &v["rawstring"]),
|
||||
Expr::Number(s) => match_span(s, &v["number"]),
|
||||
Expr::String(s) => match_span(&s.0, &v["string"]),
|
||||
Expr::RawString(s) => match_span(&s.0, &v["rawstring"]),
|
||||
Expr::Number(s) => match_span(&s.0, &v["number"]),
|
||||
Expr::True(s) => match_span(s, v),
|
||||
Expr::False(s) => match_span(s, v),
|
||||
Expr::Null(s) => match_span(s, v),
|
||||
Expr::Var(s) => match_span(s, &v["var"]),
|
||||
Expr::Var(s) => match_span(&s.0, &v["var"]),
|
||||
Expr::Array { span, items } => match_vec(span, items, &v["array"]),
|
||||
Expr::Set { span, items } => match_vec(span, items, &v["set"]),
|
||||
Expr::Object { span, fields } => match_object(span, fields, &v["object"]),
|
||||
@@ -141,7 +141,7 @@ fn match_expr_impl(e: &Expr, v: &Value) -> Result<()> {
|
||||
Expr::RefDot { span, refr, field } => {
|
||||
match_span_opt(span, &v["refdot"]["span"])?;
|
||||
match_expr(refr, &v["refdot"]["refr"])?;
|
||||
match_span(field, &v["refdot"]["field"])
|
||||
match_span(&field.0, &v["refdot"]["field"])
|
||||
}
|
||||
Expr::RefBrack { span, refr, index } => {
|
||||
match_span_opt(span, &v["refbrack"]["span"])?;
|
||||
|
||||
Reference in New Issue
Block a user