GithubHelp home page GithubHelp logo

go-bexpr's Introduction

bexpr - Boolean Expression Evaluator GoDoc CircleCI

bexpr is a Go (golang) library to provide generic boolean expression evaluation and filtering for Go data structures and maps. Under the hood, bexpr uses pointerstructure, meaning that any path within a map or structure that can be expressed via that library can be used with bexpr. This also means that you can use the custom bexpr dotted syntax (kept mainly for backwards compatibility) to select values in expressions, or, by enclosing the selectors in quotes, you can use JSON Pointer syntax to select values in expressions.

Usage (Reflection)

This example program is available in examples/simple

package main

import (
   "fmt"
   "github.com/hashicorp/go-bexpr"
)

type Example struct {
   X int

   // Can rename a field with the struct tag
   Y string `bexpr:"y"`
   Z bool `bexpr:"foo"`

   // Tag with "-" to prevent allowing this field from being used
   Hidden string `bexpr:"-"`

   // Unexported fields are not available for evaluation
   unexported string
}

func main() {
   value := map[string]Example{
      "foo": Example{X: 5, Y: "foo", Z: true, Hidden: "yes", unexported: "no"},
      "bar": Example{X: 42, Y: "bar", Z: false, Hidden: "no", unexported: "yes"},
   }

   expressions := []string{
		"foo.X == 5",
		"bar.y == bar",
		"foo.baz == true",

		// will error in evaluator creation
		"bar.Hidden != yes",

		// will error in evaluator creation
		"foo.unexported == no",
	}

   for _, expression := range expressions {
      eval, err := bexpr.CreateEvaluator(expression)

      if err != nil {
         fmt.Printf("Failed to create evaluator for expression %q: %v\n", expression, err)
         continue
      }

      result, err := eval.Evaluate(value)
      if err != nil {
         fmt.Printf("Failed to run evaluation of expression %q: %v\n", expression, err)
         continue
      }

      fmt.Printf("Result of expression %q evaluation: %t\n", expression, result)
   }
}

This will output:

Result of expression "foo.X == 5" evaluation: true
Result of expression "bar.y == bar" evaluation: true
Result of expression "foo.baz == true" evaluation: true
Failed to run evaluation of expression "bar.Hidden != yes": error finding value in datum: /bar/Hidden at part 1: struct field "Hidden" is ignored and cannot be used
Failed to run evaluation of expression "foo.unexported == no": error finding value in datum: /foo/unexported at part 1: couldn't find struct field with name "unexported"

Testing

The Makefile contains 3 main targets to aid with testing:

  1. make test - runs the standard test suite
  2. make coverage - runs the test suite gathering coverage information
  3. make bench - this will run benchmarks. You can use the benchcmp tool to compare subsequent runs of the tool to compare performance. There are a few arguments you can provide to the make invocation to alter the behavior a bit
    • BENCHFULL=1 - This will enable running all the benchmarks. Some could be fairly redundant but could be useful when modifying specific sections of the code.
    • BENCHTIME=5s - By default the -benchtime paramater used for the go test invocation is 2s. 1s seemed like too little to get results consistent enough for comparison between two runs. For the highest degree of confidence that performance has remained steady increase this value even further. The time it takes to run the bench testing suite grows linearly with this value.
    • BENCHTESTS=BenchmarkEvaluate - This is used to run a particular benchmark including all of its sub-benchmarks. This is just an example and "BenchmarkEvaluate" can be replaced with any benchmark functions name.

go-bexpr's People

Contributors

alvin-huang avatar claire-labry avatar dependabot[bot] avatar fkarakas avatar hashicorp-copywrite[bot] avatar hc-github-team-es-release-engineering avatar jefferai avatar mitchellh avatar mkeeler avatar remilapeyre avatar schmichael avatar shore avatar talanknight avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-bexpr's Issues

Comparisons against missing map keys return an error unless preceded by an "in" check

hashicorp/nomad#16758 pointed out the following surprising bexpr behavior which can be reproduced with: https://go.dev/play/p/iHY_PWXljwW

To provide an inline example:

type Node struct {
  // ...other fields exist but are unrelated...

  Meta map[string]string
}

The filter "foo" in Meta and Meta["foo"] == "bar" works as expected: Node instances with a foo=bar entry are returned. Node instances without a foo key or a foo key equal to other values are dropped.

Surprisingly the seemingly equivalent filter Meta["foo"] == "bar" behaves differently: Node instances without a foo key will return an error. This breaks Filter.Execute(...) uses, and it's not really possible for the caller of Evaluator.Evaluate(...) to tell if the returned error is for a map key or a struct field.

Proposal

I think field lookups on maps should be given special treatment to avoid requiring "k" in m and ... prefixes anytime m varies in shape between objects.

I can think of 2 options if we want to fix this:

Option 1: Skip unknown map keys

When pointerstructure returns ErrNotFound, peek at the reflect.Kind of the parent path and if it exists and is a map: return false instead of an error.

The major downside of this approach is bexpr users who don't use dynamically shaped maps. If all your maps have the same keys, the existing behavior is fine. This seems like an unusual use case though.

Option 2: Option to skip unknown map keys

Same behavior as Option 1 but put behind an Option.

The major downside to this approach is that variation between implementations (eg Nomad and Consul) seem unnecessarily awkward. I can't imagine why anyone wouldn't want this behavior.

Non-option: UnknownValue

The UnknownValue("") option doesn't work for 2 reasons:

  1. UnknownValue ignores invalid struct fields in addition to the desirable skip-missing-map-keys behavior. These errors are desirable so users can tell the difference between a typo and no-matching-objects.
  2. UnknownValue lacks the ability to ensure a missing map key is skipped (returns false from evaluate(...)). Since UnknownValue always coalesces missing values to a valid value, it's impossible to different missing fields from a match. There are hacks around this like setting the unknown value to a UUID or a hash of the input expression, but you try getting that past coworkers in a review. ๐Ÿ˜…

I actually don't know when UnknownValue would be useful due to the above caveats. ๐Ÿคท

Implement better support for embedded/anonymous types

Right now you have to use the <embedded struct type name>.<field name> to select an field in an embedded type. In Go we can elide the type name and just select the field name so go-bexpr should allow similar things.

GitHub Actions - deprecated warnings found - action required!

Workflow Name: Test and Build
Branch: main
Run URL: https://github.com/hashicorp/go-bexpr/actions/runs/4852829733

save-state deprecation warnings: 0
set-output deprecation warnings: 1
node12 deprecation warnings: 1

Please review these deprecation warnings as soon as possible and merge in the necessary updates.

GitHub will be removing support for these commands and plan to fully disable them on 31st May 2023. At this time, any workflow that still utilizes these commands will fail. See https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/.

GitHub have not finalized a date for deprecating node12 yet but have indicated that this will be summer 2023. So it is advised to switch to node16 asap. See https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/.

If you need any help, please reach out to us in #team-rel-eng.

Implement support of cyclical data structures.

Right now if we try to generate field configurations on a cyclical data structure the reflection logic will generate a stack overflow due to recursing too many times.

One way I have been thinking about fixing this is to keep a map of types to field configurations during the generation phase. If we encounter a type a second time we just reuses the memoized field configuration and do not recurse. I hacked this in there and it seemed to work but I probably should do it in a little cleaner way.

Operator Resolution Order

I'm interested in using this library to filter large amounts of candidates efficiently, but there doesn't seem from my initial reading to be any support for using parenthesis to control the evaluation order. Is there any support or workaround for order-sensitive evaluations?

Panic on using map[string]interface{}

I am trying to use this library for evaluation of user provided expressions. Which means that there are no defined structs for objects. Instead it is a parsed JSON, (which simply outputs map[string]interface{}). However, this library seems unable to handle such use case? Or am I using it wrong way?

Example code:

func main() {
	userjson := `{"id": 123}`

	filejson := `{"owner": 123}`

	var User map[string]interface{}
	var File map[string]interface{}

	err := fastjson.Unmarshal([]byte(userjson), &User)
	if err != nil {
		panic(err)
	}

	err = fastjson.Unmarshal([]byte(filejson), &File)
	if err != nil {
		panic(err)
	}

	type Env map[string]interface{}

	e := map[string]interface{}{
		"file": File,
		"user": User,
	}

	expr := "file.owner == user.id"

	eval, err := bexpr.CreateEvaluatorForType(expr, nil, (*map[string]interface{})(nil))
	if err != nil {
		panic(err)
	}

	result, err := eval.Evaluate(e)
	if err != nil {
		panic(err)
	}

	fmt.Println(result)
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.