Commit e1de2fbc authored by Denis Arh's avatar Denis Arh
Browse files

Add basic integeration test infrastrcuture, apitest package

parent 6957d284
......@@ -3,7 +3,7 @@
/*.iml
/.env*
/.dev*
/.cover*
/.*cover.out*
/overalls*
profile.coverprofile
/public_html
......
......@@ -33,6 +33,22 @@
pruneopts = "UT"
revision = "2a93ba0f464da2a77748a1dc2e9e5bea8bbdd4b9"
[[projects]]
digest = "1:e3983013d82da8c49fb6776c6317d0da4b9cb926f1bb11af37f8c975889db042"
name = "github.com/PaesslerAG/gval"
packages = ["."]
pruneopts = "UT"
revision = "0ce847fc5e6163ee05ae770a441ba497fc77dcaa"
version = "v1.0.1"
[[projects]]
digest = "1:07379bf741135a43b7010cc5b52885900ce8dabf7ab1eb774ee9f331c386199d"
name = "github.com/PaesslerAG/jsonpath"
packages = ["."]
pruneopts = "UT"
revision = "c18d0f043db32b5d4295e14c6518fa9160e45d15"
version = "0.1.1"
[[projects]]
branch = "master"
digest = "1:8029ebc737ffdac4a8df494853595eb086e93b24be9b5e439087f2c8a8f24698"
......@@ -338,6 +354,14 @@
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
version = "v0.8.1"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:bd9efe4e0b0f768302a1e2f0c22458149278de533e521206e5ddc71848c269a0"
......@@ -443,6 +467,30 @@
revision = "298182f68c66c05229eb03ac171abe6e309ee79a"
version = "v1.0.3"
[[projects]]
digest = "1:f23c558574deb8daa162da8defc384a310813761b60ddb7299077b96a03560d2"
name = "github.com/steinfletcher/apitest"
packages = ["."]
pruneopts = "UT"
revision = "9819a0ef6ff226a0728903954acb81bf89f51caa"
version = "v1.3.8"
[[projects]]
digest = "1:f82068df13b0f00eb8c616d4c29c5a064d4fe3c9271a435911b02f5ea137676a"
name = "github.com/steinfletcher/apitest-jsonpath"
packages = ["."]
pruneopts = "UT"
revision = "732f9b76bd271130a9a7fc87466de104ff033c85"
version = "v1.3.0"
[[projects]]
digest = "1:8548c309c65a85933a625be5e7d52b6ac927ca30c56869fae58123b8a77a75e1"
name = "github.com/stretchr/testify"
packages = ["assert"]
pruneopts = "UT"
revision = "221dbe5ed46703ee255b1da0dec05086f5035f62"
version = "v1.4.0"
[[projects]]
branch = "master"
digest = "1:6f9b3f65b090dd2c458eed073ce9a861836060077009f92148e9ccf5d0e42fe8"
......@@ -687,6 +735,14 @@
revision = "730df5f748271903322feb182be83b43ebbbe27d"
version = "v2.3.1"
[[projects]]
digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
version = "v2.2.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
......@@ -732,6 +788,8 @@
"github.com/spf13/afero",
"github.com/spf13/cast",
"github.com/spf13/cobra",
"github.com/steinfletcher/apitest",
"github.com/steinfletcher/apitest-jsonpath",
"github.com/titpetric/factory",
"github.com/titpetric/factory/logger",
"github.com/titpetric/factory/resputil",
......
.PHONY: help docker docker-push realize dep dep.update test test.messaging test.compose qa critic vet codegen integration
PKG = "github.com/$(shell cat .project)"
GO = go
GOGET = $(GO) get -u
GOTEST = go test
......@@ -9,6 +7,14 @@ GOTEST = go test
BASEPKGS = system compose messaging
IMAGES = corteza-server-system corteza-server-compose corteza-server-messaging corteza-server
# Run watcher with a different event-trigger delay, eg:
# $> WATCH_DELAY=5s make watch.test.integration
WATCH_DELAY ?= 1s
# Run go test cmd with flags, eg:
# $> TEST_FLAGS=-v make test.integration
TEST_FLAGS ?=
########################################################################################################################
# Tool bins
DEP = $(GOPATH)/bin/dep
......@@ -18,6 +24,11 @@ MOCKGEN = ${GOPATH}/bin/mockgen
STATICCHECK = ${GOPATH}/bin/staticcheck
PROTOGEN = ${GOPATH}/bin/protoc-gen-go
# Using nodemon in development environment for "watch.*" tasks
# https://nodemon.io
NODEMON = /usr/local/bin/nodemon
WATCHER = $(NODEMON) --delay ${WATCH_DELAY} -e go -w . --exec
help:
@echo
@echo Usage: make [target]
......@@ -63,9 +74,29 @@ codegen: $(PROTOGEN)
mailhog.up:
docker run --rm --publish 8025:8025 --publish 1025:1025 mailhog/mailhog
watch.test.integration: $(NODEMON)
# Development helper - watches for file
# changes & reruns integration tests
$(WATCHER) "make test.integration"
watch.test.integration.coverage: $(NODEMON)
# Development helper - watches for file
# changes & reruns integration tests
$(WATCHER) "make test.integration.coverage"
########################################################################################################################
# QA
## refactored
test.integration:
$(GO) test $(TEST_FLAGS) ./tests/...
test.integration.coverage:
$(GO) test $(TEST_FLAGS) -covermode=count -coverprofile=.integration.cover.out -coverpkg=./... ./tests/...
## old:
test:
# Run basic unit tests
$(GO) test ./pkg/... ./internal/... ./compose/... ./messaging/... ./system/...
......@@ -170,5 +201,8 @@ $(STATICCHECK):
$(PROTOGEN):
$(GOGET) github.com/golang/protobuf/protoc-gen-go
$(NODEMON):
npm install -g nodemon
clean:
rm -f $(REALIZE) $(GOCRITIC)
......@@ -14,6 +14,7 @@ import (
type (
token struct {
// Expiration time in minutes
expiry int64
tokenAuth *jwtauth.JWTAuth
}
......
package helpers
import (
"encoding/json"
"net/http"
"github.com/pkg/errors"
)
type (
Assert func(*http.Response, *http.Request) error
StdErrorResponse struct{ Error struct{ Message string } }
)
// decodes response body to given struct
func decodeBody(rsp *http.Response, s interface{}) error {
if err := json.NewDecoder(rsp.Body).Decode(&s); err != nil {
return errors.Wrap(err, "could not assert IsAuthorized")
}
return nil
}
// Returns first input that could be an error
func firstErr(ee ...interface{}) error {
for _, e := range ee {
switch t := e.(type) {
case error:
if t != nil {
return t
}
case StdErrorResponse:
if t.Error.Message != "" {
return errors.New(t.Error.Message)
}
case string:
if t != "" {
return errors.New(t)
}
}
}
return nil
}
// Ensures there are no errors in the response
func NoErrors(rsp *http.Response, _ *http.Request) (err error) {
tmp := StdErrorResponse{}
return firstErr(decodeBody(rsp, &tmp), tmp)
}
package helpers
import (
"net/http"
"github.com/go-chi/chi"
"github.com/steinfletcher/apitest"
"github.com/cortezaproject/corteza-server/internal/auth"
"github.com/cortezaproject/corteza-server/internal/rand"
"github.com/cortezaproject/corteza-server/system/types"
)
var (
jwtHandler auth.TokenHandler
)
func InitAuth() {
if jwtHandler != nil {
return
}
var err error
jwtHandler, err = auth.JWT(string(rand.Bytes(32)), 10)
if err != nil {
panic(err)
}
}
func BindAuthMiddleware(r chi.Router) {
r.Use(
jwtHandler.HttpVerifier(),
jwtHandler.HttpAuthenticator(),
)
}
func ReqHeaderAuthBearer(user *types.User) apitest.Intercept {
return func(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+jwtHandler.Encode(user))
}
}
package helpers
import (
"os"
"github.com/joho/godotenv"
)
// RecursiveDotEnvLoad loads ENV variables from .evn files 3 levels down
func RecursiveDotEnvLoad() {
for _, loc := range []string{".env", "../.env", "../../.env"} {
if _, err := os.Stat(loc); err == nil {
godotenv.Load(loc)
}
}
}
package messaging
import (
"net/http"
"testing"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/cortezaproject/corteza-server/tests/helpers"
)
func TestChannelList(t *testing.T) {
NewApiTest("get list of channels", &types.User{ID: 5}).
Get("/channels/").
Expect(t).
Status(http.StatusOK).
Assert(helpers.NoErrors).
End()
}
func TestChannelRead(t *testing.T) {
NewApiTest("find single channel by ID", &types.User{ID: 5}).
Get("/channels/324234").
Expect(t).
Status(http.StatusOK).
Assert(helpers.NoErrors).
End()
}
func TestChannelCreate(t *testing.T) {
t.Skip()
NewApiTest("create channel", &types.User{ID: 5}).
Post("/channels/").
Body(`{"name":"test channel"}`).
Expect(t).
Status(http.StatusOK).
Assert(helpers.NoErrors).
End()
}
package messaging
import (
"context"
"os"
"testing"
_ "github.com/joho/godotenv/autoload"
"github.com/go-chi/chi"
"go.uber.org/zap"
"github.com/steinfletcher/apitest"
"github.com/cortezaproject/corteza-server/messaging"
"github.com/cortezaproject/corteza-server/messaging/rest"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/pkg/cli"
"github.com/cortezaproject/corteza-server/pkg/logger"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/cortezaproject/corteza-server/tests/helpers"
)
var (
cfg *cli.Config
r chi.Router
)
func InitConfig() {
if cfg != nil {
return
}
helpers.RecursiveDotEnvLoad()
ctx := context.Background()
log, _ := zap.NewDevelopment()
cfg = messaging.Configure()
cfg.Log = log
cfg.Init()
if err := cfg.RootCommandDBSetup.Run(ctx, nil, cfg); err != nil {
panic(err)
}
logger.SetDefault(log)
cfg.InitServices(ctx, cfg)
}
func InitApp() {
InitConfig()
helpers.InitAuth()
if r != nil {
return
}
r = chi.NewRouter()
r.Use(api.Base(logger.Default())...)
helpers.BindAuthMiddleware(r)
rest.MountRoutes(r)
}
func NewApiTest(name string, user *types.User) *apitest.APITest {
InitApp()
return apitest.
New(name).
Handler(r).
Intercept(helpers.ReqHeaderAuthBearer(user))
}
func TestMain(m *testing.M) {
InitApp()
os.Exit(m.Run())
}
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
coverage.out
manual_test.go
*.out
*.err
.vscode
\ No newline at end of file
language: go
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
script:
- go test -bench=. -benchmem -timeout 10m -coverprofile coverage.out
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN
- go test -bench=Random -benchtime 5m -timeout 30m -benchmem -coverprofile coverage.out
go: "1.11"
Copyright (c) 2017, Paessler AG <support@paessler.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
# Gval
[![Godoc](https://godoc.org/github.com/PaesslerAG/gval?status.png)](https://godoc.org/github.com/PaesslerAG/gval)
[![Build Status](https://api.travis-ci.org/PaesslerAG/gval.svg?branch=master)](https://travis-ci.org/PaesslerAG/gval)
[![Coverage Status](https://coveralls.io/repos/github/PaesslerAG/gval/badge.svg?branch=master)](https://coveralls.io/github/PaesslerAG/gval?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/PaesslerAG/gval)](https://goreportcard.com/report/github.com/PaesslerAG/gval)
Gval (Go eVALuate) provides support for evaluating arbitrary expressions, in particular Go-like expressions.
![gopher](./prtg-batmin-gopher.png)
## Evaluate
Gval can evaluate expressions with parameters, arimethetic, logical, and string operations:
- basic expression: [10 > 0](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Basic)
- parameterized expression: [foo > 0](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Parameter)
- nested parameterized expression: [foo.bar > 0](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--NestedParameter)
- arithmetic expression: [(requests_made * requests_succeeded / 100) >= 90](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Arithmetic)
- string expression: [http_response_body == "service is ok"](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--String)
- float64 expression: [(mem_used / total_mem) * 100](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Float64)
It can easily be extended with custom functions or operators:
- custom date comparator: [date(\`2014-01-02\`) > date(\`2014-01-01 23:59:59\`)](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--DateComparison)
- string length: [strlen("someReallyLongInputString") <= 16](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Strlen)
You can parse gval.Expressions once and re-use them multiple times. Parsing is the compute-intensive phase of the process, so if you intend to use the same expression with different parameters, just parse it once:
- [Parsing and Evaluation](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluable)
The normal Go-standard order of operators is respected. When writing an expression, be sure that you either order the operators correctly, or use parentheses to clarify which portions of an expression should be run first.
Strings, numbers, and booleans can be used like in Go:
- [(7 < "47" == true ? "hello world!\n\u263a") + \` more text\`](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Encoding)
## Parameter
Variables can be accessed via string literals. They can be used for values with string keys if the parameter is a `map[string]interface{}` or `map[interface{}]interface{}` and for fields or methods if the parameter is a struct.
- [foo > 0](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Parameter)
### Bracket Selector
Map and array elements and Struct Field can be accessed via `[]`.
- [foo[0]](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Array)
- [foo["b" + "a" + "r"]](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--ExampleEvaluate_ComplexAccessor)
### Dot Selector
A nested variable with a name containing only letters and underscores can be accessed via a dot selector.
- [foo.bar > 0](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--NestedParameter)
### Custom Selector
Parameter names like `response-time` will be interpreted as `response` minus `time`. While gval doesn't support these parameter names directly, you can easily access them via a custom extension like [JSON Path](https://github.com/PaesslerAG/jsonpath):
- [$["response-time"]](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Jsonpath)
Jsonpath is also suitable for accessing array elements.
### Fields and Methods
If you have structs in your parameters, you can access their fields and methods in the usual way:
- [foo.Hello + foo.World()](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--FlatAccessor)
It also works if the parameter is a struct directly
[Hello + World()](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--Accessor)
or if the fields are nested
[foo.Hello + foo.World()](https://godoc.org/github.com/PaesslerAG/gval/#example-Evaluate--NestedAccessor)
This may be convenient but note that using accessors on strucs makes the expression about four times slower than just using a parameter (consult the benchmarks for more precise measurements on your system). If there are functions you want to use, it's faster (and probably cleaner) to define them as functions (see the Evaluate section). These approaches use no reflection, and are designed to be fast and clean.
## Default Language
The default language is in serveral sub languages like text, arithmetic or propositional logic defined. See [Godoc](https://godoc.org/github.com/PaesslerAG/gval/#Gval) for details. All sub languages are merged into gval.Full which contains the following elements:
- Modifiers: `+` `-` `/` `*` `&` `|` `^` `**` `%` `>>` `<<`
- Comparators: `>` `>=` `<` `<=` `==` `!=` `=~` `!~`
- Logical ops: `||` `&&`
- Numeric constants, as 64-bit floating point (`12345.678`)
- String constants (double quotes: `"foobar"`)
- Date function 'Date(x)', using any permutation of RFC3339, ISO8601, ruby date, or unix date
- Boolean constants: `true` `false`
- Parentheses to control order of evaluation `(` `)`
- Json Arrays : `[1, 2, "foo"]`
- Json Objects : `{"a":1, "b":2, "c":"foo"}`
- Prefixes: `!` `-` `~`
- Ternary conditional: `?` `:`
- Null coalescence: `??`
## Customize
Gval is completly customizable. Every constant, function or operator can be defined separately and existing expression languages can be reused:
- [foo.Hello + foo.World()](https://godoc.org/github.com/PaesslerAG/gval/#example-Language)
For details see [Godoc](https://godoc.org/github.com/PaesslerAG/gval).
### External gval Languages
A list of external libraries for gval. Feel free to add your own library.
- [gvalstrings](https://github.com/generikvault/gvalstrings) parse single quoted strings in gval.
- [jsonpath](https://github.com/PaesslerAG/jsonpath) full support for jsonpath in gval.
## Performance
The library is built with the intention of being quick but has not been aggressively profiled and optimized. For most applications, though, it is completely fine.
If performance is an issue, make sure to create your expression language with all functions, constants and operators only once. Evaluating an expression like gval.Evaluate("expression, const1, func1, func2, ...) creates a new gval.Language everytime it is called and slows execution.
The library comes with a bunch of benchmarks to measure the performance of parsing and evaluating expressions. You can run them with `go test -bench=.`.
For a very rough idea of performance, here are the results from a benchmark run on a Dell Latitude E7470 Win 10 i5-6300U.
``` text
BenchmarkGval/const_evaluation-4 500000000 3.57 ns/op
BenchmarkGval/const_parsing-4 1000000 1144 ns/op
BenchmarkGval/single_parameter_evaluation-4 10000000 165 ns/op
BenchmarkGval/single_parameter_parsing-4 1000000 1648 ns/op
BenchmarkGval/parameter_evaluation-4 5000000 352 ns/op
BenchmarkGval/parameter_parsing-4 500000 2773 ns/op
BenchmarkGval/common_evaluation-4 3000000 434 ns/op
BenchmarkGval/common_parsing-4 300000 4419 ns/op
BenchmarkGval/complex_evaluation-4 100000000 11.6 ns/op
BenchmarkGval/complex_parsing-4 100000 17936 ns/op
BenchmarkGval/literal_evaluation-4 300000000 3.84 ns/op
BenchmarkGval/literal_parsing-4 500000 2559 ns/op
BenchmarkGval/modifier_evaluation-4 500000000 3.54 ns/op
BenchmarkGval/modifier_parsing-4 500000 3755 ns/op
BenchmarkGval/regex_evaluation-4 50000 21347 ns/op
BenchmarkGval/regex_parsing-4 200000 6480 ns/op
BenchmarkGval/constant_regex_evaluation-4 1000000 1000 ns/op
BenchmarkGval/constant_regex_parsing-4 200000 9417 ns/op
BenchmarkGval/accessors_evaluation-4 3000000 417 ns/op
BenchmarkGval/accessors_parsing-4 1000000 1778 ns/op
BenchmarkGval/accessors_method_evaluation-4 1000000 1931 ns/op
BenchmarkGval/accessors_method_parsing-4 1000000 1729 ns/op
BenchmarkGval/accessors_method_parameter_evaluation-4 1000000 2162 ns/op
BenchmarkGval/accessors_method_parameter_parsing-4 500000 2618 ns/op
BenchmarkGval/nested_accessors_evaluation-4 2000000 681 ns/op
BenchmarkGval/nested_accessors_parsing-4 1000000 2115 ns/op
BenchmarkRandom-4 500000 3631 ns/op
ok
```
## API Breaks
Gval is designed with easy expandability in mind and API breaks will be avoided if possible. If API breaks are unavoidable they wil be explicitly stated via an increased major version number.
-------------------------------------
Credits to Reene French for the gophers.
package gval
import (
"context"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
)
// Evaluable evaluates given parameter
type Evaluable func(c context.Context, parameter interface{}) (interface{}, error)
//EvalInt evaluates given parameter to an int
func (e Evaluable) EvalInt(c context.Context, parameter interface{}) (int, error) {
v, err := e(c, parameter)
if err != nil {
return 0, err
}
f, ok := convertToFloat(v)
if !ok {
return 0, fmt.Errorf("expected number but got %v (%T)", v, v)
}
return int(f), nil
}
//EvalFloat64 evaluates given parameter to an int
func (e Evaluable) EvalFloat64(c context.Context, parameter interface{}) (float64, error) {
v, err := e(c, parameter)
if err != nil {
return 0, err
}
f, ok := convertToFloat(v)
if !ok {
return 0, fmt.Errorf("expected number but got %v (%T)", v, v)
}
return f, nil
}
//EvalBool evaluates given parameter to a bool
func (e Evaluable) EvalBool(c context.Context, parameter interface{}) (bool, error) {
v, err := e(c, parameter)
if err != nil {
return false, err
}
b, ok := convertToBool(v)
if !ok {
return false, fmt.Errorf("expected bool but got %v (%T)", v, v)
}
return b, nil
}
//EvalString evaluates given parameter to a string
func (e Evaluable) EvalString(c context.Context, parameter interface{}) (string, error) {
o, err := e(c, parameter)
if err != nil {
return "", err
}
return fmt.Sprintf("%v", o), nil
}
//Const Evaluable represents given constant
func (*Parser) Const(value interface{}) Evaluable {
return constant(value)
}
func constant(value interface{}) Evaluable {
return func(c context.Context, v interface{}) (interface{}, error) {
return value, nil
}
}
//Var Evaluable represents value at given path.
//It supports with default language VariableSelector:
// map[interface{}]interface{},
// map[string]interface{} and
// []interface{} and via reflect
// struct fields,
// struct methods,
// slices and
// map with int or string key.
func (p *Parser) Var(path ...Evaluable) Evaluable {
if p.Language.selector == nil {
return variable(path)
}
return p.Language.selector(path)
}
// Evaluables is a slice of Evaluable.
type Evaluables []Evaluable
// EvalStrings evaluates given parameter to a string slice
func (evs Evaluables) EvalStrings(c context.Context, parameter interface{}) ([]string, error) {
strs := make([]string, len(evs))
for i, p := range evs {
k, err := p.EvalString(c, parameter)
if err != nil {
return nil, err
}
strs[i] = k
}
return strs, nil
}
func variable(path Evaluables) Evaluable {
return func(c context.Context, v interface{}) (interface{}, error) {
keys, err := path.EvalStrings(c, v)
if err != nil {
return nil, err
}
for i, k := range keys {
switch o := v.(type) {
case map[interface{}]interface{}:
v = o[k]
continue
case map[string]interface{}:
v = o[k]
continue
case []interface{}:
if i, err := strconv.Atoi(k); err == nil && i >= 0 && len(o) > i {
v = o[i]
continue
}
default:
var ok bool
v, ok = reflectSelect(k, o)
if !ok {
return nil, fmt.Errorf("unknown parameter %s", strings.Join(keys[:i+1], "."))
}
}
}
return v, nil
}
}
func reflectSelect(key string, value interface{}) (selection interface{}, ok bool) {
vv := reflect.ValueOf(value)
vvElem := resolvePotentialPointer(vv)
switch vvElem.Kind() {
case reflect.Map:
mapKey, ok := reflectConvertTo(vv.Type().Key().Kind(), key)
if !ok {
return nil, false
}
vvElem = vv.MapIndex(reflect.ValueOf(mapKey))
vvElem = resolvePotentialPointer(vvElem)
if vvElem.IsValid() {
return vvElem.Interface(), true
}
case reflect.Slice:
if i, err := strconv.Atoi(key); err == nil && i >= 0 && vv.Len() > i {
vvElem = resolvePotentialPointer(vv.Index(i))
return vvElem.Interface(), true
}
case reflect.Struct:
field := vvElem.FieldByName(key)
if field.IsValid() {
return field.Interface(), true
}
method := vv.MethodByName(key)
if method.IsValid() {
return method.Interface(), true
}
}
return nil, false
}
func resolvePotentialPointer(value reflect.Value) reflect.Value {
if value.Kind() == reflect.Ptr {
return value.Elem()
}
return value
}
func reflectConvertTo(k reflect.Kind, value string) (interface{}, bool) {
switch k {
case reflect.String:
return value, true
case reflect.Int:
if i, err := strconv.Atoi(value); err == nil {
return i, true
}
}
return nil, false
}
func (*Parser) callFunc(fun function, args ...Evaluable) Evaluable {
return func(c context.Context, v interface{}) (ret interface{}, err error) {
a := make([]interface{}, len(args))
for i, arg := range args {
ai, err := arg(c, v)
if err != nil {
return nil, err
}
a[i] = ai
}
return fun(a...)
}
}
func (*Parser) callEvaluable(fullname string, fun Evaluable, args ...Evaluable) Evaluable {
return func(c context.Context, v interface{}) (ret interface{}, err error) {
f, err := fun(c, v)
if err != nil {
return nil, fmt.Errorf("could not call function: %v", err)
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("failed to execute function '%s': %s", fullname, r)
ret = nil
}
}()
ff := reflect.ValueOf(f)
if ff.Kind() != reflect.Func {
return nil, fmt.Errorf("could not call '%s' type %T", fullname, f)
}
a := make([]reflect.Value, len(args))
for i := range args {
arg, err := args[i](c, v)
if err != nil {
return nil, err
}
a[i] = reflect.ValueOf(arg)
}
rr := ff.Call(a)
r := make([]interface{}, len(rr))
for i, e := range rr {
r[i] = e.Interface()
}
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
if len(r) > 0 && ff.Type().Out(len(r)-1).Implements(errorInterface) {
if r[len(r)-1] != nil {
err = r[len(r)-1].(error)
}
r = r[0 : len(r)-1]
}
switch len(r) {
case 0:
return err, nil
case 1:
return r[0], err
default:
return r, err
}
}
}
//IsConst returns if the Evaluable is a Parser.Const() value
func (e Evaluable) IsConst() bool {
pc := reflect.ValueOf(constant(nil)).Pointer()
pe := reflect.ValueOf(e).Pointer()
return pc == pe
}
func regEx(a, b Evaluable) (Evaluable, error) {
if !b.IsConst() {
return func(c context.Context, o interface{}) (interface{}, error) {
a, err := a.EvalString(c, o)
if err != nil {
return nil, err
}
b, err := b.EvalString(c, o)
if err != nil {
return nil, err
}
matched, err := regexp.MatchString(b, a)
return matched, err
}, nil
}
s, err := b.EvalString(nil, nil)
if err != nil {
return nil, err
}
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
return func(c context.Context, v interface{}) (interface{}, error) {
s, err := a.EvalString(c, v)
if err != nil {
return nil, err
}
return regex.MatchString(s), nil
}, nil
}
func notRegEx(a, b Evaluable) (Evaluable, error) {
if !b.IsConst() {
return func(c context.Context, o interface{}) (interface{}, error) {
a, err := a.EvalString(c, o)
if err != nil {
return nil, err
}
b, err := b.EvalString(c, o)
if err != nil {
return nil, err
}
matched, err := regexp.MatchString(b, a)
return !matched, err
}, nil
}
s, err := b.EvalString(nil, nil)
if err != nil {
return nil, err
}
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
return func(c context.Context, v interface{}) (interface{}, error) {
s, err := a.EvalString(c, v)
if err != nil {
return nil, err
}
return !regex.MatchString(s), nil
}, nil
}
package gval
import (
"fmt"
"reflect"
)
type function func(arguments ...interface{}) (interface{}, error)
func toFunc(f interface{}) function {
if f, ok := f.(func(arguments ...interface{}) (interface{}, error)); ok {
return function(f)
}
return func(args ...interface{}) (interface{}, error) {
fun := reflect.ValueOf(f)
t := fun.Type()
in, err := createCallArguments(t, args)
if err != nil {
return nil, err
}
out := fun.Call(in)
r := make([]interface{}, len(out))
for i, e := range out {
r[i] = e.Interface()
}
err = nil
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
if len(r) > 0 && t.Out(len(r)-1).Implements(errorInterface) {
if r[len(r)-1] != nil {
err = r[len(r)-1].(error)
}
r = r[0 : len(r)-1]
}
switch len(r) {
case 0:
return nil, err
case 1:
return r[0], err
default:
return r, err
}
}
}
func createCallArguments(t reflect.Type, args []interface{}) ([]reflect.Value, error) {
variadic := t.IsVariadic()
numIn := t.NumIn()
if (!variadic && len(args) != numIn) || (variadic && len(args) < numIn-1) {
return nil, fmt.Errorf("invalid number of parameters")
}
in := make([]reflect.Value, len(args))
var inType reflect.Type
for i, arg := range args {
if !variadic || i < numIn-1 {
inType = t.In(i)
} else if i == numIn-1 {
inType = t.In(numIn - 1).Elem()
}
argVal := reflect.ValueOf(arg)
if arg == nil || !argVal.Type().AssignableTo(inType) {
return nil, fmt.Errorf("expected type %s for parameter %d but got %T",
inType.String(), i, arg)
}
in[i] = argVal
}
return in, nil
}
module github.com/PaesslerAG/gval
require github.com/PaesslerAG/jsonpath v0.1.0
github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI=
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
// Package gval provides a generic expression language.
// All functions, infix and prefix operators can be replaced by composing languages into a new one.
//
// The package contains concrete expression languages for common application in text, arithmetic, propositional logic and so on.
// They can be used as basis for a custom expression language or to evaluate expressions directly.
package gval
import (
"context"
"fmt"
"math"
"reflect"
"text/scanner"
"time"
)
//Evaluate given parameter with given expression in gval full language
func Evaluate(expression string, parameter interface{}, opts ...Language) (interface{}, error) {
l := full
if len(opts) > 0 {
l = NewLanguage(append([]Language{l}, opts...)...)
}
return l.Evaluate(expression, parameter)
}
// Full is the union of Arithmetic, Bitmask, Text, PropositionalLogic, and Json
// Operator in: a in b is true iff value a is an element of array b
// Operator ??: a ?? b returns a if a is not false or nil, otherwise n
// Operator ?: a ? b : c returns b if bool a is true, otherwise b
//
// Function Date: Date(a) parses string a. a must match RFC3339, ISO8601, ruby date, or unix date
func Full(extensions ...Language) Language {
if len(extensions) == 0 {
return full
}
return NewLanguage(append([]Language{full}, extensions...)...)
}
// Arithmetic contains base, plus(+), minus(-), divide(/), power(**), negative(-)
// and numerical order (<=,<,>,>=)
//
// Arithmetic operators expect float64 operands.
// Called with unfitting input, they try to convert the input to float64.
// They can parse strings and convert any type of int or float.
func Arithmetic() Language {
return arithmetic
}
// Bitmask contains base, bitwise and(&), bitwise or(|) and bitwise not(^).
//
// Bitmask operators expect float64 operands.
// Called with unfitting input they try to convert the input to float64.
// They can parse strings and convert any type of int or float.
func Bitmask() Language {
return bitmask
}
// Text contains base, lexical order on strings (<=,<,>,>=),
// regex match (=~) and regex not match (!~)
func Text() Language {
return text
}
// PropositionalLogic contains base, not(!), and (&&), or (||) and Base.
//
// Propositional operator expect bool operands.
// Called with unfitting input they try to convert the input to bool.
// Numbers other than 0 and the strings "TRUE" and "true" are interpreted as true.
// 0 and the strings "FALSE" and "false" are interpreted as false.
func PropositionalLogic() Language {
return propositionalLogic
}
// JSON contains json objects ({string:expression,...})
// and json arrays ([expression, ...])
func JSON() Language {
return ljson
}
// Base contains equal (==) and not equal (!=), perentheses and general support for variables, constants and functions
// It contains true, false, (floating point) number, string ("" or ``) and char ('') constants
func Base() Language {
return base
}
var full = NewLanguage(arithmetic, bitmask, text, propositionalLogic, ljson,
InfixOperator("in", inArray),
InfixShortCircuit("??", func(a interface{}) (interface{}, bool) {
return a, a != false && a != nil
}),
InfixOperator("??", func(a, b interface{}) (interface{}, error) {
if a == false || a == nil {
return b, nil
}
return a, nil
}),
PostfixOperator("?", parseIf),
Function("date", func(arguments ...interface{}) (interface{}, error) {
if len(arguments) != 1 {
return nil, fmt.Errorf("date() expects exactly one string argument")
}
s, ok := arguments[0].(string)
if !ok {
return nil, fmt.Errorf("date() expects exactly one string argument")
}
for _, format := range [...]string{
time.ANSIC,
time.UnixDate,
time.RubyDate,
time.Kitchen,
time.RFC3339,
time.RFC3339Nano,
"2006-01-02", // RFC 3339
"2006-01-02 15:04", // RFC 3339 with minutes
"2006-01-02 15:04:05", // RFC 3339 with seconds
"2006-01-02 15:04:05-07:00", // RFC 3339 with seconds and timezone
"2006-01-02T15Z0700", // ISO8601 with hour
"2006-01-02T15:04Z0700", // ISO8601 with minutes
"2006-01-02T15:04:05Z0700", // ISO8601 with seconds
"2006-01-02T15:04:05.999999999Z0700", // ISO8601 with nanoseconds
} {
ret, err := time.ParseInLocation(format, s, time.Local)
if err == nil {
return ret, nil
}
}
return nil, fmt.Errorf("date() could not parse %s", s)
}),
)
var ljson = NewLanguage(
PrefixExtension('[', parseJSONArray),
PrefixExtension('{', parseJSONObject),
)
var arithmetic = NewLanguage(
InfixNumberOperator("+", func(a, b float64) (interface{}, error) { return a + b, nil }),
InfixNumberOperator("-", func(a, b float64) (interface{}, error) { return a - b, nil }),
InfixNumberOperator("*", func(a, b float64) (interface{}, error) { return a * b, nil }),
InfixNumberOperator("/", func(a, b float64) (interface{}, error) { return a / b, nil }),
InfixNumberOperator("%", func(a, b float64) (interface{}, error) { return math.Mod(a, b), nil }),
InfixNumberOperator("**", func(a, b float64) (interface{}, error) { return math.Pow(a, b), nil }),
InfixNumberOperator(">", func(a, b float64) (interface{}, error) { return a > b, nil }),
InfixNumberOperator(">=", func(a, b float64) (interface{}, error) { return a >= b, nil }),
InfixNumberOperator("<", func(a, b float64) (interface{}, error) { return a < b, nil }),
InfixNumberOperator("<=", func(a, b float64) (interface{}, error) { return a <= b, nil }),
InfixNumberOperator("==", func(a, b float64) (interface{}, error) { return a == b, nil }),
InfixNumberOperator("!=", func(a, b float64) (interface{}, error) { return a != b, nil }),
base,
)
var bitmask = NewLanguage(
InfixNumberOperator("^", func(a, b float64) (interface{}, error) { return float64(int64(a) ^ int64(b)), nil }),
InfixNumberOperator("&", func(a, b float64) (interface{}, error) { return float64(int64(a) & int64(b)), nil }),
InfixNumberOperator("|", func(a, b float64) (interface{}, error) { return float64(int64(a) | int64(b)), nil }),
InfixNumberOperator("<<", func(a, b float64) (interface{}, error) { return float64(int64(a) << uint64(b)), nil }),
InfixNumberOperator(">>", func(a, b float64) (interface{}, error) { return float64(int64(a) >> uint64(b)), nil }),
PrefixOperator("~", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToFloat(v)
if !ok {
return nil, fmt.Errorf("unexpected %T expected number", v)
}
return float64(^int64(i)), nil
}),
)
var text = NewLanguage(
InfixTextOperator("+", func(a, b string) (interface{}, error) { return fmt.Sprintf("%v%v", a, b), nil }),
InfixTextOperator("<", func(a, b string) (interface{}, error) { return a < b, nil }),
InfixTextOperator("<=", func(a, b string) (interface{}, error) { return a <= b, nil }),
InfixTextOperator(">", func(a, b string) (interface{}, error) { return a > b, nil }),
InfixTextOperator(">=", func(a, b string) (interface{}, error) { return a >= b, nil }),
InfixEvalOperator("=~", regEx),
InfixEvalOperator("!~", notRegEx),
base,
)
var propositionalLogic = NewLanguage(
PrefixOperator("!", func(c context.Context, v interface{}) (interface{}, error) {
b, ok := convertToBool(v)
if !ok {
return nil, fmt.Errorf("unexpected %T expected bool", v)
}
return !b, nil
}),
InfixShortCircuit("&&", func(a interface{}) (interface{}, bool) { return false, a == false }),
InfixBoolOperator("&&", func(a, b bool) (interface{}, error) { return a && b, nil }),
InfixShortCircuit("||", func(a interface{}) (interface{}, bool) { return true, a == true }),
InfixBoolOperator("||", func(a, b bool) (interface{}, error) { return a || b, nil }),
InfixBoolOperator("==", func(a, b bool) (interface{}, error) { return a == b, nil }),
InfixBoolOperator("!=", func(a, b bool) (interface{}, error) { return a != b, nil }),
base,
)
var base = NewLanguage(
PrefixExtension(scanner.Int, parseNumber),
PrefixExtension(scanner.Float, parseNumber),
PrefixOperator("-", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToFloat(v)
if !ok {
return nil, fmt.Errorf("unexpected %v(%T) expected number", v, v)
}
return -i, nil
}),
PrefixExtension(scanner.String, parseString),
PrefixExtension(scanner.Char, parseString),
PrefixExtension(scanner.RawString, parseString),
Constant("true", true),
Constant("false", false),
InfixOperator("==", func(a, b interface{}) (interface{}, error) { return reflect.DeepEqual(a, b), nil }),
InfixOperator("!=", func(a, b interface{}) (interface{}, error) { return !reflect.DeepEqual(a, b), nil }),
PrefixExtension('(', parseParentheses),
Precedence("??", 0),
Precedence("||", 20),
Precedence("&&", 21),
Precedence("==", 40),
Precedence("!=", 40),
Precedence(">", 40),
Precedence(">=", 40),
Precedence("<", 40),
Precedence("<=", 40),
Precedence("=~", 40),
Precedence("!~", 40),
Precedence("in", 40),
Precedence("^", 60),
Precedence("&", 60),
Precedence("|", 60),
Precedence("<<", 90),
Precedence(">>", 90),
Precedence("+", 120),
Precedence("-", 120),
Precedence("*", 150),
Precedence("/", 150),
Precedence("%", 150),
Precedence("**", 200),
PrefixMetaPrefix(scanner.Ident, parseIdent),
)
package gval
import (
"context"
"fmt"
"text/scanner"
"unicode"
)
// Language is an expression language
type Language struct {
prefixes map[interface{}]prefix
operators map[string]operator
operatorSymbols map[rune]struct{}
selector func(Evaluables) Evaluable
}
// NewLanguage returns the union of given Languages as new Language.
func NewLanguage(bases ...Language) Language {
l := newLanguage()
for _, base := range bases {
for i, e := range base.prefixes {
l.prefixes[i] = e
}
for i, e := range base.operators {
l.operators[i] = e.merge(l.operators[i])
l.operators[i].initiate(i)
}
for i := range base.operatorSymbols {
l.operatorSymbols[i] = struct{}{}
}
if base.selector != nil {
l.selector = base.selector
}
}
return l
}
func newLanguage() Language {
return Language{
prefixes: map[interface{}]prefix{},
operators: map[string]operator{},
operatorSymbols: map[rune]struct{}{},
}
}
// NewEvaluable returns an Evaluable for given expression in the specified language
func (l Language) NewEvaluable(expression string) (Evaluable, error) {
p := newParser(expression, l)
eval, err := p.ParseExpression(context.Background())
if err == nil && p.isCamouflaged() && p.lastScan != scanner.EOF {
err = p.camouflage
}
if err != nil {
pos := p.scanner.Pos()
return nil, fmt.Errorf("parsing error: %s - %d:%d %s", p.scanner.Position, pos.Line, pos.Column, err)
}
return eval, nil
}
// Evaluate given parameter with given expression
func (l Language) Evaluate(expression string, parameter interface{}) (interface{}, error) {
eval, err := l.NewEvaluable(expression)
if err != nil {
return nil, err
}
v, err := eval(context.Background(), parameter)
if err != nil {
return nil, fmt.Errorf("can not evaluate %s: %v", expression, err)
}
return v, nil
}
// Function returns a Language with given function.
// Function has no conversion for input types.
//
// If the function returns an error it must be the last return parameter.
//
// If the function has (without the error) more then one return parameter,
// it returns them as []interface{}.
func Function(name string, function interface{}) Language {
l := newLanguage()
l.prefixes[name] = func(c context.Context, p *Parser) (eval Evaluable, err error) {
args := []Evaluable{}
scan := p.Scan()
switch scan {
case '(':
args, err = p.parseArguments(c)
if err != nil {
return nil, err
}
default:
p.Camouflage("function call", '(')
}
return p.callFunc(toFunc(function), args...), nil
}
return l
}
// Constant returns a Language with given constant
func Constant(name string, value interface{}) Language {
l := newLanguage()
l.prefixes[l.makePrefixKey(name)] = func(c context.Context, p *Parser) (eval Evaluable, err error) {
return p.Const(value), nil
}
return l
}
// PrefixExtension extends a Language
func PrefixExtension(r rune, ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.prefixes[r] = ext
return l
}
// PrefixMetaPrefix chooses a Prefix to be executed
func PrefixMetaPrefix(r rune, ext func(context.Context, *Parser) (call string, alternative func() (Evaluable, error), err error)) Language {
l := newLanguage()
l.prefixes[r] = func(c context.Context, p *Parser) (Evaluable, error) {
call, alternative, err := ext(c, p)
if err != nil {
return nil, err
}
if prefix, ok := p.prefixes[l.makePrefixKey(call)]; ok {
return prefix(c, p)
}
return alternative()
}
return l
}
//PrefixOperator returns a Language with given prefix
func PrefixOperator(name string, e Evaluable) Language {
l := newLanguage()
l.prefixes[l.makePrefixKey(name)] = func(c context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseNextExpression(c)
if err != nil {
return nil, err
}
prefix := func(c context.Context, v interface{}) (interface{}, error) {
a, err := eval(c, v)
if err != nil {
return nil, err
}
return e(c, a)
}
if eval.IsConst() {
v, err := prefix(context.Background(), nil)
if err != nil {
return nil, err
}
prefix = p.Const(v)
}
return prefix, nil
}
return l
}
// PostfixOperator extends a Language.
func PostfixOperator(name string, ext func(context.Context, *Parser, Evaluable) (Evaluable, error)) Language {
l := newLanguage()
l.operators[l.makeInfixKey(name)] = postfix{
f: func(c context.Context, p *Parser, eval Evaluable, pre operatorPrecedence) (Evaluable, error) {
return ext(c, p, eval)
},
}
return l
}
// InfixOperator for two arbitrary values.
func InfixOperator(name string, f func(a, b interface{}) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{arbitrary: f})
}
// InfixShortCircuit operator is called after the left operand is evaluated.
func InfixShortCircuit(name string, f func(a interface{}) (interface{}, bool)) Language {
return newLanguageOperator(name, &infix{shortCircuit: f})
}
// InfixTextOperator for two text values.
func InfixTextOperator(name string, f func(a, b string) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{text: f})
}
// InfixNumberOperator for two number values.
func InfixNumberOperator(name string, f func(a, b float64) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{number: f})
}
// InfixBoolOperator for two bool values.
func InfixBoolOperator(name string, f func(a, b bool) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{boolean: f})
}
// Precedence of operator. The Operator with higher operatorPrecedence is evaluated first.
func Precedence(name string, operatorPrecendence uint8) Language {
return newLanguageOperator(name, operatorPrecedence(operatorPrecendence))
}
// InfixEvalOperator operates on the raw operands.
// Therefore it cannot be combined with operators for other operand types.
func InfixEvalOperator(name string, f func(a, b Evaluable) (Evaluable, error)) Language {
return newLanguageOperator(name, directInfix{infixBuilder: f})
}
func newLanguageOperator(name string, op operator) Language {
op.initiate(name)
l := newLanguage()
l.operators[l.makeInfixKey(name)] = op
return l
}
func (l *Language) makePrefixKey(key string) interface{} {
runes := []rune(key)
if len(runes) == 1 && !unicode.IsLetter(runes[0]) {
return runes[0]
}
return key
}
func (l *Language) makeInfixKey(key string) string {
runes := []rune(key)
for _, r := range runes {
l.operatorSymbols[r] = struct{}{}
}
return key
}
// VariableSelector returns a Language which uses given variable selector.
// It must be combined with a Language that uses the vatiable selector. E.g. gval.Base().
func VariableSelector(selector func(path Evaluables) Evaluable) Language {
l := newLanguage()
l.selector = selector
return l
}
package gval
import (
"context"
"fmt"
"reflect"
"strconv"
)
type stage struct {
Evaluable
infixBuilder
operatorPrecedence
}
type stageStack []stage //operatorPrecedence in stacktStage is continuously, monotone ascending
func (s *stageStack) push(b stage) error {
for len(*s) > 0 && s.peek().operatorPrecedence >= b.operatorPrecedence {
a := s.pop()
eval, err := a.infixBuilder(a.Evaluable, b.Evaluable)
if err != nil {
return err
}
if a.IsConst() && b.IsConst() {
v, err := eval(nil, nil)
if err != nil {
return err
}
b.Evaluable = constant(v)
continue
}
b.Evaluable = eval
}
*s = append(*s, b)
return nil
}
func (s *stageStack) peek() stage {
return (*s)[len(*s)-1]
}
func (s *stageStack) pop() stage {
a := s.peek()
(*s) = (*s)[:len(*s)-1]
return a
}
type infixBuilder func(a, b Evaluable) (Evaluable, error)
func (l Language) isSymbolOperation(r rune) bool {
_, in := l.operatorSymbols[r]
return in
}
func (op *infix) initiate(name string) {
f := func(a, b interface{}) (interface{}, error) {
return nil, fmt.Errorf("invalid operation (%T) %s (%T)", a, name, b)
}
if op.arbitrary != nil {
f = op.arbitrary
}
for _, typeConvertion := range []bool{true, false} {
if op.text != nil && (!typeConvertion || op.arbitrary == nil) {
f = getStringOpFunc(op.text, f, typeConvertion)
}
if op.boolean != nil {
f = getBoolOpFunc(op.boolean, f, typeConvertion)
}
if op.number != nil {
f = getFloatOpFunc(op.number, f, typeConvertion)
}
}
if op.shortCircuit == nil {
op.builder = func(a, b Evaluable) (Evaluable, error) {
return func(c context.Context, x interface{}) (interface{}, error) {
a, err := a(c, x)
if err != nil {
return nil, err
}
b, err := b(c, x)
if err != nil {
return nil, err
}
return f(a, b)
}, nil
}
return
}
shortF := op.shortCircuit
op.builder = func(a, b Evaluable) (Evaluable, error) {
return func(c context.Context, x interface{}) (interface{}, error) {
a, err := a(c, x)
if err != nil {
return nil, err
}
if r, ok := shortF(a); ok {
return r, nil
}
b, err := b(c, x)
if err != nil {
return nil, err
}
return f(a, b)
}, nil
}
return
}
type opFunc func(a, b interface{}) (interface{}, error)
func getStringOpFunc(s func(a, b string) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
if a != nil && b != nil {
return s(fmt.Sprintf("%v", a), fmt.Sprintf("%v", b))
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
s1, k := a.(string)
s2, l := b.(string)
if k && l {
return s(s1, s2)
}
return f(a, b)
}
}
func convertToBool(o interface{}) (bool, bool) {
if b, ok := o.(bool); ok {
return b, true
}
v := reflect.ValueOf(o)
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
o = v.Interface()
}
if o == false || o == nil || o == "false" || o == "FALSE" {
return false, true
}
if o == true || o == "true" || o == "TRUE" {
return true, true
}
if f, ok := convertToFloat(o); ok {
return f != 0., true
}
return false, false
}
func getBoolOpFunc(o func(a, b bool) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToBool(a)
y, l := convertToBool(b)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(bool)
y, l := b.(bool)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
func convertToFloat(o interface{}) (float64, bool) {
if i, ok := o.(float64); ok {
return i, true
}
v := reflect.ValueOf(o)
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
o = v.Interface()
}
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(v.Int()), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return float64(v.Uint()), true
case reflect.Float32, reflect.Float64:
return v.Float(), true
}
if s, ok := o.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return f, true
}
}
return 0, false
}
func getFloatOpFunc(o func(a, b float64) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToFloat(a)
y, l := convertToFloat(b)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(float64)
y, l := b.(float64)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
type operator interface {
merge(operator) operator
precedence() operatorPrecedence
initiate(name string)
}
type operatorPrecedence uint8
func (pre operatorPrecedence) merge(op operator) operator {
if op, ok := op.(operatorPrecedence); ok {
if op > pre {
return op
}
return pre
}
if op == nil {
return pre
}
return op.merge(pre)
}
func (pre operatorPrecedence) precedence() operatorPrecedence {
return pre
}
func (pre operatorPrecedence) initiate(name string) {}
type infix struct {
operatorPrecedence
number func(a, b float64) (interface{}, error)
boolean func(a, b bool) (interface{}, error)
text func(a, b string) (interface{}, error)
arbitrary func(a, b interface{}) (interface{}, error)
shortCircuit func(a interface{}) (interface{}, bool)
builder infixBuilder
}
func (op infix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case *infix:
if op2.number != nil {
op.number = op2.number
}
if op2.boolean != nil {
op.boolean = op2.boolean
}
if op2.text != nil {
op.text = op2.text
}
if op2.arbitrary != nil {
op.arbitrary = op2.arbitrary
}
if op2.shortCircuit != nil {
op.shortCircuit = op2.shortCircuit
}
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return &op
}
type directInfix struct {
operatorPrecedence
infixBuilder
}
func (op directInfix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case operatorPrecedence:
op.operatorPrecedence = op2
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return op
}
type prefix func(context.Context, *Parser) (Evaluable, error)
type postfix struct {
operatorPrecedence
f func(context.Context, *Parser, Evaluable, operatorPrecedence) (Evaluable, error)
}
func (op postfix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case postfix:
if op2.f != nil {
op.f = op2.f
}
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return op
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment