Skip to content

Commit

Permalink
Optional field selection runtime (#599)
Browse files Browse the repository at this point in the history
* Refactor of attributes qualification to support presence testing
* Implementation of optional field selection runtime changes
* CEL library changes to expose runtime functions
  • Loading branch information
TristonianJones authored Nov 2, 2022
1 parent 33363a2 commit 37f2cc1
Show file tree
Hide file tree
Showing 8 changed files with 711 additions and 147 deletions.
1 change: 1 addition & 0 deletions cel/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ go_library(
"//checker/decls:go_default_library",
"//common:go_default_library",
"//common/containers:go_default_library",
"//common/operators:go_default_library",
"//common/overloads:go_default_library",
"//common/types:go_default_library",
"//common/types/pb:go_default_library",
Expand Down
33 changes: 24 additions & 9 deletions cel/library.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"time"

"github.com/google/cel-go/checker"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/overloads"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
Expand Down Expand Up @@ -87,19 +88,22 @@ func (stdLibrary) ProgramOptions() []ProgramOption {
type optionalLibrary struct{}

func (optionalLibrary) CompileOptions() []EnvOption {
paramType := TypeParamType("T")
optionalType := OpaqueType("optional", paramType)
paramTypeK := TypeParamType("K")
paramTypeV := TypeParamType("V")
optionalTypeV := OptionalType(paramTypeV)
listTypeV := ListType(paramTypeV)
mapTypeKV := MapType(paramTypeK, paramTypeV)

return []EnvOption{
Types(types.OptionalType),
// Global and member functions for working with optional values.
Function("optional.of",
Overload("optional_of", []*Type{paramType}, optionalType,
Overload("optional_of", []*Type{paramTypeV}, optionalTypeV,
UnaryBinding(func(value ref.Val) ref.Val {
return types.OptionalOf(value)
}))),
Function("optional.ofNonZeroValue",
Overload("optional_ofNonZeroValue", []*Type{paramType}, optionalType,
Overload("optional_ofNonZeroValue", []*Type{paramTypeV}, optionalTypeV,
UnaryBinding(func(value ref.Val) ref.Val {
v, isZeroer := value.(traits.Zeroer)
if !isZeroer || !v.IsZeroValue() {
Expand All @@ -108,28 +112,39 @@ func (optionalLibrary) CompileOptions() []EnvOption {
return types.OptionalNone
}))),
Function("optional.none",
Overload("optional_none", []*Type{}, optionalType,
Overload("optional_none", []*Type{}, optionalTypeV,
FunctionBinding(func(values ...ref.Val) ref.Val {
return types.OptionalNone
}))),
Function("value",
MemberOverload("optional_value", []*Type{optionalType}, paramType,
MemberOverload("optional_value", []*Type{optionalTypeV}, paramTypeV,
UnaryBinding(func(value ref.Val) ref.Val {
opt := value.(*types.Optional)
return opt.GetValue()
}))),
Function("hasValue",
MemberOverload("optional_hasValue", []*Type{optionalType}, paramType,
MemberOverload("optional_hasValue", []*Type{optionalTypeV}, paramTypeV,
UnaryBinding(func(value ref.Val) ref.Val {
opt := value.(*types.Optional)
return types.Bool(opt.HasValue())
}))),
// Implementation of 'or' and 'orValue' are special-cased to support short-circuiting in the
// evaluation chain.
Function("or",
MemberOverload("optional_or_optional", []*Type{optionalType, optionalType}, optionalType)),
MemberOverload("optional_or_optional", []*Type{optionalTypeV, optionalTypeV}, optionalTypeV)),
Function("orValue",
MemberOverload("optional_orValue_value", []*Type{optionalType, paramType}, paramType)),
MemberOverload("optional_orValue_value", []*Type{optionalTypeV, paramTypeV}, paramTypeV)),
// OptSelect is handled specially by the type-checker, so the receiver's field type is used to determine the
// optput type.
Function(operators.OptSelect,
MemberOverload("select_optional_field", []*Type{DynType, StringType}, optionalTypeV)),
// OptIndex is handled mostly like any other indexing operation on a list or map, so the type-checker can use
// these signatures to determine type-agreement without any special handling.
Function(operators.OptIndex,
MemberOverload("list_index_optional_int", []*Type{listTypeV, IntType}, optionalTypeV),
MemberOverload("optional_list_index_optional_int", []*Type{OptionalType(listTypeV), IntType}, optionalTypeV),
MemberOverload("map_index_optional_value", []*Type{mapTypeKV, paramTypeK}, optionalTypeV),
MemberOverload("optional_map_index_optional_value", []*Type{OptionalType(mapTypeKV), paramTypeK}, optionalTypeV)),
}
}

Expand Down
2 changes: 1 addition & 1 deletion interpreter/attribute_patterns.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func (fac *partialAttributeFactory) matchesUnknownPatterns(
}
// If this resolution behavior ever changes, new implementations of the
// qualifierValueEquator may be required to handle proper resolution.
qual, err = fac.NewQualifier(nil, qual.ID(), val)
qual, err = fac.NewQualifier(nil, qual.ID(), val, attr.IsOptional())
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions interpreter/attribute_patterns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ func TestAttributePattern_CrossReference(t *testing.T) {
map[string]any{"a": []int64{1, 2}, "b": 0},
NewAttributePattern("a").QualInt(0).QualString("c"))
// Qualify a[b] with 'c', a[b].c
c, _ := fac.NewQualifier(nil, 3, "c")
c, _ := fac.NewQualifier(nil, 3, "c", false)
a.AddQualifier(c)
// The resolve step should return unknown
val, err = a.Resolve(partVars)
Expand All @@ -324,7 +324,7 @@ func genAttr(fac AttributeFactory, a attr) Attribute {
attr = fac.AbsoluteAttribute(1, a.name)
}
for _, q := range a.quals {
qual, _ := fac.NewQualifier(nil, id, q)
qual, _ := fac.NewQualifier(nil, id, q, false)
attr.AddQualifier(qual)
id++
}
Expand Down
Loading

0 comments on commit 37f2cc1

Please sign in to comment.