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 417aa43
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 72 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
13 changes: 4 additions & 9 deletions function.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 {
Expand Down
73 changes: 72 additions & 1 deletion gal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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())
}
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
Loading

0 comments on commit 417aa43

Please sign in to comment.