Skip to content

Commit

Permalink
added ObjectGetMethod in prep for object function support
Browse files Browse the repository at this point in the history
  • Loading branch information
seborama committed May 11, 2024
1 parent b82fc99 commit 64c9e73
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 53 deletions.
4 changes: 2 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'.
Expand Down Expand Up @@ -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`.
Expand Down
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand All @@ -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:
Expand All @@ -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.
Expand All @@ -172,12 +174,44 @@ 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.

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:
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
154 changes: 112 additions & 42 deletions object.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package gal
import (
"fmt"
"reflect"

"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
}
Expand All @@ -32,53 +35,120 @@ 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)
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) {
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'", 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.Float32:
return reflect.ValueOf(float32(item.(Numberer).Number().Float64()))
case reflect.Float64:
return reflect.ValueOf(item.(Numberer).Number().Float64())
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])
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(vValue reflect.Value) (Value, error) {
vValueI := vValue.Interface()

switch vValueIType := vValueI.(type) {
case Value:
return vValueIType, nil
case int:
return NewNumberFromInt(int64(vValueIType)), nil
case int32:
return NewNumberFromInt(int64(vValueIType)), nil
case int64:
return NewNumberFromInt(vValueIType), nil
case uint:
return NewNumberFromInt(int64(vValueIType)), nil
case uint32:
return NewNumberFromInt(int64(vValueIType)), nil
case uint64:
n, err := NewNumberFromString(fmt.Sprintf("%d", vValueIType))
if err != nil {
return nil, errors.Errorf("value uint64(%d) cannot be converted to a Number", vValueIType)
}
return n, nil
case float32: // this will commonly suffer from floating point issues
return NewNumberFromFloat(float64(vValueIType)), nil
case float64:
return NewNumberFromFloat(vValueIType), nil
case string:
return NewString(vValueIType), nil
case bool:
return NewBool(vValueIType), nil
default:
return nil, errors.Errorf("type '%T' cannot be mapped to gal.Value", vValueIType)
}
}
Loading

0 comments on commit 64c9e73

Please sign in to comment.