Update third_party/forked/celopenapi to support kubernetes schema extensions and CEL property name escaping

This commit is contained in:
Joe Betz 2021-11-15 21:41:53 -05:00
parent 66af4ecfd5
commit f0a80eda46
7 changed files with 281 additions and 195 deletions

View File

@ -17,14 +17,11 @@ limitations under the License.
package model
import (
"fmt"
"strings"
"regexp"
"k8s.io/apimachinery/pkg/util/sets"
)
// TODO: replace escaping with new rules described in kEP update
// celReservedSymbols is a list of RESERVED symbols defined in the CEL lexer.
// No identifiers are allowed to collide with these symbols.
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#syntax
@ -36,47 +33,78 @@ var celReservedSymbols = sets.NewString(
"var", "void", "while",
)
// celLanguageIdentifiers is a list of identifiers that are part of the CEL language.
// This does NOT include builtin macro or function identifiers.
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#values
var celLanguageIdentifiers = sets.NewString(
"int", "uint", "double", "bool", "string", "bytes", "list", "map", "null_type", "type",
)
// expandMatcher matches the escape sequence, characters that are escaped, and characters that are unsupported
var expandMatcher = regexp.MustCompile(`(__|[-./]|[^a-zA-Z0-9-./_])`)
// IsRootReserved returns true if an identifier is reserved by CEL. Declaring root variables in CEL with
// these identifiers is not allowed and would result in an "overlapping identifier for name '<identifier>'"
// CEL compilation error.
func IsRootReserved(prop string) bool {
return celLanguageIdentifiers.Has(prop)
}
// Escape escapes identifiers in the AlwaysReservedIdentifiers set by prefixing ident with "_" and by prefixing
// any ident already prefixed with N '_' with N+1 '_'.
// For an identifier that does not require escaping, the identifier is returned as-is.
func Escape(ident string) string {
if strings.HasPrefix(ident, "_") || celReservedSymbols.Has(ident) {
return "_" + ident
// Escape escapes ident and returns a CEL identifier (of the form '[a-zA-Z_][a-zA-Z0-9_]*'), or returns
// false if the ident does not match the supported input format of `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*`.
// Escaping Rules:
// - '__' escapes to '__underscores__'
// - '.' escapes to '__dot__'
// - '-' escapes to '__dash__'
// - '/' escapes to '__slash__'
// - Identifiers that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are: "true", "false",
// "null", "in", "as", "break", "const", "continue", "else", "for", "function", "if", "import", "let", loop", "package",
// "namespace", "return".
func Escape(ident string) (string, bool) {
if len(ident) == 0 || ('0' <= ident[0] && ident[0] <= '9') {
return "", false
}
return ident
}
// EscapeSlice returns identifiers with Escape applied to each.
func EscapeSlice(idents []string) []string {
result := make([]string, len(idents))
for i, prop := range idents {
result[i] = Escape(prop)
if celReservedSymbols.Has(ident) {
return "__" + ident + "__", true
}
return result
}
// Unescape unescapes an identifier escaped by Escape.
func Unescape(escaped string) string {
if strings.HasPrefix(escaped, "_") {
trimmed := strings.TrimPrefix(escaped, "_")
if strings.HasPrefix(trimmed, "_") || celReservedSymbols.Has(trimmed) {
return trimmed
ok := true
ident = expandMatcher.ReplaceAllStringFunc(ident, func(s string) string {
switch s {
case "__":
return "__underscores__"
case ".":
return "__dot__"
case "-":
return "__dash__"
case "/":
return "__slash__"
default: // matched a unsupported supported
ok = false
return ""
}
panic(fmt.Sprintf("failed to unescape improperly escaped string: %v", escaped))
})
if !ok {
return "", false
}
return escaped
return ident, true
}
var unexpandMatcher = regexp.MustCompile(`(_{2}[^_]+_{2})`)
// Unescape unescapes an CEL identifier containing the escape sequences described in Escape, or return false if the
// string contains invalid escape sequences. The escaped input is expected to be a valid CEL identifier, but is
// not checked.
func Unescape(escaped string) (string, bool) {
ok := true
escaped = unexpandMatcher.ReplaceAllStringFunc(escaped, func(s string) string {
contents := s[2 : len(s)-2]
switch contents {
case "underscores":
return "__"
case "dot":
return "."
case "dash":
return "-"
case "slash":
return "/"
}
if celReservedSymbols.Has(contents) {
if len(s) != len(escaped) {
ok = false
}
return contents
}
ok = false
return ""
})
if !ok {
return "", false
}
return escaped, true
}

View File

@ -17,99 +17,156 @@ limitations under the License.
package model
import (
"fmt"
"regexp"
"testing"
fuzz "github.com/google/gofuzz"
)
// TestEscaping tests that property names are escaped as expected.
func TestEscaping(t *testing.T) {
cases := []struct{
unescaped string
escaped string
reservedAtRoot bool
} {
cases := []struct {
unescaped string
escaped string
unescapable bool
}{
// '.', '-', '/' and '__' are escaped since
// CEL only allows identifiers of the form: [a-zA-Z_][a-zA-Z0-9_]*
{unescaped: "a.a", escaped: "a__dot__a"},
{unescaped: "a-a", escaped: "a__dash__a"},
{unescaped: "a__a", escaped: "a__underscores__a"},
{unescaped: "a.-/__a", escaped: "a__dot____dash____slash____underscores__a"},
{unescaped: "a._a", escaped: "a__dot___a"},
{unescaped: "a__.__a", escaped: "a__underscores____dot____underscores__a"},
{unescaped: "a___a", escaped: "a__underscores___a"},
{unescaped: "a____a", escaped: "a__underscores____underscores__a"},
{unescaped: "a__dot__a", escaped: "a__underscores__dot__underscores__a"},
{unescaped: "a__underscores__a", escaped: "a__underscores__underscores__underscores__a"},
// CEL lexer RESERVED keywords must be escaped
{ unescaped: "true", escaped: "_true" },
{ unescaped: "false", escaped: "_false" },
{ unescaped: "null", escaped: "_null" },
{ unescaped: "in", escaped: "_in" },
{ unescaped: "as", escaped: "_as" },
{ unescaped: "break", escaped: "_break" },
{ unescaped: "const", escaped: "_const" },
{ unescaped: "continue", escaped: "_continue" },
{ unescaped: "else", escaped: "_else" },
{ unescaped: "for", escaped: "_for" },
{ unescaped: "function", escaped: "_function" },
{ unescaped: "if", escaped: "_if" },
{ unescaped: "import", escaped: "_import" },
{ unescaped: "let", escaped: "_let" },
{ unescaped: "loop", escaped: "_loop" },
{ unescaped: "package", escaped: "_package" },
{ unescaped: "namespace", escaped: "_namespace" },
{ unescaped: "return", escaped: "_return" },
{ unescaped: "var", escaped: "_var" },
{ unescaped: "void", escaped: "_void" },
{ unescaped: "while", escaped: "_while" },
// CEL language identifiers do not need to be escaped, but collide with builtin language identifier if bound as
// root variable names.
// i.e. "self.int == 1" is legal, but "int == 1" is not.
{ unescaped: "int", escaped: "int", reservedAtRoot: true },
{ unescaped: "uint", escaped: "uint", reservedAtRoot: true },
{ unescaped: "double", escaped: "double", reservedAtRoot: true },
{ unescaped: "bool", escaped: "bool", reservedAtRoot: true },
{ unescaped: "string", escaped: "string", reservedAtRoot: true },
{ unescaped: "bytes", escaped: "bytes", reservedAtRoot: true },
{ unescaped: "list", escaped: "list", reservedAtRoot: true },
{ unescaped: "map", escaped: "map", reservedAtRoot: true },
{ unescaped: "null_type", escaped: "null_type", reservedAtRoot: true },
{ unescaped: "type", escaped: "type", reservedAtRoot: true },
// To prevent escaping from colliding with other identifiers, all identifiers prefixed by _s are escaped by
// prefixing them with N+1 _s.
{ unescaped: "_if", escaped: "__if" },
{ unescaped: "__if", escaped: "___if" },
{ unescaped: "___if", escaped: "____if" },
{ unescaped: "_has", escaped: "__has" },
{ unescaped: "_int", escaped: "__int" },
{ unescaped: "_anything", escaped: "__anything" },
// CEL macro and function names do not need to be escaped because the parser can disambiguate them from the function and
// macro identifiers.
{ unescaped: "has", escaped: "has" },
{ unescaped: "all", escaped: "all" },
{ unescaped: "exists", escaped: "exists" },
{ unescaped: "exists_one", escaped: "exists_one" },
{ unescaped: "filter", escaped: "filter" },
{ unescaped: "size", escaped: "size" },
{ unescaped: "contains", escaped: "contains" },
{ unescaped: "startsWith", escaped: "startsWith" },
{ unescaped: "endsWith", escaped: "endsWith" },
{ unescaped: "matches", escaped: "matches" },
{ unescaped: "duration", escaped: "duration" },
{ unescaped: "timestamp", escaped: "timestamp" },
{ unescaped: "getDate", escaped: "getDate" },
{ unescaped: "getDayOfMonth", escaped: "getDayOfMonth" },
{ unescaped: "getDayOfWeek", escaped: "getDayOfWeek" },
{ unescaped: "getFullYear", escaped: "getFullYear" },
{ unescaped: "getHours", escaped: "getHours" },
{ unescaped: "getMilliseconds", escaped: "getMilliseconds" },
{ unescaped: "getMinutes", escaped: "getMinutes" },
{ unescaped: "getMonth", escaped: "getMonth" },
{ unescaped: "getSeconds", escaped: "getSeconds" },
{unescaped: "true", escaped: "__true__"},
{unescaped: "false", escaped: "__false__"},
{unescaped: "null", escaped: "__null__"},
{unescaped: "in", escaped: "__in__"},
{unescaped: "as", escaped: "__as__"},
{unescaped: "break", escaped: "__break__"},
{unescaped: "const", escaped: "__const__"},
{unescaped: "continue", escaped: "__continue__"},
{unescaped: "else", escaped: "__else__"},
{unescaped: "for", escaped: "__for__"},
{unescaped: "function", escaped: "__function__"},
{unescaped: "if", escaped: "__if__"},
{unescaped: "import", escaped: "__import__"},
{unescaped: "let", escaped: "__let__"},
{unescaped: "loop", escaped: "__loop__"},
{unescaped: "package", escaped: "__package__"},
{unescaped: "namespace", escaped: "__namespace__"},
{unescaped: "return", escaped: "__return__"},
{unescaped: "var", escaped: "__var__"},
{unescaped: "void", escaped: "__void__"},
{unescaped: "while", escaped: "__while__"},
// Not all property names are escapable
{unescaped: "@", unescapable: true},
{unescaped: "1up", unescapable: true},
{unescaped: "👑", unescapable: true},
// CEL macro and function names do not need to be escaped because the parser keeps identifiers in a
// different namespace than function and macro names.
{unescaped: "has", escaped: "has"},
{unescaped: "all", escaped: "all"},
{unescaped: "exists", escaped: "exists"},
{unescaped: "exists_one", escaped: "exists_one"},
{unescaped: "filter", escaped: "filter"},
{unescaped: "size", escaped: "size"},
{unescaped: "contains", escaped: "contains"},
{unescaped: "startsWith", escaped: "startsWith"},
{unescaped: "endsWith", escaped: "endsWith"},
{unescaped: "matches", escaped: "matches"},
{unescaped: "duration", escaped: "duration"},
{unescaped: "timestamp", escaped: "timestamp"},
{unescaped: "getDate", escaped: "getDate"},
{unescaped: "getDayOfMonth", escaped: "getDayOfMonth"},
{unescaped: "getDayOfWeek", escaped: "getDayOfWeek"},
{unescaped: "getFullYear", escaped: "getFullYear"},
{unescaped: "getHours", escaped: "getHours"},
{unescaped: "getMilliseconds", escaped: "getMilliseconds"},
{unescaped: "getMinutes", escaped: "getMinutes"},
{unescaped: "getMonth", escaped: "getMonth"},
{unescaped: "getSeconds", escaped: "getSeconds"},
// we don't escape a single _
{unescaped: "_if", escaped: "_if"},
{unescaped: "_has", escaped: "_has"},
{unescaped: "_int", escaped: "_int"},
{unescaped: "_anything", escaped: "_anything"},
}
for _, tc := range cases {
t.Run(tc.unescaped, func(t *testing.T) {
e := Escape(tc.unescaped)
e, escapable := Escape(tc.unescaped)
if tc.unescapable {
if escapable {
t.Errorf("Expected escapable=false, but got %t", escapable)
}
return
}
if !escapable {
t.Fatalf("Expected escapable=true, but got %t", escapable)
}
if tc.escaped != e {
t.Errorf("Expected %s to escape to %s, but got %s", tc.unescaped, tc.escaped, e)
}
u := Unescape(tc.escaped)
if tc.unescaped != u {
t.Errorf("Expected %s to unescape to %s, but got %s", tc.escaped, tc.unescaped, e)
if !validCelIdent.MatchString(e) {
t.Errorf("Expected %s to escape to a valid CEL identifier, but got %s", tc.unescaped, e)
}
isRootReserved := IsRootReserved(tc.unescaped)
if tc.reservedAtRoot != isRootReserved {
t.Errorf("Expected isRootReserved=%t for %s, but got %t", tc.reservedAtRoot, tc.unescaped, isRootReserved)
u, ok := Unescape(tc.escaped)
if !ok {
t.Fatalf("Expected %s to be escapable, but it was not", tc.escaped)
}
if tc.unescaped != u {
t.Errorf("Expected %s to unescape to %s, but got %s", tc.escaped, tc.unescaped, u)
}
})
}
}
func TestUnescapeMalformed(t *testing.T) {
for _, s := range []string{"__int__extra", "__illegal__"} {
t.Run(s, func(t *testing.T) {
e, ok := Unescape(s)
if ok {
t.Fatalf("Expected %s to be unescapable, but it escaped to: %s", s, e)
}
})
}
}
func TestEscapingFuzz(t *testing.T) {
fuzzer := fuzz.New()
for i := 0; i < 1000; i++ {
var unescaped string
fuzzer.Fuzz(&unescaped)
t.Run(fmt.Sprintf("%d - '%s'", i, unescaped), func(t *testing.T) {
if len(unescaped) == 0 {
return
}
escaped, ok := Escape(unescaped)
if !ok {
return
}
if !validCelIdent.MatchString(escaped) {
t.Errorf("Expected %s to escape to a valid CEL identifier, but got %s", unescaped, escaped)
}
u, ok := Unescape(escaped)
if !ok {
t.Fatalf("Expected %s to be unescapable, but it was not", escaped)
}
if unescaped != u {
t.Errorf("Expected %s to unescape to %s, but got %s", escaped, unescaped, u)
}
})
}
}
var validCelIdent = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)

View File

@ -18,45 +18,61 @@ import (
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
// SchemaDeclTypes constructs a top-down set of DeclType instances whose name is derived from the root
// type name provided on the call, if not set to a custom type.
func SchemaDeclTypes(s *schema.Structural, maybeRootType string) (*DeclType, map[string]*DeclType) {
root := SchemaDeclType(s).MaybeAssignTypeName(maybeRootType)
types := FieldTypeMap(maybeRootType, root)
return root, types
}
// SchemaDeclType returns the cel type name associated with the schema element.
func SchemaDeclType(s *schema.Structural) *DeclType {
// SchemaDeclType converts the structural schema to a CEL declaration, or returns nil if the
// the structural schema should not be exposed in CEL expressions.
// Set isResourceRoot to true for the root of a custom resource or embedded resource.
//
// Schemas with XPreserveUnknownFields not exposed unless they are objects. Array and "maps" schemas
// are not exposed if their items or additionalProperties schemas are not exposed. Object Properties are not exposed
// if their schema is not exposed.
//
// The CEL declaration for objects with XPreserveUnknownFields does not expose unknown fields.
func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType {
if s == nil {
return nil
}
if s.XIntOrString {
// schemas using this extension are not required to have a type, so they must be handled before type lookup
return intOrStringType
}
declType, found := openAPISchemaTypes[s.Type]
if !found {
return nil
// schemas using XIntOrString are not required to have a type.
// intOrStringType represents the x-kubernetes-int-or-string union type in CEL expressions.
// In CEL, the type is represented as dynamic value, which can be thought of as a union type of all types.
// All type checking for XIntOrString is deferred to runtime, so all access to values of this type must
// be guarded with a type check, e.g.:
//
// To require that the string representation be a percentage:
// `type(intOrStringField) == string && intOrStringField.matches(r'(\d+(\.\d+)?%)')`
// To validate requirements on both the int and string representation:
// `type(intOrStringField) == int ? intOrStringField < 5 : double(intOrStringField.replace('%', '')) < 0.5
//
return DynType
}
// We ignore XPreserveUnknownFields since we don't support validation rules on
// data that we don't have schema information for.
if s.XEmbeddedResource {
// 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are always accessible
// to validation rules since this part of the schema is well known and validated when CRDs
// are created and updated.
if isResourceRoot {
// 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are always accessible to validator rules
// at the root of resources, even if not specified in the schema.
// This includes the root of a custom resource and the root of XEmbeddedResource objects.
s = WithTypeAndObjectMeta(s)
}
switch declType.TypeName() {
case ListType.TypeName():
return NewListType(SchemaDeclType(s.Items))
case MapType.TypeName():
switch s.Type {
case "array":
if s.Items != nil {
itemsType := SchemaDeclType(s.Items, s.Items.XEmbeddedResource)
if itemsType != nil {
return NewListType(itemsType)
}
}
return nil
case "object":
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil {
return NewMapType(StringType, SchemaDeclType(s.AdditionalProperties.Structural))
propsType := SchemaDeclType(s.AdditionalProperties.Structural, s.AdditionalProperties.Structural.XEmbeddedResource)
if propsType != nil {
return NewMapType(StringType, propsType)
}
return nil
}
fields := make(map[string]*DeclField, len(s.Properties))
@ -73,23 +89,23 @@ func SchemaDeclType(s *schema.Structural) *DeclType {
enumValues = append(enumValues, e.Object)
}
}
if fieldType := SchemaDeclType(&prop); fieldType != nil {
fields[Escape(name)] = &DeclField{
Name: Escape(name),
Required: required[name],
Type: fieldType,
defaultValue: prop.Default.Object,
enumValues: enumValues, // Enum values are represented as strings in CEL
if fieldType := SchemaDeclType(&prop, prop.XEmbeddedResource); fieldType != nil {
if propName, ok := Escape(name); ok {
fields[propName] = &DeclField{
Name: propName,
Required: required[name],
Type: fieldType,
defaultValue: prop.Default.Object,
enumValues: enumValues, // Enum values are represented as strings in CEL
}
}
}
}
return NewObjectType("object", fields)
case StringType.TypeName():
case "string":
if s.ValueValidation != nil {
switch s.ValueValidation.Format {
case "byte":
return StringType // OpenAPIv3 byte format represents base64 encoded string
case "binary":
return BytesType
case "duration":
return DurationType
@ -97,39 +113,17 @@ func SchemaDeclType(s *schema.Structural) *DeclType {
return TimestampType
}
}
return StringType
case "boolean":
return BoolType
case "number":
return DoubleType
case "integer":
return IntType
}
return declType
return nil
}
var (
openAPISchemaTypes = map[string]*DeclType{
"boolean": BoolType,
"number": DoubleType,
"integer": IntType,
"null": NullType,
"string": StringType,
"date": DateType,
"array": ListType,
"object": MapType,
}
// intOrStringType represents the x-kubernetes-int-or-string union type in CEL expressions.
// In CEL, the type is represented as an object where either the srtVal
// or intVal field is set. In CEL, this allows for typesafe expressions like:
//
// require that the string representation be a percentage:
// `has(intOrStringField.strVal) && intOrStringField.strVal.matches(r'(\d+(\.\d+)?%)')`
// validate requirements on both the int and string representation:
// `has(intOrStringField.intVal) ? intOrStringField.intVal < 5 : double(intOrStringField.strVal.replace('%', '')) < 0.5
//
intOrStringType = NewObjectType("intOrString", map[string]*DeclField{
"strVal": {Name: "strVal", Type: StringType},
"intVal": {Name: "intVal", Type: IntType},
})
)
// TODO: embedded objects should have objectMeta only, and name and generateName are both optional
// WithTypeAndObjectMeta ensures the kind, apiVersion and
// metadata.name and metadata.generateName properties are specified, making a shallow copy of the provided schema if needed.
func WithTypeAndObjectMeta(s *schema.Structural) *schema.Structural {

View File

@ -21,12 +21,13 @@ import (
"github.com/google/cel-go/common/types"
"google.golang.org/protobuf/proto"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
func TestSchemaDeclType(t *testing.T) {
ts := testSchema()
cust := SchemaDeclType(ts)
cust := SchemaDeclType(ts, false)
if cust.TypeName() != "object" {
t.Errorf("incorrect type name, got %v, wanted object", cust.TypeName())
}
@ -82,7 +83,8 @@ func TestSchemaDeclType(t *testing.T) {
func TestSchemaDeclTypes(t *testing.T) {
ts := testSchema()
cust, typeMap := SchemaDeclTypes(ts, "CustomObject")
cust := SchemaDeclType(ts, true).MaybeAssignTypeName("CustomObject")
typeMap := FieldTypeMap("CustomObject", cust)
nested, _ := cust.FindField("nested")
metadata, _ := cust.FindField("metadata")
expectedObjTypeMap := map[string]*DeclType{

View File

@ -24,9 +24,9 @@ import (
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/protobuf/proto"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
@ -306,15 +306,19 @@ func (f *DeclField) EnumValues() []ref.Val {
// NewRuleTypes returns an Open API Schema-based type-system which is CEL compatible.
func NewRuleTypes(kind string,
schema *schema.Structural,
isResourceRoot bool,
res Resolver) (*RuleTypes, error) {
// Note, if the schema indicates that it's actually based on another proto
// then prefer the proto definition. For expressions in the proto, a new field
// annotation will be needed to indicate the expected environment and type of
// the expression.
schemaTypes, err := newSchemaTypeProvider(kind, schema)
schemaTypes, err := newSchemaTypeProvider(kind, schema, isResourceRoot)
if err != nil {
return nil, err
}
if schemaTypes == nil {
return nil, nil
}
return &RuleTypes{
Schema: schema,
ruleSchemaDeclTypes: schemaTypes,
@ -467,7 +471,6 @@ func (rt *RuleTypes) convertToCustomType(dyn *DynValue, declType *DeclType) *Dyn
dyn.SetValue(obj)
return dyn
}
// TODO: handle complex map types which have non-string keys.
fieldType := declType.ElemType
for _, f := range v.fieldMap {
f.Ref = rt.convertToCustomType(f.Ref, fieldType)
@ -485,8 +488,12 @@ func (rt *RuleTypes) convertToCustomType(dyn *DynValue, declType *DeclType) *Dyn
}
}
func newSchemaTypeProvider(kind string, schema *schema.Structural) (*schemaTypeProvider, error) {
root := SchemaDeclType(schema).MaybeAssignTypeName(kind)
func newSchemaTypeProvider(kind string, schema *schema.Structural, isResourceRoot bool) (*schemaTypeProvider, error) {
delType := SchemaDeclType(schema, isResourceRoot)
if delType == nil {
return nil, nil
}
root := delType.MaybeAssignTypeName(kind)
types := FieldTypeMap(kind, root)
return &schemaTypeProvider{
root: root,

View File

@ -67,7 +67,7 @@ func TestTypes_MapType(t *testing.T) {
func TestTypes_RuleTypesFieldMapping(t *testing.T) {
stdEnv, _ := cel.NewEnv()
reg := NewRegistry(stdEnv)
rt, err := NewRuleTypes("CustomObject", testSchema(), reg)
rt, err := NewRuleTypes("CustomObject", testSchema(), true, reg)
if err != nil {
t.Fatal(err)
}

View File

@ -205,8 +205,6 @@ func (sv *structValue) ConvertToNative(typeDesc reflect.Type) (interface{}, erro
return nil, fmt.Errorf("type conversion error from object to '%v'", typeDesc)
}
// TODO: Special case handling for protobuf Struct and Any if needed
// Unwrap pointers, but track their use.
isPtr := false
if typeDesc.Kind() == reflect.Ptr {