From 0b4a5903038fd9e9f00c353dc6b5701681a76ed9 Mon Sep 17 00:00:00 2001 From: seborama Date: Fri, 10 May 2024 22:43:43 +0100 Subject: [PATCH] added ObjectGetMethod in prep for object function support --- .golangci.yml | 4 +- README.md | 46 +++++++++++- function.go | 13 +--- gal_test.go | 73 +++++++++++++++++- go.mod | 2 + go.sum | 4 + object.go | 171 ++++++++++++++++++++++++++++++++----------- object_test.go | 117 +++++++++++++++++++++++++++-- tree.go | 29 ++++++-- tree_builder.go | 3 +- tree_builder_test.go | 2 - value.go | 36 ++------- 12 files changed, 402 insertions(+), 98 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 961a584..63562c3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -207,7 +207,7 @@ linters-settings: gocognit: # Minimal code complexity to report # Default: 30 (but we recommend 10-20) - min-complexity: 15 + min-complexity: 20 gocritic: # Which checks should be enabled; can't be combined with 'disabled-checks'. @@ -300,7 +300,7 @@ linters-settings: gocyclo: # Minimal code complexity to report. # Default: 30 (but we recommend 10-20) - min-complexity: 15 + min-complexity: 20 godot: # Comments to be checked: `declarations`, `toplevel`, or `all`. diff --git a/README.md b/README.md index 2743c19..d22c0f9 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Escapes are supported: ## Bools -In addition to boolean expressions, sepcial contants `True` and `False` may be used. +In addition to boolean expressions, special contants `True` and `False` may be used. Do not double-quote them, or they will become plain strings! @@ -148,7 +148,7 @@ This is container `Value`. It can contain zero or any number of `Value`'s. Curre * Go classifies bit shift operators with the higher `*`. * `&&` is synonymous of `And`. * `||` is synonymous of `Or`. - * Worded operators such as `And` and `Or` are **case-sensitive** and must be followed by a blank character. `True Or (False)` is a Bool expression with the `Or` operator but `True Or(False)` is an invalid expression attempting to call a user-defined function called `Or()`. + * Worded operators such as `And` and `Or` are **case-sensitive** and must be followed by a blank character. `True Or (False)` is a Bool expression with the `Or` operator but `True Or(False)` is an expression attempting to call a user-defined function called `Or()`. * Types: String, Number, Bool, MultiValue * Associativity with parentheses: `(` and `)` * Functions: @@ -158,6 +158,8 @@ This is container `Value`. It can contain zero or any number of `Value`'s. Curre ## Functions +(See also Objects) + A function is defined as a Go type: `type FunctionalValue func(...Value) Value` Function names are case-insensitive. @@ -172,12 +174,52 @@ This allows parsing the expression once with `Parse` and run `Tree`.`Eval` multi ## Variables +(See also Objects) + Variable names are case-sensitive. Values are passed as a `map[string]Value` using `WithVariables` when calling `Eval` from `Tree`. This allows parsing the expression once with `Parse` and run `Tree`.`Eval` multiple times with different variable values. +## Objects + +Objects are Go `struct`'s which **properties** act as **gal variables** and **methods** as **gal functions**. + +Object definitions are passed as a `map[string]Object` using `WithObjects` when calling `Eval` from `Tree`. + +This allows parsing the expression once with `Parse` and run `Tree`.`Eval` multiple times with different instances of an object. + +Object methods generally follow the same rules as **gal Functions**: +- methods can optionally accept one or more arguments +- methods must return a single value (which can be a `MultiValue` to emulate multiple return values) +- arguments and return value are preferred to be of `gal.Value` type. + However, **gal** will attempt to convert Go types to `gal.Value` types on best endeavour: + - A method signature of `MyMethod(arg1 int64) bool` will translate the supplied `gal.Value`'s and attempt to map them to `int64` and `bool` using `gal.Numberer` and `gal.Booler`. + - Type conversion may lead to a panic when the type cannot be interpreted. + +Example: + +`type Car struct` has several properties and methods - one of which is `func (c *Car) CurrentSpeed() gal.Value`. + +```go + expr := `aCar.MaxSpeed - aCar.CurrentSpeed()` + parsedExpr := gal.Parse(expr) + + got := parsedExpr.Eval( + gal.WithObjects(map[string]gal.Object{ + "aCar": Car{ + Make: "Lotus Esprit", + Mileage: gal.NewNumberFromInt(2000), + Speed: 100, + MaxSpeed: 250, + }, + }), + ) + // result: 150 == 250 - 100 + +``` + ## High level design Expressions are parsed in two stages: diff --git a/function.go b/function.go index b20a779..27c7478 100644 --- a/function.go +++ b/function.go @@ -71,6 +71,10 @@ var builtInFunctions = map[string]FunctionalValue{ // It returns `nil` when no built-in function exists by the specified name. // This signals the Evaluator to attempt to find a user defined function. func BuiltInFunction(name string) FunctionalValue { + if builtInFunctions == nil { + return nil + } + // note: for now function names are arbitrarily case-insensitive lowerName := strings.ToLower(name) @@ -82,15 +86,6 @@ func BuiltInFunction(name string) FunctionalValue { return nil } -// UserDefinedFunction is a helper function that returns the definition of the -// provided function name from the supplied userFunctions. -func UserDefinedFunction(name string, userFunctions Functions) FunctionalValue { - // note: for now function names are arbitrarily case-insensitive - lowerName := strings.ToLower(name) - - return userFunctions.Function(lowerName) -} - // Pi returns the Value of math.Pi. func Pi(args ...Value) Value { if len(args) != 0 { diff --git a/gal_test.go b/gal_test.go index 60aa602..0c6059f 100644 --- a/gal_test.go +++ b/gal_test.go @@ -359,7 +359,7 @@ func TestFunctionsAndStringsWithSpaces(t *testing.T) { assert.Equal(t, `"ab cdef gh"`, got.String()) } -func TestObjects(t *testing.T) { +func TestObjects_Properties(t *testing.T) { expr := `aCar.MaxSpeed - aCar.Speed` parsedExpr := gal.Parse(expr) @@ -375,3 +375,74 @@ func TestObjects(t *testing.T) { ) assert.Equal(t, "150", got.String()) } + +func TestObjects_Methods(t *testing.T) { + expr := `aCar.MaxSpeed - aCar.CurrentSpeed()` + parsedExpr := gal.Parse(expr) + + got := parsedExpr.Eval( + gal.WithObjects(map[string]gal.Object{ + "aCar": &Car{ + Make: "Lotus Esprit", + Mileage: gal.NewNumberFromInt(2000), + Speed: 100, + MaxSpeed: 250, + }, + }), + ) + assert.Equal(t, "150", got.String()) +} + +func TestObjects_Methods_WithSubTree(t *testing.T) { + expr := `2 * (aCar.MaxSpeed - aCar.CurrentSpeed())` + parsedExpr := gal.Parse(expr) + + got := parsedExpr.Eval( + gal.WithObjects(map[string]gal.Object{ + "aCar": &Car{ + Make: "Lotus Esprit", + Mileage: gal.NewNumberFromInt(2000), + Speed: 100, + MaxSpeed: 250, + }, + }), + ) + assert.Equal(t, "300", got.String()) +} + +func TestObjects_Methods_WithArgsSubTree(t *testing.T) { + expr := `2 * (aCar.MaxSpeed - aCar.TillMaxSpeed(aCar.CurrentSpeed()))` + parsedExpr := gal.Parse(expr) + + got := parsedExpr.Eval( + gal.WithObjects(map[string]gal.Object{ + "aCar": &Car{ + Make: "Lotus Esprit", + Mileage: gal.NewNumberFromInt(2000), + Speed: 100, + MaxSpeed: 250, + }, + }), + ) + assert.Equal(t, "200", got.String()) +} + +func TestObjects_MethodReceiver(t *testing.T) { + expr := `aCar.MaxSpeed - aCar.CurrentSpeed()` + parsedExpr := gal.Parse(expr) + + got := parsedExpr.Eval( + gal.WithObjects(map[string]gal.Object{ + "aCar": Car{ + Make: "Lotus Esprit", + Mileage: gal.NewNumberFromInt(2000), + Speed: 100, + MaxSpeed: 250, + }, + }), + ) + // Note: in this test, WithObjects is called with a `Car`, not a `*Car`. + // However, Car.CurrentSpeed has a *Car receiver, hence from a Go perspective, the method + // exists on *Car but it does NOT exist on Car! + assert.Equal(t, "undefined: type 'gal_test.Car' does not have a method 'CurrentSpeed' (check if it has a pointer receiver)", got.String()) +} diff --git a/go.mod b/go.mod index 4b46a2c..221e560 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,13 @@ require github.com/shopspring/decimal v1.4.0 require ( github.com/google/go-cmp v0.6.0 github.com/pkg/errors v0.9.1 + github.com/samber/lo v1.39.0 github.com/stretchr/testify v1.7.1 ) require ( github.com/davecgh/go-spew v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6d574b9..f309c6d 100644 --- a/go.sum +++ b/go.sum @@ -6,11 +6,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/object.go b/object.go index 025d437..00b2bc5 100644 --- a/object.go +++ b/object.go @@ -3,10 +3,14 @@ package gal import ( "fmt" "reflect" + "strconv" + + "github.com/pkg/errors" + "github.com/samber/lo" ) // TODO: implement support for nested structs? -func ObjectGetProperty(obj Object, name string) (Value, bool) { +func ObjectGetProperty(obj Object, name string) (Value, bool) { //nolint: gocognit, gocyclo, cyclop if obj == nil { return NewUndefinedWithReasonf("object is nil"), false } @@ -32,53 +36,138 @@ func ObjectGetProperty(obj Object, name string) (Value, bool) { } } + // TODO: we only support `struct` for now. Perhaps simple types (int, float, etc) are a worthwhile enhancement? if t.Kind() != reflect.Struct { - // TODO: we only support `struct` for now. Perhaps simple types (int, float, etc) are a worthwhile enhancement? return NewUndefinedWithReasonf("object is '%s' but only 'struct' and '*struct' are currently supported", t.Kind()), false } - typeName := t.Name() - - // Iterate over the fields of the struct - for i := 0; i < v.NumField(); i++ { - vName := v.Type().Field(i).Name - vType := v.Type().Field(i).Type.Name() - vValueI := v.Field(i).Interface() - if vName == name { - if vValue, ok := vValueI.(Value); ok { - return vValue, true - } else { - switch vValueIType := vValueI.(type) { - case int: - return NewNumberFromInt(int64(vValueIType)), true - case int32: - return NewNumberFromInt(int64(vValueIType)), true - case int64: - return NewNumberFromInt(vValueIType), true - case uint: - return NewNumberFromInt(int64(vValueIType)), true - case uint32: - return NewNumberFromInt(int64(vValueIType)), true - case uint64: - n, err := NewNumberFromString(fmt.Sprintf("%d", vValueIType)) - if err != nil { - return NewUndefinedWithReasonf("value '%d' of property '%s:%s' cannot be converted to a Number", vValueIType, typeName, name), false - } - return n, true - case float32: // this will commonly suffer from floating point issues - return NewNumberFromFloat(float64(vValueIType)), true - case float64: - return NewNumberFromFloat(vValueIType), true - case string: - return NewString(vValueIType), true - case bool: - return NewBool(vValueIType), true - default: - return NewUndefinedWithReasonf("property '%s:%s' is of type '%s', not a gal.Value", typeName, name, vType), false + vValue := v.FieldByName(name) + if !vValue.IsValid() { + return NewUndefinedWithReasonf("property '%T:%s' does not exist on object", obj, name), false + } + + galValue, err := goAnyToGalType(vValue.Interface()) + if err != nil { + return NewUndefinedWithReasonf("object::%T:%s - %s", obj, name, err.Error()), false + } + return galValue, true +} + +func ObjectGetMethod(obj Object, name string) (FunctionalValue, bool) { //nolint: cyclop + if obj == nil { + return func(...Value) Value { + return NewUndefinedWithReasonf("object is nil for type '%T'", obj) + }, false + } + + value := reflect.ValueOf(obj) + if value.IsZero() || !value.IsValid() { + return func(...Value) Value { + return NewUndefinedWithReasonf("object is nil for type '%T' or does not have a method '%s'", obj, name) + }, false + } + + methodValue := value.MethodByName(name) + if !methodValue.IsValid() { + return func(...Value) Value { + return NewUndefinedWithReasonf("type '%T' does not have a method '%s' (check if it has a pointer receiver)", obj, name) + }, false + } + + methodType := methodValue.Type() + numParams := methodType.NumIn() + + var fn FunctionalValue = func(args ...Value) (retValue Value) { + if len(args) != numParams { + return NewUndefinedWithReasonf("invalid function call - object::%T:%s - wants %d args, received %d instead", obj, name, numParams, len(args)) + } + + callArgs := lo.Map(args, func(item Value, index int) reflect.Value { + paramType := methodType.In(index) + + switch paramType.Kind() { + // TODO: continue with more "case"'s + case reflect.Int: + return reflect.ValueOf(int(item.(Numberer).Number().Int64())) + case reflect.Int32: + return reflect.ValueOf(int32(item.(Numberer).Number().Int64())) + case reflect.Int64: + return reflect.ValueOf(item.(Numberer).Number().Int64()) + case reflect.Uint: + return reflect.ValueOf(uint(item.(Numberer).Number().Int64())) + case reflect.Uint32: + return reflect.ValueOf(uint32(item.(Numberer).Number().Int64())) + case reflect.Uint64: + n, err := strconv.ParseUint(item.(Stringer).AsString().RawString(), 10, 64) + if err != nil { + panic(err) // no other safe way } + return reflect.ValueOf(n) + case reflect.Float32: + return reflect.ValueOf(float32(item.(Numberer).Number().Float64())) + case reflect.Float64: + return reflect.ValueOf(item.(Numberer).Number().Float64()) + case reflect.String: + return reflect.ValueOf(item.(Stringer).AsString().RawString()) + case reflect.Bool: + return reflect.ValueOf(item.(Booler).Bool().value) + default: + return reflect.ValueOf(item) } + }) + + defer func() { + if r := recover(); r != nil { + retValue = NewUndefinedWithReasonf("invalid function call - object::%T:%s - invalid argument type passed to function - %v", obj, name, r) + return + } + }() + + out := methodValue.Call(callArgs) + if len(out) != 1 { + return NewUndefinedWithReasonf("invalid function call - object::%T:%s - must return 1 value, returned %d instead", obj, name, len(out)) } + + retValue, err := goAnyToGalType(out[0].Interface()) + if err != nil { + return NewUndefinedWithReasonf("object::%T:%s - %s", obj, name, err.Error()) + } + return } - return NewUndefinedWithReasonf("property '%s:%s' does not exist on object", typeName, name), false + return fn, true +} + +// attempt to convert a Go 'any' type to an equivalent gal.Value +func goAnyToGalType(value any) (Value, error) { + switch typedValue := value.(type) { + case Value: + return typedValue, nil + case int: + return NewNumberFromInt(int64(typedValue)), nil + case int32: + return NewNumberFromInt(int64(typedValue)), nil + case int64: + return NewNumberFromInt(typedValue), nil + case uint: + return NewNumberFromInt(int64(typedValue)), nil + case uint32: + return NewNumberFromInt(int64(typedValue)), nil + case uint64: + n, err := NewNumberFromString(fmt.Sprintf("%d", typedValue)) + if err != nil { + return nil, errors.Errorf("value uint64(%d) cannot be converted to a Number", typedValue) + } + return n, nil + case float32: // this will commonly suffer from floating point issues + return NewNumberFromFloat(float64(typedValue)), nil + case float64: + return NewNumberFromFloat(typedValue), nil + case string: + return NewString(typedValue), nil + case bool: + return NewBool(typedValue), nil + default: + return nil, errors.Errorf("type '%T' cannot be mapped to gal.Value", typedValue) + } } diff --git a/object_test.go b/object_test.go index 119c9da..3d6602f 100644 --- a/object_test.go +++ b/object_test.go @@ -4,16 +4,61 @@ import ( "fmt" "testing" - "github.com/seborama/gal/v8" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/seborama/gal/v8" ) type Car struct { - Make string - Mileage gal.Number - Speed float32 - MaxSpeed int64 + Make string + Mileage gal.Number + Speed float32 + MaxSpeed int64 + ComplexProperty complex128 +} + +func (c *Car) Ignite() gal.Value { + return gal.True +} + +func (c Car) Shutdown() gal.Value { //nolint: gocritic + return gal.True +} + +func (c *Car) CurrentSpeed() gal.Value { + return gal.NewNumberFromFloat(float64(c.Speed)) +} + +func (c *Car) CurrentSpeed2() float32 { + return c.Speed +} + +func (c *Car) SetSpeed(speed gal.Number) { + c.Speed = float32(speed.Float64()) +} + +func (c *Car) SetSpeed2(speed gal.Number) gal.Bool { + c.Speed = float32(speed.Float64()) + return gal.True +} + +func (c *Car) SetSpeed3(speed float32) gal.Bool { + c.Speed = speed + return gal.True +} + +func (c *Car) TillMaxSpeed(speed gal.Number) gal.Number { + return gal.NewNumberFromInt(c.MaxSpeed).Add(speed.Neg()).(gal.Numberer).Number() +} + +type fancyType struct { + Speed float32 +} + +func (c *Car) SetSpeed4(speed fancyType) gal.Bool { + c.Speed = speed.Speed + return gal.True } func (c *Car) String() string { @@ -51,7 +96,69 @@ func TestObjectGetProperty(t *testing.T) { require.False(t, ok) assert.Equal(t, "undefined: object is 'complex128' but only 'struct' and '*struct' are currently supported", val.String()) + val, ok = gal.ObjectGetProperty(myCar, "ComplexProperty") + require.False(t, ok) + assert.Equal(t, "undefined: object::*gal_test.Car:ComplexProperty - type 'complex128' cannot be mapped to gal.Value", val.String()) + val, ok = gal.ObjectGetProperty(myCar, "MaxSpeed") require.True(t, ok) assert.Equal(t, gal.NewNumberFromInt(250), val) } + +func TestObjectGetMethod(t *testing.T) { + myCar := &Car{ + Make: "Lotus", + Mileage: gal.NewNumberFromInt(100), + Speed: 50.345, + MaxSpeed: 250, + } + + var nilCar *Car + + val, ok := gal.ObjectGetMethod(nilCar, "Ignite") + require.False(t, ok) + assert.Equal(t, "undefined: object is nil for type '*gal_test.Car' or does not have a method 'Ignite'", val().String()) + + val, ok = gal.ObjectGetMethod(myCar, "DoesNotExist!") + require.False(t, ok) + assert.Equal(t, "undefined: type '*gal_test.Car' does not have a method 'DoesNotExist!' (check if it has a pointer receiver)", val().String()) + + val, ok = gal.ObjectGetMethod(myCar, "Ignite") + require.True(t, ok) + assert.Equal(t, gal.True, val()) + + val, ok = gal.ObjectGetMethod(myCar, "CurrentSpeed") + require.True(t, ok) + assert.Equal(t, "50.345", val().(gal.Numberer).Number().Trunc(3).String()) + + val, ok = gal.ObjectGetMethod(myCar, "CurrentSpeed2") + require.True(t, ok) + assert.Equal(t, "50.345", val().(gal.Numberer).Number().Trunc(3).String()) + + val, ok = gal.ObjectGetMethod(myCar, "SetSpeed") + require.True(t, ok) + got := val(gal.NewNumberFromFloat(76), gal.True, gal.False) + assert.Equal(t, "undefined: invalid function call - object::*gal_test.Car:SetSpeed - wants 1 args, received 3 instead", got.String()) + + val, ok = gal.ObjectGetMethod(myCar, "SetSpeed") + require.True(t, ok) + got = val(gal.NewNumberFromFloat(86)) + assert.Equal(t, "undefined: invalid function call - object::*gal_test.Car:SetSpeed - must return 1 value, returned 0 instead", got.String()) + + val, ok = gal.ObjectGetMethod(myCar, "SetSpeed2") + require.True(t, ok) + got = val(gal.NewNumberFromFloat(96)) + assert.Equal(t, gal.True, got) + assert.Equal(t, "96", myCar.CurrentSpeed().String()) + + val, ok = gal.ObjectGetMethod(myCar, "SetSpeed3") + require.True(t, ok) + got = val(gal.NewNumberFromFloat(106)) + assert.Equal(t, gal.True, got) + assert.Equal(t, "106", myCar.CurrentSpeed().String()) + + val, ok = gal.ObjectGetMethod(myCar, "SetSpeed4") + require.True(t, ok) + got = val(gal.NewString("blah")) + assert.Equal(t, "undefined: invalid function call - object::*gal_test.Car:SetSpeed4 - invalid argument type passed to function - reflect: Call using gal.String as type gal_test.fancyType", got.String()) +} diff --git a/tree.go b/tree.go index ec376c6..0374eff 100644 --- a/tree.go +++ b/tree.go @@ -66,12 +66,25 @@ type Variables map[string]Value type Functions map[string]FunctionalValue // Function returns the function definition of the function of the specified name. -func (f Functions) Function(name string) FunctionalValue { - if f == nil { +func (tc treeConfig) Function(name string) FunctionalValue { + splits := strings.Split(name, ".") + if len(splits) > 1 { + // TODO: add recursive handling i.e. obj1.obj2.func1()? + if tc.objects != nil { + obj, ok := tc.objects[splits[0]] + if ok { + fv, _ := ObjectGetMethod(obj, splits[1]) + return fv + } + } + return nil + } + + if tc.functions == nil { return nil } - if val, ok := f[name]; ok { + if val, ok := tc.functions[name]; ok { return val } @@ -174,6 +187,10 @@ func (tree Tree) Eval(opts ...treeOption) Value { // Split divides a Tree trunk at points where two consecutive entries are present without // an operator in between. func (tree Tree) Split() []Tree { + if len(tree) == 0 { + return []Tree{} + } + var forest []Tree partStart := 0 @@ -248,7 +265,7 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree } } - rhsVal := e.(Tree).Eval(WithFunctions(cfg.functions), WithVariables(cfg.variables)) + rhsVal := e.(Tree).Eval(WithFunctions(cfg.functions), WithVariables(cfg.variables), WithObjects(cfg.objects)) if v, ok := rhsVal.(Undefined); ok { slog.Debug("Tree.Calc: val is Undefined", "i", i, "val", v.String()) return Tree{v} @@ -280,10 +297,10 @@ func (tree Tree) Calc(isOperatorInPrecedenceGroup func(Operator) bool, cfg *tree slog.Debug("Tree.Calc: functionEntryKind", "i", i, "name", e.(Function).Name) f := e.(Function) //nolint: errcheck if f.BodyFn == nil { - f.BodyFn = cfg.functions.Function(f.Name) + f.BodyFn = cfg.Function(f.Name) } - rhsVal := f.Eval(WithFunctions(cfg.functions), WithVariables(cfg.variables)) + rhsVal := f.Eval(WithFunctions(cfg.functions), WithVariables(cfg.variables), WithObjects(cfg.objects)) if v, ok := rhsVal.(Undefined); ok { slog.Debug("Tree.Calc: val is Undefined", "i", i, "val", v.String()) return Tree{v} diff --git a/tree_builder.go b/tree_builder.go index 41ea25f..b7946ce 100644 --- a/tree_builder.go +++ b/tree_builder.go @@ -183,7 +183,8 @@ func extractPart(expr string) (string, exprType, int, error) { switch { case errors.Is(err, errFunctionNameWithoutParens): if strings.Contains(fname, ".") { - // object property found: act like a variable (TODO: could create a new objectPropertyType) + // object property found: act like a variable + // TODO: could create a new objectPropertyType return fname, variableType, pos + lf, nil } // allow to continue so we can check alphanumerical operator names such as "And", "Or", etc diff --git a/tree_builder_test.go b/tree_builder_test.go index 92dfbfc..06e79e6 100644 --- a/tree_builder_test.go +++ b/tree_builder_test.go @@ -1,7 +1,6 @@ package gal_test import ( - "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -42,7 +41,6 @@ func TestTreeBuilder_FromExpr_PlusMinus_String(t *testing.T) { expr := `"-3 + -4" + -3 --4 / ( 1 + 2+3+4) +tan(10)` tree, err := gal.NewTreeBuilder().FromExpr(expr) require.NoError(t, err) - fmt.Println(tree.Eval()) expectedTree := gal.Tree{ gal.NewString(`-3 + -4`), diff --git a/value.go b/value.go index b1ec4ba..2832929 100644 --- a/value.go +++ b/value.go @@ -29,35 +29,13 @@ func ToValue(value any) Value { v, _ := toValue(value) return v } + func toValue(value any) (Value, bool) { - switch typedValue := value.(type) { - case int: - return NewNumberFromInt(int64(typedValue)), true - case int32: - return NewNumberFromInt(int64(typedValue)), true - case int64: - return NewNumberFromInt(typedValue), true - case uint: - return NewNumberFromInt(int64(typedValue)), true - case uint32: - return NewNumberFromInt(int64(typedValue)), true - case uint64: - n, err := NewNumberFromString(fmt.Sprintf("%d", typedValue)) - if err != nil { - return NewUndefinedWithReasonf("value '%d' cannot be converted to a Number", typedValue), false - } - return n, true - case float32: // this will commonly suffer from floating point issues - return NewNumberFromFloat(float64(typedValue)), true - case float64: - return NewNumberFromFloat(typedValue), true - case string: - return NewString(typedValue), true - case bool: - return NewBool(typedValue), true - default: - return NewUndefinedWithReasonf("type '%T', does not resolve to a gal.Value", typedValue), false + v, err := goAnyToGalType(value) + if err != nil { + return NewUndefinedWithReasonf("value type %T - %s", value, err.Error()), false } + return v, true } func ToNumber(val Value) Number { @@ -77,8 +55,8 @@ func ToBool(val Value) Bool { // Functions can accept a MultiValue, and also return a MultiValue. // This allows a function to effectively return multiple values as a MultiValue. // TODO: we could add a syntax to instantiate a MultiValue within an expression. -// TODO: ... perhaps along the lines of [[v1 v2 ...]] or simply a built-in function such as -// TODO: ... MultiValue(...) - nothing stops the user from creating their own for now :-) +// ... perhaps along the lines of [[v1 v2 ...]] or simply a built-in function such as +// ... MultiValue(...) - nothing stops the user from creating their own for now :-) // // TODO: implement other methods such as Add, LessThan, etc (if meaningful) type MultiValue struct {