Repository: google/cel-go
Pull request #1141: Expose extension option factory as a public method
Author: login: aakash070
Created: 2025-03-10T11:14:57Z | Merged: 2025-03-11T17:48:52Z

Refactor code to expose extension option factory which can be used to convert a serialized extension config value to a CEL environment option 



Diff:
diff --git a/common/env/env.go b/common/env/env.go
index aa7b066c5..07294c696 100644
--- a/common/env/env.go
+++ b/common/env/env.go
@@ -115,7 +115,7 @@ func (c *Config) AddVariableDecls(vars ...*decls.VariableDecl) *Config {
 		if v == nil {
 			continue
 		}
-		convVars[i] = NewVariable(v.Name(), serializeTypeDesc(v.Type()))
+		convVars[i] = NewVariable(v.Name(), SerializeTypeDesc(v.Type()))
 	}
 	return c.AddVariables(convVars...)
 }
@@ -146,9 +146,9 @@ func (c *Config) AddFunctionDecls(funcs ...*decls.FunctionDecl) *Config {
 			overloadID := o.ID()
 			args := make([]*TypeDesc, 0, len(o.ArgTypes()))
 			for _, a := range o.ArgTypes() {
-				args = append(args, serializeTypeDesc(a))
+				args = append(args, SerializeTypeDesc(a))
 			}
-			ret := serializeTypeDesc(o.ResultType())
+			ret := SerializeTypeDesc(o.ResultType())
 			if o.IsMemberFunction() {
 				overloads = append(overloads, NewMemberOverload(overloadID, args[0], args[1:], ret))
 			} else {
@@ -836,7 +836,8 @@ func (td *TypeDesc) AsCELType(tp types.Provider) (*types.Type, error) {
 	}
 }
 
-func serializeTypeDesc(t *types.Type) *TypeDesc {
+// SerializeTypeDesc converts *types.Type to a serialized format TypeDesc
+func SerializeTypeDesc(t *types.Type) *TypeDesc {
 	typeName := t.TypeName()
 	if t.Kind() == types.TypeParamKind {
 		return NewTypeParam(typeName)
@@ -848,7 +849,7 @@ func serializeTypeDesc(t *types.Type) *TypeDesc {
 	}
 	var params []*TypeDesc
 	for _, p := range t.Parameters() {
-		params = append(params, serializeTypeDesc(p))
+		params = append(params, SerializeTypeDesc(p))
 	}
 	return NewTypeDesc(typeName, params...)
 }
diff --git a/ext/BUILD.bazel b/ext/BUILD.bazel
index b764fa1f5..62863c17a 100644
--- a/ext/BUILD.bazel
+++ b/ext/BUILD.bazel
@@ -10,6 +10,7 @@ go_library(
         "bindings.go",
         "comprehensions.go",
         "encoders.go",
+        "extension_option_factory.go",
         "formatting.go",
         "guards.go",
         "lists.go",
@@ -26,6 +27,7 @@ go_library(
         "//checker:go_default_library",
         "//common/ast:go_default_library",
         "//common/decls:go_default_library",
+        "//common/env:go_default_library",
         "//common/overloads:go_default_library",
         "//common/operators:go_default_library",
         "//common/types:go_default_library",
@@ -48,7 +50,8 @@ go_test(
     srcs = [
         "bindings_test.go",
         "comprehensions_test.go",
-        "encoders_test.go",        
+        "encoders_test.go",
+        "extension_option_factory_test.go",
         "lists_test.go",
         "math_test.go",
         "native_test.go",
@@ -62,6 +65,7 @@ go_test(
     deps = [
         "//cel:go_default_library",
         "//checker:go_default_library",
+        "//common/env:go_default_library",
         "//common/types:go_default_library",
         "//common/types/ref:go_default_library",
         "//common/types/traits:go_default_library",
diff --git a/ext/extension_option_factory.go b/ext/extension_option_factory.go
new file mode 100644
index 000000000..4906227a5
--- /dev/null
+++ b/ext/extension_option_factory.go
@@ -0,0 +1,72 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ext
+
+import (
+	"fmt"
+
+	"github.com/google/cel-go/cel"
+	"github.com/google/cel-go/common/env"
+)
+
+// ExtensionOptionFactory converts an ExtensionConfig value to a CEL environment option.
+func ExtensionOptionFactory(configElement any) (cel.EnvOption, bool) {
+	ext, isExtension := configElement.(*env.Extension)
+	if !isExtension {
+		return nil, false
+	}
+	fac, found := extFactories[ext.Name]
+	if !found {
+		return nil, false
+	}
+	// If the version is 'latest', set the version value to the max uint.
+	ver, err := ext.VersionNumber()
+	if err != nil {
+		return func(*cel.Env) (*cel.Env, error) {
+			return nil, fmt.Errorf("invalid extension version: %s - %s", ext.Name, ext.Version)
+		}, true
+	}
+	return fac(ver), true
+}
+
+// extensionFactory accepts a version and produces a CEL environment associated with the versioned extension.
+type extensionFactory func(uint32) cel.EnvOption
+
+var extFactories = map[string]extensionFactory{
+	"bindings": func(version uint32) cel.EnvOption {
+		return Bindings(BindingsVersion(version))
+	},
+	"encoders": func(version uint32) cel.EnvOption {
+		return Encoders(EncodersVersion(version))
+	},
+	"lists": func(version uint32) cel.EnvOption {
+		return Lists(ListsVersion(version))
+	},
+	"math": func(version uint32) cel.EnvOption {
+		return Math(MathVersion(version))
+	},
+	"protos": func(version uint32) cel.EnvOption {
+		return Protos(ProtosVersion(version))
+	},
+	"sets": func(version uint32) cel.EnvOption {
+		return Sets(SetsVersion(version))
+	},
+	"strings": func(version uint32) cel.EnvOption {
+		return Strings(StringsVersion(version))
+	},
+	"two-var-comprehensions": func(version uint32) cel.EnvOption {
+		return TwoVarComprehensions(TwoVarComprehensionsVersion(version))
+	},
+}
diff --git a/ext/extension_option_factory_test.go b/ext/extension_option_factory_test.go
new file mode 100644
index 000000000..f721bb6bf
--- /dev/null
+++ b/ext/extension_option_factory_test.go
@@ -0,0 +1,67 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ext
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/google/cel-go/cel"
+	"github.com/google/cel-go/common/env"
+)
+
+func TestExtensionOptionFactoryInvalidExtension(t *testing.T) {
+	invalidExtension := "invalid extension"
+	_, validExtension := ExtensionOptionFactory(invalidExtension)
+	if validExtension {
+		t.Fatalf("ExtensionOptionFactory(%s) returned valid extension for invalid input", invalidExtension)
+	}
+}
+
+func TestExtensionOptionFactoryInvalidExtensionName(t *testing.T) {
+	e := &env.Extension{Name: "invalid extension name"}
+	_, validExtension := ExtensionOptionFactory(e)
+	if validExtension {
+		t.Fatalf("ExtensionOptionFactory(%s) returned valid extension for invalid extension name", e.Name)
+	}
+}
+
+func TestExtensionOptionFactoryInvalidExtensionVersion(t *testing.T) {
+	e := &env.Extension{Name: "bindings", Version: "invalid version"}
+	opt, validExtension := ExtensionOptionFactory(e)
+	if !validExtension {
+		t.Fatalf("ExtensionOptionFactory(%s) returned invalid extension", e.Name)
+	}
+	_, err := cel.NewCustomEnv(opt)
+	if err == nil || err.Error() != fmt.Sprintf("invalid extension version: %s - %s", e.Name, e.Version) {
+		t.Fatalf("ExtensionOptionFactory(%s) returned invalid extension version", e.Name)
+	}
+}
+
+func TestExtensionOptionFactoryValidBindingsExtension(t *testing.T) {
+	e := &env.Extension{Name: "bindings", Version: "latest"}
+	opt, validExtension := ExtensionOptionFactory(e)
+	if !validExtension {
+		t.Fatalf("ExtensionOptionFactory(%s) returned invalid extension", e.Name)
+	}
+	en, err := cel.NewCustomEnv(opt)
+	if err != nil {
+		t.Fatalf("ExtensionOptionFactory(%s) returned invalid extension", e.Name)
+	}
+	cfg, err := en.ToConfig("test config")
+	if len(cfg.Extensions) != 1 || cfg.Extensions[0].Name != "cel.lib.ext.cel.bindings" || cfg.Extensions[0].Version != "latest" {
+		t.Fatalf("ExtensionOptionFactory(%s) returned invalid extension", e.Name)
+	}
+}
diff --git a/policy/config.go b/policy/config.go
index 12fbe44a7..02243922b 100644
--- a/policy/config.go
+++ b/policy/config.go
@@ -15,8 +15,6 @@
 package policy
 
 import (
-	"fmt"
-
 	"github.com/google/cel-go/cel"
 	"github.com/google/cel-go/common/env"
 	"github.com/google/cel-go/ext"
@@ -28,55 +26,5 @@ import (
 // a set of configuration ConfigOptionFactory values to handle extensions and other config features
 // which may be defined outside of the `cel` package.
 func FromConfig(config *env.Config) cel.EnvOption {
-	return cel.FromConfig(config, extensionOptionFactory)
-}
-
-// extensionOptionFactory converts an ExtensionConfig value to a CEL environment option.
-func extensionOptionFactory(configElement any) (cel.EnvOption, bool) {
-	ext, isExtension := configElement.(*env.Extension)
-	if !isExtension {
-		return nil, false
-	}
-	fac, found := extFactories[ext.Name]
-	if !found {
-		return nil, false
-	}
-	// If the version is 'latest', set the version value to the max uint.
-	ver, err := ext.VersionNumber()
-	if err != nil {
-		return func(*cel.Env) (*cel.Env, error) {
-			return nil, fmt.Errorf("invalid extension version: %s - %s", ext.Name, ext.Version)
-		}, true
-	}
-	return fac(ver), true
-}
-
-// extensionFactory accepts a version and produces a CEL environment associated with the versioned extension.
-type extensionFactory func(uint32) cel.EnvOption
-
-var extFactories = map[string]extensionFactory{
-	"bindings": func(version uint32) cel.EnvOption {
-		return ext.Bindings(ext.BindingsVersion(version))
-	},
-	"encoders": func(version uint32) cel.EnvOption {
-		return ext.Encoders(ext.EncodersVersion(version))
-	},
-	"lists": func(version uint32) cel.EnvOption {
-		return ext.Lists(ext.ListsVersion(version))
-	},
-	"math": func(version uint32) cel.EnvOption {
-		return ext.Math(ext.MathVersion(version))
-	},
-	"protos": func(version uint32) cel.EnvOption {
-		return ext.Protos(ext.ProtosVersion(version))
-	},
-	"sets": func(version uint32) cel.EnvOption {
-		return ext.Sets(ext.SetsVersion(version))
-	},
-	"strings": func(version uint32) cel.EnvOption {
-		return ext.Strings(ext.StringsVersion(version))
-	},
-	"two-var-comprehensions": func(version uint32) cel.EnvOption {
-		return ext.TwoVarComprehensions(ext.TwoVarComprehensionsVersion(version))
-	},
+	return cel.FromConfig(config, ext.ExtensionOptionFactory)
 }


Reviews:
user: login: TristonianJones
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T22:27:40Z
user: login: TristonianJones
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T22:32:21Z
user: login: TristonianJones
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T22:34:20Z
user: login: TristonianJones
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T22:35:14Z
user: login: aakash070
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T11:21:45Z
user: login: TristonianJones
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-11T17:18:00Z

Review comments:
diff_hunk: @@ -649,6 +649,21 @@ func ProtoConstantAsVal(c *celpb.Constant) (ref.Val, error) {
 	return nil, fmt.Errorf("unsupported constant kind: %v", c.GetConstantKind())
 }
 
+// ProtoStructValueAsVal converts a canonical structpb.Value protobuf to a CEL-native ref.Val.
+func ProtoStructValueAsVal(c *structpb.Value) (ref.Val, error) {
path: common/ast/conversion.go
user: login: TristonianJones
body: You should just be able to use `types.DefaultTypeAdapter.NativeToValue()` and have this work correctly. Please revert.
created_at: 2025-03-10T22:27:40Z
author_association: COLLABORATOR
diff_hunk: @@ -141,25 +141,33 @@ func (c *Config) AddFunctionDecls(funcs ...*decls.FunctionDecl) *Config {
 		if fn == nil {
 			continue
 		}
-		overloads := make([]*Overload, 0, len(fn.OverloadDecls()))
-		for _, o := range fn.OverloadDecls() {
-			overloadID := o.ID()
-			args := make([]*TypeDesc, 0, len(o.ArgTypes()))
-			for _, a := range o.ArgTypes() {
-				args = append(args, serializeTypeDesc(a))
-			}
-			ret := serializeTypeDesc(o.ResultType())
-			if o.IsMemberFunction() {
-				overloads = append(overloads, NewMemberOverload(overloadID, args[0], args[1:], ret))
-			} else {
-				overloads = append(overloads, NewOverload(overloadID, args, ret))
-			}
-		}
-		convFuncs[i] = NewFunction(fn.Name(), overloads...)
+		convFuncs[i] = FunctionDeclToFunction(fn)
 	}
 	return c.AddFunctions(convFuncs...)
 }
 
+// FunctionDeclToFunction converts a protobuf CEL FunctionDecl to a serializable Function definition
+// which can be added to the config.
+//
+// FunctionDecl inputs are expected to be well-formed.
+func FunctionDeclToFunction(fn *decls.FunctionDecl) *Function {
path: common/env/env.go
user: login: TristonianJones
body: Where this ends up getting used in a later change, prefer converting directly to an `env.Function` rather than a `decls.FunctionDecl` first. Please revert.
created_at: 2025-03-10T22:32:21Z
author_association: COLLABORATOR
diff_hunk: @@ -309,6 +309,32 @@ func MemberOverload(overloadID string, args []*Type, resultType *Type, opts ...O
 	return decls.MemberOverload(overloadID, args, resultType, opts...)
 }
 
+// FunctionDeclOverloadToFunctionOpt converts a list of protobuf CEL FunctionDecl_Overload instances
+// to a list of FunctionOpt instances.
+func FunctionDeclOverloadToFunctionOpt(overloads []*celpb.Decl_FunctionDecl_Overload) ([]FunctionOpt, error) {
path: cel/decls.go
user: login: TristonianJones
body: I believe the desired state would be to convert to a `env.Function` directly rather than to a `decls.FunctionDecl` first.
created_at: 2025-03-10T22:34:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,72 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ext
path: ext/extension_option_factory.go
user: login: TristonianJones
body: Please add test cases that showcase the desired behavior with a serializable environment, see `cel/env_test.go` for reference.
created_at: 2025-03-10T22:35:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,72 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ext
path: ext/extension_option_factory.go
user: login: aakash070
body: Added a test file with `TestExtensionOptionFactoryValidBindingsExtension` which tests the functionality on a manually instantiated serializable env.Extension object. Will cover the behavior for a serialized environment file along with tests for compiler tool.
created_at: 2025-03-11T11:21:40Z
author_association: COLLABORATOR

PR comments:
user: login: TristonianJones
created_at: 2025-03-10T22:13:36Z
body: /gcbrun
author_association: COLLABORATOR
user: login: TristonianJones
created_at: 2025-03-11T17:15:09Z
body: /gcbrun
author_association: COLLABORATOR
user: login: github-advanced-security[bot]
created_at: 2025-03-11T17:18:54Z
body: This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on [this overview](/google/cel-go/security/code-scanning?query=pr%3A1141+is%3Aopen). Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out [the documentation](https://docs.github.com/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning). 
author_association: NONE