Skip to content

Commit 9a4b48b

Browse files
ContextEval support for Unknowns (#1126)
* Fix for ContextEval with unknown attributes
1 parent c053251 commit 9a4b48b

File tree

5 files changed

+58
-10
lines changed

5 files changed

+58
-10
lines changed

cel/cel_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,48 @@ func TestContextEval(t *testing.T) {
10051005
}
10061006
}
10071007

1008+
func TestContextEvalUnknowns(t *testing.T) {
1009+
env, err := NewEnv(
1010+
Variable("groups", ListType(IntType)),
1011+
Variable("id", IntType),
1012+
)
1013+
if err != nil {
1014+
t.Fatalf("NewEnv() failed: %v", err)
1015+
}
1016+
1017+
pvars, err := PartialVars(
1018+
map[string]any{
1019+
"groups": []int{1, 2, 3},
1020+
},
1021+
AttributePattern("id"),
1022+
)
1023+
if err != nil {
1024+
t.Fatalf("PartialVars() failed: %v", err)
1025+
}
1026+
1027+
ast, iss := env.Compile(`groups.exists(t, t == id)`)
1028+
if iss.Err() != nil {
1029+
t.Fatalf("env.Compile() failed: %v", iss.Err())
1030+
}
1031+
1032+
prg, err := env.Program(ast, EvalOptions(OptTrackState, OptPartialEval), InterruptCheckFrequency(100))
1033+
if err != nil {
1034+
t.Fatalf("env.Program() failed: %v", err)
1035+
}
1036+
1037+
out, _, err := prg.Eval(pvars)
1038+
if err != nil {
1039+
t.Fatalf("prg.Eval() failed: %v", err)
1040+
}
1041+
ctxOut, _, err := prg.ContextEval(context.Background(), pvars)
1042+
if err != nil {
1043+
t.Fatalf("prg.ContextEval() failed: %v", err)
1044+
}
1045+
if !reflect.DeepEqual(out, ctxOut) {
1046+
t.Errorf("got %v, wanted %v", out, ctxOut)
1047+
}
1048+
}
1049+
10081050
func BenchmarkContextEval(b *testing.B) {
10091051
env := testEnv(b,
10101052
Variable("items", ListType(IntType)),

cel/program.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,11 @@ func (a *ctxEvalActivation) Parent() Activation {
466466
return a.parent
467467
}
468468

469+
func (a *ctxEvalActivation) AsPartialActivation() (interpreter.PartialActivation, bool) {
470+
pa, ok := a.parent.(interpreter.PartialActivation)
471+
return pa, ok
472+
}
473+
469474
func newCtxEvalActivationPool() *ctxEvalActivationPool {
470475
return &ctxEvalActivationPool{
471476
Pool: sync.Pool{

interpreter/activation.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ type PartialActivation interface {
158158

159159
// partialActivationConverter indicates whether an Activation implementation supports conversion to a PartialActivation
160160
type partialActivationConverter interface {
161-
asPartialActivation() (PartialActivation, bool)
161+
// AsPartialActivation converts the current activation to a PartialActivation
162+
AsPartialActivation() (PartialActivation, bool)
162163
}
163164

164165
// partActivation is the default implementations of the PartialActivation interface.
@@ -172,19 +173,19 @@ func (a *partActivation) UnknownAttributePatterns() []*AttributePattern {
172173
return a.unknowns
173174
}
174175

175-
// asPartialActivation returns the partActivation as a PartialActivation interface.
176-
func (a *partActivation) asPartialActivation() (PartialActivation, bool) {
176+
// AsPartialActivation returns the partActivation as a PartialActivation interface.
177+
func (a *partActivation) AsPartialActivation() (PartialActivation, bool) {
177178
return a, true
178179
}
179180

180-
func asPartialActivation(vars Activation) (PartialActivation, bool) {
181+
func AsPartialActivation(vars Activation) (PartialActivation, bool) {
181182
// Only internal activation instances may implement this interface
182183
if pv, ok := vars.(partialActivationConverter); ok {
183-
return pv.asPartialActivation()
184+
return pv.AsPartialActivation()
184185
}
185186
// Since Activations may be hierarchical, test whether a parent converts to a PartialActivation
186187
if vars.Parent() != nil {
187-
return asPartialActivation(vars.Parent())
188+
return AsPartialActivation(vars.Parent())
188189
}
189190
return nil, false
190191
}

interpreter/attribute_patterns.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ func (m *attributeMatcher) AddQualifier(qual Qualifier) (Attribute, error) {
358358
func (m *attributeMatcher) Resolve(vars Activation) (any, error) {
359359
id := m.NamespacedAttribute.ID()
360360
// Bug in how partial activation is resolved, should search parents as well.
361-
partial, isPartial := asPartialActivation(vars)
361+
partial, isPartial := AsPartialActivation(vars)
362362
if isPartial {
363363
unk, err := m.fac.matchesUnknownPatterns(
364364
partial,

interpreter/interpretable.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,16 +1370,16 @@ func (f *folder) Parent() Activation {
13701370
// if they were provided to the input activation, or an empty set if the proxied activation is not partial.
13711371
func (f *folder) UnknownAttributePatterns() []*AttributePattern {
13721372
if pv, ok := f.activation.(partialActivationConverter); ok {
1373-
if partial, isPartial := pv.asPartialActivation(); isPartial {
1373+
if partial, isPartial := pv.AsPartialActivation(); isPartial {
13741374
return partial.UnknownAttributePatterns()
13751375
}
13761376
}
13771377
return []*AttributePattern{}
13781378
}
13791379

1380-
func (f *folder) asPartialActivation() (PartialActivation, bool) {
1380+
func (f *folder) AsPartialActivation() (PartialActivation, bool) {
13811381
if pv, ok := f.activation.(partialActivationConverter); ok {
1382-
if _, isPartial := pv.asPartialActivation(); isPartial {
1382+
if _, isPartial := pv.AsPartialActivation(); isPartial {
13831383
return f, true
13841384
}
13851385
}

0 commit comments

Comments
 (0)