Skip to content

Commit b337cf9

Browse files
authored
n-ary AST rules evaluation (#134)
* rule ast new * Upgrade strategy * comment * make sure eval still works * tmp * rulesv3 * conditionally evaluate v3 ast
1 parent e36b89c commit b337cf9

File tree

7 files changed

+686
-7
lines changed

7 files changed

+686
-7
lines changed

pkg/encoding/encoding.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ import (
3232
// type.
3333
func ParseFeature(ctx context.Context, rootPath string, featureFile feature.FeatureFile, nsMD *metadata.NamespaceConfigRepoMetadata, provider fs.Provider) (feature.EvaluableFeature, error) {
3434
switch nsMD.Version {
35-
case feature.NamespaceVersionV1Beta1.String():
36-
fallthrough
37-
case feature.NamespaceVersionV1Beta2.String():
38-
fallthrough
3935
case feature.NamespaceVersionV1Beta3.String():
4036
fallthrough
4137
case feature.NamespaceVersionV1Beta4.String():

pkg/feature/evaluate.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/lekkodev/cli/pkg/fs"
2828
featurev1beta1 "github.com/lekkodev/cli/pkg/gen/proto/go/lekko/feature/v1beta1"
2929
rulesv1beta2 "github.com/lekkodev/cli/pkg/gen/proto/go/lekko/rules/v1beta2"
30+
rulesv1beta3 "github.com/lekkodev/cli/pkg/gen/proto/go/lekko/rules/v1beta3"
3031
"github.com/lekkodev/cli/pkg/metadata"
3132
"github.com/lekkodev/cli/pkg/rules"
3233
"github.com/pkg/errors"
@@ -80,7 +81,7 @@ func (v1b3 *v1beta3) evaluate(context map[string]interface{}) (*anypb.Any, []int
8081
}
8182

8283
func (v1b3 *v1beta3) traverse(constraint *featurev1beta1.Constraint, context map[string]interface{}) (*anypb.Any, bool, []int, error) {
83-
passes, err := v1b3.evaluateRule(constraint.GetRuleAst(), context)
84+
passes, err := v1b3.evaluateRule(constraint.GetRuleAst(), constraint.GetRuleAstNew(), context)
8485
if err != nil {
8586
return nil, false, []int{}, errors.Wrap(err, "processing")
8687
}
@@ -108,7 +109,15 @@ func (v1b3 *v1beta3) traverse(constraint *featurev1beta1.Constraint, context map
108109
return retVal, passes, []int{}, nil
109110
}
110111

111-
func (v1b3 *v1beta3) evaluateRule(rule *rulesv1beta2.Rule, context map[string]interface{}) (bool, error) {
112+
func (v1b3 *v1beta3) evaluateRule(rule *rulesv1beta2.Rule, ruleV3 *rulesv1beta3.Rule, context map[string]interface{}) (bool, error) {
113+
// evaluate using the new rule AST if it exists.
114+
if ruleV3 != nil {
115+
passes, err := rules.NewV1Beta3(ruleV3).EvaluateRule(context)
116+
if err != nil {
117+
return false, errors.Wrap(err, "evaluating rule v3")
118+
}
119+
return passes, nil
120+
}
112121
if rule == nil {
113122
// empty rule evaluates to 'true'
114123
return true, nil

pkg/fixtures/fixtures.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,14 @@ func genConstraint(ruleStr string, value *anypb.Any, constraints ...*featurev1be
205205
if err != nil {
206206
log.Fatal(err)
207207
}
208+
ruleASTV3, err := parser.BuildASTV3(ruleStr)
209+
if err != nil {
210+
log.Fatal(err)
211+
}
208212
return &featurev1beta1.Constraint{
209213
Rule: ruleStr,
210214
RuleAst: ruleAST,
215+
RuleAstNew: ruleASTV3,
211216
Value: value,
212217
Constraints: constraints,
213218
}

pkg/rules/v1beta2.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,6 @@ func errMismatchedType(actual interface{}, expected ...string) error {
235235
return errors.Wrapf(ErrMismatchedType, "expected %v, got %T", strings.Join(expected, ","), actual)
236236
}
237237

238-
func errUnsupportedType(co rulesv1beta2.ComparisonOperator, actual *structpb.Value) error {
238+
func errUnsupportedType(co fmt.Stringer, actual *structpb.Value) error {
239239
return errors.Wrapf(ErrUnsupportedType, "rule type %T for comparison operator %s", actual.GetKind(), co.String())
240240
}

pkg/rules/v1beta3.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright 2022 Lekko Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package rules
16+
17+
import (
18+
"strings"
19+
20+
rulesv1beta3 "github.com/lekkodev/cli/pkg/gen/proto/go/lekko/rules/v1beta3"
21+
"github.com/pkg/errors"
22+
"google.golang.org/protobuf/types/known/structpb"
23+
)
24+
25+
// v1beta3 refers to the version of the rules protobuf type in lekko.rules.v1beta3.rules.proto
26+
type v1beta3 struct {
27+
rule *rulesv1beta3.Rule
28+
}
29+
30+
// Represents the rules defined in the proto package 'lekko.rules.v1beta3'.
31+
func NewV1Beta3(rule *rulesv1beta3.Rule) *v1beta3 {
32+
return &v1beta3{rule: rule}
33+
}
34+
35+
func (v1b3 *v1beta3) EvaluateRule(context map[string]interface{}) (bool, error) {
36+
return v1b3.evaluateRule(v1b3.rule, context)
37+
}
38+
39+
func (v1b3 *v1beta3) evaluateRule(rule *rulesv1beta3.Rule, context map[string]interface{}) (bool, error) {
40+
if rule == nil {
41+
return false, ErrEmptyRule
42+
}
43+
switch r := rule.Rule.(type) {
44+
case *rulesv1beta3.Rule_BoolConst:
45+
return r.BoolConst, nil
46+
case *rulesv1beta3.Rule_Not:
47+
innerPasses, err := v1b3.evaluateRule(r.Not, context)
48+
if err != nil {
49+
return false, errors.Wrap(err, "not: ")
50+
}
51+
return !innerPasses, nil
52+
case *rulesv1beta3.Rule_LogicalExpression:
53+
var bools []bool
54+
if len(r.LogicalExpression.GetRules()) == 0 {
55+
return false, errors.New("no rules found in logical expression")
56+
}
57+
for i, rule := range r.LogicalExpression.GetRules() {
58+
passes, err := v1b3.evaluateRule(rule, context)
59+
if err != nil {
60+
return false, errors.Wrapf(err, "rule idx %d", i)
61+
}
62+
bools = append(bools, passes)
63+
}
64+
return reduce(bools, r.LogicalExpression.LogicalOperator)
65+
case *rulesv1beta3.Rule_Atom:
66+
contextKey := r.Atom.GetContextKey()
67+
runtimeCtxVal, present := context[contextKey]
68+
if r.Atom.ComparisonOperator == rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_PRESENT {
69+
return present, nil
70+
}
71+
if r.Atom.ComparisonValue == nil {
72+
return false, errors.Wrapf(ErrEmptyRuleComparisonValue, "%s", r.Atom.String())
73+
}
74+
if !present {
75+
// All other comparison operators expect the context key to be present. If
76+
// it is not present, return false.
77+
return false, nil
78+
}
79+
switch r.Atom.ComparisonOperator {
80+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_EQUALS:
81+
return v1b3.evaluateEquals(r.Atom.GetComparisonValue(), runtimeCtxVal)
82+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_LESS_THAN:
83+
return v1b3.evaluateNumberComparator(r.Atom.ComparisonOperator, r.Atom.GetComparisonValue(), runtimeCtxVal)
84+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_LESS_THAN_OR_EQUALS:
85+
return v1b3.evaluateNumberComparator(r.Atom.ComparisonOperator, r.Atom.GetComparisonValue(), runtimeCtxVal)
86+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_GREATER_THAN:
87+
return v1b3.evaluateNumberComparator(r.Atom.ComparisonOperator, r.Atom.GetComparisonValue(), runtimeCtxVal)
88+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_GREATER_THAN_OR_EQUALS:
89+
return v1b3.evaluateNumberComparator(r.Atom.ComparisonOperator, r.Atom.GetComparisonValue(), runtimeCtxVal)
90+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_CONTAINED_WITHIN:
91+
return v1b3.evaluateContainedWithin(r.Atom.GetComparisonValue(), runtimeCtxVal)
92+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_STARTS_WITH:
93+
return v1b3.evaluateStringComparator(r.Atom.ComparisonOperator, r.Atom.GetComparisonValue(), runtimeCtxVal)
94+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_ENDS_WITH:
95+
return v1b3.evaluateStringComparator(r.Atom.ComparisonOperator, r.Atom.GetComparisonValue(), runtimeCtxVal)
96+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_CONTAINS:
97+
return v1b3.evaluateStringComparator(r.Atom.ComparisonOperator, r.Atom.GetComparisonValue(), runtimeCtxVal)
98+
}
99+
}
100+
return false, errors.Errorf("unknown rule type %T", rule.Rule)
101+
}
102+
103+
func reduce(bools []bool, op rulesv1beta3.LogicalOperator) (bool, error) {
104+
ret := op == rulesv1beta3.LogicalOperator_LOGICAL_OPERATOR_AND
105+
for _, b := range bools {
106+
switch op {
107+
case rulesv1beta3.LogicalOperator_LOGICAL_OPERATOR_AND:
108+
ret = ret && b
109+
case rulesv1beta3.LogicalOperator_LOGICAL_OPERATOR_OR:
110+
ret = ret || b
111+
default:
112+
return false, errors.Wrap(ErrUnknownLogicalOperator, op.String())
113+
}
114+
}
115+
return ret, nil
116+
}
117+
118+
// Only accepts bool, number or string ruleVal.
119+
func (v1b3 *v1beta3) evaluateEquals(ruleVal *structpb.Value, contextVal interface{}) (bool, error) {
120+
switch typed := ruleVal.Kind.(type) {
121+
case *structpb.Value_BoolValue:
122+
boolVal, ok := contextVal.(bool)
123+
if !ok {
124+
return false, errMismatchedType(contextVal, "bool")
125+
}
126+
return typed.BoolValue == boolVal, nil
127+
case *structpb.Value_NumberValue:
128+
numVal, err := getNumber(contextVal)
129+
if err != nil {
130+
return false, err
131+
}
132+
return typed.NumberValue == numVal, nil
133+
case *structpb.Value_StringValue:
134+
stringVal, ok := contextVal.(string)
135+
if !ok {
136+
return false, errMismatchedType(contextVal, "string")
137+
}
138+
return typed.StringValue == stringVal, nil
139+
default:
140+
return false, errUnsupportedType(rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_EQUALS, ruleVal)
141+
}
142+
}
143+
144+
func (v1b3 *v1beta3) evaluateNumberComparator(co rulesv1beta3.ComparisonOperator, ruleVal *structpb.Value, contextVal interface{}) (bool, error) {
145+
numVal, err := getNumber(contextVal)
146+
if err != nil {
147+
return false, err
148+
}
149+
typedNumVal, ok := ruleVal.Kind.(*structpb.Value_NumberValue)
150+
if !ok {
151+
return false, errUnsupportedType(co, ruleVal)
152+
}
153+
ruleNumVal := typedNumVal.NumberValue
154+
switch co {
155+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_LESS_THAN:
156+
return numVal < ruleNumVal, nil
157+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_LESS_THAN_OR_EQUALS:
158+
return numVal <= ruleNumVal, nil
159+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_GREATER_THAN:
160+
return numVal > ruleNumVal, nil
161+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_GREATER_THAN_OR_EQUALS:
162+
return numVal >= ruleNumVal, nil
163+
default:
164+
return false, errors.Errorf("expected numerical comparison operator, got %v", co)
165+
}
166+
}
167+
168+
func (v1b3 *v1beta3) evaluateContainedWithin(ruleVal *structpb.Value, contextVal interface{}) (bool, error) {
169+
listRuleVal, ok := ruleVal.Kind.(*structpb.Value_ListValue)
170+
if !ok {
171+
return false, errUnsupportedType(rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_CONTAINED_WITHIN, ruleVal)
172+
}
173+
for _, elem := range listRuleVal.ListValue.Values {
174+
result, err := v1b3.evaluateEquals(elem, contextVal)
175+
if err != nil || !result {
176+
continue // no match, check next element
177+
}
178+
return true, nil
179+
}
180+
return false, nil
181+
}
182+
183+
func (v1b3 *v1beta3) evaluateStringComparator(co rulesv1beta3.ComparisonOperator, ruleVal *structpb.Value, contextVal interface{}) (bool, error) {
184+
stringVal, ok := contextVal.(string)
185+
if !ok {
186+
return false, errMismatchedType(contextVal, "string")
187+
}
188+
typedRuleVal, ok := ruleVal.Kind.(*structpb.Value_StringValue)
189+
if !ok {
190+
return false, errUnsupportedType(co, ruleVal)
191+
}
192+
ruleStringVal := typedRuleVal.StringValue
193+
switch co {
194+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_STARTS_WITH:
195+
return strings.HasPrefix(stringVal, ruleStringVal), nil
196+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_ENDS_WITH:
197+
return strings.HasSuffix(stringVal, ruleStringVal), nil
198+
case rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_CONTAINS:
199+
return strings.Contains(stringVal, ruleStringVal), nil
200+
default:
201+
return false, errors.Errorf("expected string comparison operator, got %v", co)
202+
}
203+
}

pkg/rules/v1beta3_fixtures.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2022 Lekko Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package rules
16+
17+
import (
18+
"log"
19+
20+
rulesv1beta3 "github.com/lekkodev/cli/pkg/gen/proto/go/lekko/rules/v1beta3"
21+
"google.golang.org/protobuf/types/known/structpb"
22+
)
23+
24+
// age ==
25+
func AgeEqualsV3(age float64) *rulesv1beta3.Atom {
26+
return AgeV3("==", age)
27+
}
28+
29+
// city ==
30+
func CityEqualsV3(city string) *rulesv1beta3.Atom {
31+
return CityV3("==", city)
32+
}
33+
34+
func AgeV3(op string, age float64) *rulesv1beta3.Atom {
35+
return atomV3(ContextKeyAge, op, structpb.NewNumberValue(age))
36+
}
37+
38+
func CityV3(op, city string) *rulesv1beta3.Atom {
39+
return atomV3(ContextKeyCity, op, structpb.NewStringValue(city))
40+
}
41+
42+
func CityInV3(cities ...interface{}) *rulesv1beta3.Atom {
43+
list, err := structpb.NewList(cities)
44+
if err != nil {
45+
log.Fatal(err)
46+
}
47+
return atomV3(ContextKeyCity, "IN", structpb.NewListValue(list))
48+
}
49+
50+
func atomV3(key string, op string, value *structpb.Value) *rulesv1beta3.Atom {
51+
protoOp := rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_UNSPECIFIED
52+
switch op {
53+
case "==":
54+
protoOp = rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_EQUALS
55+
case "<":
56+
protoOp = rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_LESS_THAN
57+
case ">":
58+
protoOp = rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_GREATER_THAN
59+
case "<=":
60+
protoOp = rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_LESS_THAN_OR_EQUALS
61+
case ">=":
62+
protoOp = rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_GREATER_THAN_OR_EQUALS
63+
case "IN":
64+
protoOp = rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_CONTAINED_WITHIN
65+
case "STARTS":
66+
protoOp = rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_STARTS_WITH
67+
case "ENDS":
68+
protoOp = rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_ENDS_WITH
69+
case "CONTAINS":
70+
protoOp = rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_CONTAINS
71+
case "PRESENT":
72+
protoOp = rulesv1beta3.ComparisonOperator_COMPARISON_OPERATOR_PRESENT
73+
}
74+
return &rulesv1beta3.Atom{
75+
ContextKey: key,
76+
ComparisonValue: value,
77+
ComparisonOperator: protoOp,
78+
}
79+
}

0 commit comments

Comments
 (0)