Skip to content

Commit

Permalink
feat: @computed attribute config and validations (#1686)
Browse files Browse the repository at this point in the history
Co-authored-by: Jon Bretman <[email protected]>
  • Loading branch information
davenewza and jonbretman authored Jan 10, 2025
1 parent d0efc0e commit 2a61c06
Show file tree
Hide file tree
Showing 50 changed files with 2,629 additions and 431 deletions.
147 changes: 100 additions & 47 deletions expressions/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,55 @@ import (
"github.com/teamkeel/keel/schema/query"
)

// Defines which types are compatible with each other for each comparison operator
// This is used to generate all the necessary combinations of operator overloads
var typeCompatibilityMapping = map[string][][]*types.Type{
operators.Equals: {
{types.StringType, typing.Text, typing.ID, typing.Markdown},
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
{typing.Boolean, types.BoolType},
{types.NewListType(types.StringType), typing.TextArray, typing.IDArray, typing.MarkdownArray},
{types.NewListType(types.IntType), types.NewListType(types.DoubleType), typing.NumberArray, typing.DecimalArray},
},
operators.NotEquals: {
{types.StringType, typing.Text, typing.ID, typing.Markdown},
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
{typing.Boolean, types.BoolType},
{types.NewListType(types.StringType), typing.TextArray, typing.IDArray, typing.MarkdownArray},
{types.NewListType(types.IntType), types.NewListType(types.DoubleType), typing.NumberArray, typing.DecimalArray},
},
operators.Greater: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
},
operators.GreaterEquals: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
},
operators.Less: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
},
operators.LessEquals: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
},
operators.Add: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
},
operators.Subtract: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
},
operators.Multiply: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
},
operators.Divide: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
},
}

// WithSchemaTypes declares schema models, enums and roles as types in the CEL environment
func WithSchemaTypes(schema []*parser.AST) expressions.Option {
return func(p *expressions.Parser) error {
Expand Down Expand Up @@ -213,42 +262,7 @@ func WithLogicalOperators() expressions.Option {
// WithComparisonOperators enables support for comparison operators for all types
func WithComparisonOperators() expressions.Option {
return func(p *expressions.Parser) error {
// Defines which types are compatible with each other for each comparison operator
// This is used to generate all the necessary combinations of operator overloads
typeCompatibilityMapping := map[string][][]*types.Type{
operators.Equals: {
{types.StringType, typing.Text, typing.ID, typing.Markdown},
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
{typing.Boolean, types.BoolType},
{types.NewListType(types.StringType), typing.TextArray, typing.IDArray, typing.MarkdownArray},
{types.NewListType(types.IntType), types.NewListType(types.DoubleType), typing.NumberArray, typing.DecimalArray},
},
operators.NotEquals: {
{types.StringType, typing.Text, typing.ID, typing.Markdown},
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
{typing.Boolean, types.BoolType},
{types.NewListType(types.StringType), typing.TextArray, typing.IDArray, typing.MarkdownArray},
{types.NewListType(types.IntType), types.NewListType(types.DoubleType), typing.NumberArray, typing.DecimalArray},
},
operators.Greater: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
},
operators.GreaterEquals: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
},
operators.Less: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
},
operators.LessEquals: {
{types.IntType, types.DoubleType, typing.Number, typing.Decimal},
{typing.Date, typing.Timestamp, types.TimestampType},
},
}
mapping := map[string][][]*types.Type{}

var err error
if p.Provider.Schema != nil {
Expand All @@ -257,12 +271,12 @@ func WithComparisonOperators() expressions.Option {
enumType := types.NewOpaqueType(enum.Name.Value)
enumTypeArr := types.NewOpaqueType(enum.Name.Value + "[]")

typeCompatibilityMapping[operators.Equals] = append(typeCompatibilityMapping[operators.Equals],
mapping[operators.Equals] = append(mapping[operators.Equals],
[]*types.Type{enumType},
[]*types.Type{enumTypeArr, types.NewListType(enumType)},
)

typeCompatibilityMapping[operators.NotEquals] = append(typeCompatibilityMapping[operators.NotEquals],
mapping[operators.NotEquals] = append(mapping[operators.NotEquals],
[]*types.Type{enumType},
[]*types.Type{enumTypeArr, types.NewListType(enumType)},
)
Expand All @@ -286,12 +300,12 @@ func WithComparisonOperators() expressions.Option {
modelType := types.NewObjectType(model.Name.Value)
modelTypeArr := types.NewObjectType(model.Name.Value + "[]")

typeCompatibilityMapping[operators.Equals] = append(typeCompatibilityMapping[operators.Equals],
mapping[operators.Equals] = append(mapping[operators.Equals],
[]*types.Type{modelType},
[]*types.Type{modelTypeArr},
)

typeCompatibilityMapping[operators.NotEquals] = append(typeCompatibilityMapping[operators.NotEquals],
mapping[operators.NotEquals] = append(mapping[operators.NotEquals],
[]*types.Type{modelType},
[]*types.Type{modelTypeArr},
)
Expand All @@ -306,15 +320,21 @@ func WithComparisonOperators() expressions.Option {
}
}

for k, v := range typeCompatibilityMapping {
mapping[k] = append(mapping[k], v...)
}

// Add operator overloads for each compatible type combination
options := []cel.EnvOption{}
for k, v := range typeCompatibilityMapping {
for _, t := range v {
for _, arg1 := range t {
for _, arg2 := range t {
name := fmt.Sprintf("%s_%s_%s", k, arg1.String(), arg2.String())
opt := cel.Function(k, cel.Overload(name, argTypes(arg1, arg2), types.BoolType))
options = append(options, opt)
for k, v := range mapping {
switch k {
case operators.Equals, operators.NotEquals, operators.Greater, operators.GreaterEquals, operators.Less, operators.LessEquals:
for _, t := range v {
for _, arg1 := range t {
for _, arg2 := range t {
opt := cel.Function(k, cel.Overload(overloadName(k, arg1, arg2), argTypes(arg1, arg2), types.BoolType))
options = append(options, opt)
}
}
}
}
Expand Down Expand Up @@ -385,6 +405,35 @@ func WithComparisonOperators() expressions.Option {
}
}

// WithArithmeticOperators enables support for arithmetic operators
func WithArithmeticOperators() expressions.Option {
return func(p *expressions.Parser) error {
// Add operator overloads for each compatible type combination
options := []cel.EnvOption{}
for k, v := range typeCompatibilityMapping {
switch k {
case operators.Add, operators.Subtract, operators.Multiply, operators.Divide:
for _, t := range v {
for _, arg1 := range t {
for _, arg2 := range t {
opt := cel.Function(k, cel.Overload(overloadName(k, arg1, arg2), argTypes(arg1, arg2), typing.Decimal))
options = append(options, opt)
}
}
}
}
}

var err error
p.CelEnv, err = p.CelEnv.Extend(options...)
if err != nil {
return err
}

return nil
}
}

// WithReturnTypeAssertion will check that the expression evaluates to a specific type
func WithReturnTypeAssertion(returnType string, asArray bool) expressions.Option {
return func(p *expressions.Parser) error {
Expand All @@ -397,3 +446,7 @@ func WithReturnTypeAssertion(returnType string, asArray bool) expressions.Option
func argTypes(args ...*types.Type) []*types.Type {
return args
}

func overloadName(op string, t1 *types.Type, t2 *types.Type) string {
return fmt.Sprintf("%s_%s_%s", op, t1.String(), t2.String())
}
2 changes: 2 additions & 0 deletions expressions/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ func typesAssignable(expected *types.Type, actual *types.Type) bool {
typing.Markdown.String(): {mapType(typing.Text.String()), mapType(typing.Markdown.String())},
typing.ID.String(): {mapType(typing.Text.String()), mapType(typing.ID.String())},
typing.Text.String(): {mapType(typing.Text.String()), mapType(typing.Markdown.String()), mapType(typing.ID.String())},
typing.Number.String(): {mapType(typing.Number.String()), mapType(typing.Decimal.String())},
typing.Decimal.String(): {mapType(typing.Number.String()), mapType(typing.Decimal.String())},
}

// Check if there are specific compatibility rules for the expected type
Expand Down
70 changes: 47 additions & 23 deletions expressions/resolve/visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,19 @@ func (w *CelVisitor[T]) run(expression *parser.Expression) (T, error) {

w.ast = ast

if err := w.eval(checkedExpr.Expr, false); err != nil {
if err := w.eval(checkedExpr.Expr, isComplexOperatorWithRespectTo(operators.LogicalAnd, checkedExpr.Expr), false); err != nil {
return zero, err
}

return w.visitor.Result()
}

func (w *CelVisitor[T]) eval(expr *exprpb.Expr, inBinaryCondition bool) error {
func (w *CelVisitor[T]) eval(expr *exprpb.Expr, nested bool, inBinary bool) error {
var err error

switch expr.ExprKind.(type) {
case *exprpb.Expr_ConstExpr, *exprpb.Expr_ListExpr, *exprpb.Expr_SelectExpr, *exprpb.Expr_IdentExpr:
if !inBinaryCondition {
if !inBinary {
err := w.visitor.StartCondition(false)
if err != nil {
return err
Expand All @@ -96,10 +96,20 @@ func (w *CelVisitor[T]) eval(expr *exprpb.Expr, inBinaryCondition bool) error {

switch expr.ExprKind.(type) {
case *exprpb.Expr_CallExpr:
err = w.visitor.StartCondition(nested)
if err != nil {
return err
}

err := w.callExpr(expr)
if err != nil {
return err
}

err = w.visitor.EndCondition(nested)
if err != nil {
return err
}
case *exprpb.Expr_ConstExpr:
err := w.constExpr(expr)
if err != nil {
Expand All @@ -126,7 +136,7 @@ func (w *CelVisitor[T]) eval(expr *exprpb.Expr, inBinaryCondition bool) error {

switch expr.ExprKind.(type) {
case *exprpb.Expr_ConstExpr, *exprpb.Expr_ListExpr, *exprpb.Expr_SelectExpr, *exprpb.Expr_IdentExpr:
if !inBinaryCondition {
if !inBinary {
err := w.visitor.EndCondition(false)
if err != nil {
return err
Expand Down Expand Up @@ -173,18 +183,12 @@ func (w *CelVisitor[T]) binaryCall(expr *exprpb.Expr) error {
op := c.GetFunction()
args := c.GetArgs()
lhs := args[0]

isComplex := isComplexOperatorWithRespectTo(operators.LogicalAnd, expr)

err := w.visitor.StartCondition(isComplex)
if err != nil {
return err
}
lhsParen := isComplexOperatorWithRespectTo(op, lhs)
var err error

inBinary := !(op == operators.LogicalAnd || op == operators.LogicalOr)

rhs := args[1]
if err := w.eval(lhs, inBinary); err != nil {
if err := w.eval(lhs, lhsParen, inBinary); err != nil {
return err
}

Expand All @@ -200,11 +204,17 @@ func (w *CelVisitor[T]) binaryCall(expr *exprpb.Expr) error {
return err
}

if err := w.eval(rhs, inBinary); err != nil {
rhs := args[1]
rhsParen := isComplexOperatorWithRespectTo(op, rhs)
if !rhsParen && isLeftRecursive(op) {
rhsParen = isSamePrecedence(op, rhs)
}

if err := w.eval(rhs, rhsParen, inBinary); err != nil {
return err
}

return w.visitor.EndCondition(isComplex)
return nil
}

func (w *CelVisitor[T]) unaryCall(expr *exprpb.Expr) error {
Expand All @@ -224,16 +234,11 @@ func (w *CelVisitor[T]) unaryCall(expr *exprpb.Expr) error {
return fmt.Errorf("not implemented: %s", fun)
}

err := w.visitor.StartCondition(isComplex)
if err != nil {
return err
}

if err := w.eval(args[0], false); err != nil {
if err := w.eval(args[0], isComplex, false); err != nil {
return err
}

return w.visitor.EndCondition(isComplex)
return nil
}

func (w *CelVisitor[T]) constExpr(expr *exprpb.Expr) error {
Expand Down Expand Up @@ -362,7 +367,7 @@ func (w *CelVisitor[T]) SelectExpr(expr *exprpb.Expr) error {

switch expr.ExprKind.(type) {
case *exprpb.Expr_CallExpr:
err := w.eval(sel.GetOperand(), true)
err := w.eval(sel.GetOperand(), true, true)
if err != nil {
return err
}
Expand Down Expand Up @@ -449,6 +454,25 @@ func isComplexOperatorWithRespectTo(op string, expr *exprpb.Expr) bool {
return isLowerPrecedence(op, expr)
}

// isLeftRecursive indicates whether the parser resolves the call in a left-recursive manner as
// this can have an effect of how parentheses affect the order of operations in the AST.
func isLeftRecursive(op string) bool {
return op != operators.LogicalAnd && op != operators.LogicalOr
}

// isSamePrecedence indicates whether the precedence of the input operator is the same as the
// precedence of the (possible) operation represented in the input Expr.
//
// If the expr is not a Call, the result is false.
func isSamePrecedence(op string, expr *exprpb.Expr) bool {
if expr.GetCallExpr() == nil {
return false
}
c := expr.GetCallExpr()
other := c.GetFunction()
return operators.Precedence(op) == operators.Precedence(other)
}

func toNative(c *exprpb.Constant) (any, error) {
switch c.ConstantKind.(type) {
case *exprpb.Constant_BoolValue:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ require (
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.4
github.com/teamkeel/graphql v0.8.2-0.20230531102419-995b8ab035b6
github.com/test-go/testify v1.1.4
github.com/twitchtv/twirp v8.1.3+incompatible
github.com/vincent-petithory/dataurl v1.0.0
github.com/xeipuuv/gojsonschema v1.2.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/teamkeel/graphql v0.8.2-0.20230531102419-995b8ab035b6 h1:q8ZbAgqr7jJlZNJ4WAI+QMuZrcCBDOw9k7orYuy+Vqs=
github.com/teamkeel/graphql v0.8.2-0.20230531102419-995b8ab035b6/go.mod h1:5td34OA5ZUdckc2w3GgE7QQoaG8MK6hIVR3dFI+qaK4=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
github.com/tkuchiki/go-timezone v0.2.0 h1:yyZVHtQRVZ+wvlte5HXvSpBkR0dPYnPEIgq9qqAqltk=
Expand Down
Loading

0 comments on commit 2a61c06

Please sign in to comment.