From 220312c81791346dea23d3eb6081f879d20b26ab Mon Sep 17 00:00:00 2001 From: Tristan Swadell Date: Thu, 24 Apr 2025 15:05:49 -0700 Subject: [PATCH] Support for CelEnvironment to YAML PiperOrigin-RevId: 751153106 --- .../src/main/java/dev/cel/bundle/BUILD.bazel | 1 + .../java/dev/cel/bundle/CelEnvironment.java | 2 +- .../bundle/CelEnvironmentYamlSerializer.java | 146 ++++++++++++++++++ .../src/test/java/dev/cel/bundle/BUILD.bazel | 1 + .../CelEnvironmentYamlSerializerTest.java | 124 +++++++++++++++ testing/environment/BUILD.bazel | 5 + .../test/resources/environment/BUILD.bazel | 5 + .../test/resources/environment/dump_env.yaml | 63 ++++++++ 8 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java create mode 100644 bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java create mode 100644 testing/src/test/resources/environment/dump_env.yaml diff --git a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel index ed401a042..81500b0c0 100644 --- a/bundle/src/main/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/main/java/dev/cel/bundle/BUILD.bazel @@ -89,6 +89,7 @@ java_library( name = "environment_yaml_parser", srcs = [ "CelEnvironmentYamlParser.java", + "CelEnvironmentYamlSerializer.java", ], tags = [ ], diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java index 1c1c05ee1..a09391a0b 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -154,7 +154,7 @@ public static Builder newBuilder() { .setVariables(ImmutableSet.of()) .setFunctions(ImmutableSet.of()); } - + /** Extends the provided {@link CelCompiler} environment with this configuration. */ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) throws CelEnvironmentException { diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java new file mode 100644 index 000000000..f070fc613 --- /dev/null +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java @@ -0,0 +1,146 @@ +// 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 dev.cel.bundle; + +import com.google.common.collect.ImmutableMap; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.representer.Represent; +import org.yaml.snakeyaml.representer.Representer; + +/** Serializes a CelEnvironment into a YAML file. */ +public final class CelEnvironmentYamlSerializer extends Representer { + + private static DumperOptions initDumperOptions() { + DumperOptions options = new DumperOptions(); + options.setIndent(2); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + return options; + } + + private static final DumperOptions YAML_OPTIONS = initDumperOptions(); + + private static final CelEnvironmentYamlSerializer INSTANCE = new CelEnvironmentYamlSerializer(); + + private CelEnvironmentYamlSerializer() { + super(YAML_OPTIONS); + this.multiRepresenters.put(CelEnvironment.class, new RepresentCelEnvironment()); + this.multiRepresenters.put(CelEnvironment.VariableDecl.class, new RepresentVariableDecl()); + this.multiRepresenters.put(CelEnvironment.FunctionDecl.class, new RepresentFunctionDecl()); + this.multiRepresenters.put(CelEnvironment.OverloadDecl.class, new RepresentOverloadDecl()); + this.multiRepresenters.put(CelEnvironment.TypeDecl.class, new RepresentTypeDecl()); + this.multiRepresenters.put( + CelEnvironment.ExtensionConfig.class, new RepresentExtensionConfig()); + } + + public static String toYaml(CelEnvironment environment) { + // Yaml is not thread-safe, so we create a new instance for each serialization. + Yaml yaml = new Yaml(INSTANCE, YAML_OPTIONS); + return yaml.dump(environment); + } + + private final class RepresentCelEnvironment implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment environment = (CelEnvironment) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("name", environment.name()); + if (!environment.description().isEmpty()) { + configMap.put("description", environment.description()); + } + if (!environment.container().isEmpty()) { + configMap.put("container", environment.container()); + } + if (!environment.extensions().isEmpty()) { + configMap.put("extensions", environment.extensions().asList()); + } + if (!environment.variables().isEmpty()) { + configMap.put("variables", environment.variables().asList()); + } + if (!environment.functions().isEmpty()) { + configMap.put("functions", environment.functions().asList()); + } + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentExtensionConfig implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment.ExtensionConfig extension = (CelEnvironment.ExtensionConfig) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("name", extension.name()); + if (extension.version() > 0 && extension.version() != Integer.MAX_VALUE) { + configMap.put("version", extension.version()); + } + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentVariableDecl implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment.VariableDecl variable = (CelEnvironment.VariableDecl) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("name", variable.name()).put("type_name", variable.type().name()); + if (!variable.type().params().isEmpty()) { + configMap.put("params", variable.type().params()); + } + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentFunctionDecl implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment.FunctionDecl function = (CelEnvironment.FunctionDecl) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("name", function.name()).put("overloads", function.overloads().asList()); + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentOverloadDecl implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment.OverloadDecl overload = (CelEnvironment.OverloadDecl) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("id", overload.id()); + if (overload.target().isPresent()) { + configMap.put("target", overload.target().get()); + } + configMap.put("args", overload.arguments()).put("return", overload.returnType()); + return represent(configMap.buildOrThrow()); + } + } + + private final class RepresentTypeDecl implements Represent { + @Override + public Node representData(Object data) { + CelEnvironment.TypeDecl type = (CelEnvironment.TypeDecl) data; + ImmutableMap.Builder configMap = new ImmutableMap.Builder<>(); + configMap.put("type_name", type.name()); + if (!type.params().isEmpty()) { + configMap.put("params", type.params()); + } + if (type.isTypeParam()) { + configMap.put("is_type_param", type.isTypeParam()); + } + return represent(configMap.buildOrThrow()); + } + } +} diff --git a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel index ad34a3f16..b98f26408 100644 --- a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel @@ -10,6 +10,7 @@ java_library( testonly = True, srcs = glob(["*Test.java"]), resources = [ + "//testing/environment:dump_env", "//testing/environment:extended_env", ], deps = [ diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java new file mode 100644 index 000000000..2e7cfe9ab --- /dev/null +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlSerializerTest.java @@ -0,0 +1,124 @@ +// 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 dev.cel.bundle; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import dev.cel.bundle.CelEnvironment.ExtensionConfig; +import dev.cel.bundle.CelEnvironment.FunctionDecl; +import dev.cel.bundle.CelEnvironment.OverloadDecl; +import dev.cel.bundle.CelEnvironment.TypeDecl; +import dev.cel.bundle.CelEnvironment.VariableDecl; +import java.io.IOException; +import java.net.URL; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CelEnvironmentYamlSerializerTest { + + @Test + public void toYaml_success() throws Exception { + CelEnvironment environment = + CelEnvironment.newBuilder() + .setName("dump_env") + .setDescription("dump_env description") + .setContainer("test.container") + .addExtensions( + ImmutableSet.of( + ExtensionConfig.of("bindings"), + ExtensionConfig.of("encoders"), + ExtensionConfig.of("lists"), + ExtensionConfig.of("math"), + ExtensionConfig.of("optional"), + ExtensionConfig.of("protos"), + ExtensionConfig.of("sets"), + ExtensionConfig.of("strings", 1))) + .setVariables( + ImmutableSet.of( + VariableDecl.create( + "request", TypeDecl.create("google.rpc.context.AttributeContext.Request")), + VariableDecl.create( + "map_var", + TypeDecl.newBuilder() + .setName("map") + .addParams(TypeDecl.create("string")) + .addParams(TypeDecl.create("string")) + .build()))) + .setFunctions( + ImmutableSet.of( + FunctionDecl.create( + "getOrDefault", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("getOrDefault_key_value") + .setTarget( + TypeDecl.newBuilder() + .setName("map") + .addParams( + TypeDecl.newBuilder() + .setName("K") + .setIsTypeParam(true) + .build()) + .addParams( + TypeDecl.newBuilder() + .setName("V") + .setIsTypeParam(true) + .build()) + .build()) + .setArguments( + ImmutableList.of( + TypeDecl.newBuilder() + .setName("K") + .setIsTypeParam(true) + .build(), + TypeDecl.newBuilder() + .setName("V") + .setIsTypeParam(true) + .build())) + .setReturnType( + TypeDecl.newBuilder().setName("V").setIsTypeParam(true).build()) + .build())), + FunctionDecl.create( + "coalesce", + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("coalesce_null_int") + .setTarget(TypeDecl.create("google.protobuf.Int64Value")) + .setArguments(ImmutableList.of(TypeDecl.create("int"))) + .setReturnType(TypeDecl.create("int")) + .build())))) + .build(); + + String yamlOutput = CelEnvironmentYamlSerializer.toYaml(environment); + try { + String yamlFileContent = readFile("environment/dump_env.yaml"); + assertThat(yamlFileContent).endsWith(yamlOutput); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static String readFile(String path) throws IOException { + URL url = Resources.getResource(Ascii.toLowerCase(path)); + return Resources.toString(url, UTF_8); + } +} diff --git a/testing/environment/BUILD.bazel b/testing/environment/BUILD.bazel index d21ce77d3..d28149c8d 100644 --- a/testing/environment/BUILD.bazel +++ b/testing/environment/BUILD.bazel @@ -4,6 +4,11 @@ package( default_visibility = ["//:internal"], ) +alias( + name = "dump_env", + actual = "//testing/src/test/resources/environment:dump_env", +) + alias( name = "extended_env", actual = "//testing/src/test/resources/environment:extended_env", diff --git a/testing/src/test/resources/environment/BUILD.bazel b/testing/src/test/resources/environment/BUILD.bazel index dfae7e3d2..fc08800d0 100644 --- a/testing/src/test/resources/environment/BUILD.bazel +++ b/testing/src/test/resources/environment/BUILD.bazel @@ -8,6 +8,11 @@ package( ], ) +filegroup( + name = "dump_env", + srcs = ["dump_env.yaml"], +) + filegroup( name = "extended_env", srcs = ["extended_env.yaml"], diff --git a/testing/src/test/resources/environment/dump_env.yaml b/testing/src/test/resources/environment/dump_env.yaml new file mode 100644 index 000000000..0a1509ebb --- /dev/null +++ b/testing/src/test/resources/environment/dump_env.yaml @@ -0,0 +1,63 @@ +# 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. + +name: dump_env +description: dump_env description +container: test.container +extensions: +- name: bindings +- name: encoders +- name: lists +- name: math +- name: optional +- name: protos +- name: sets +- name: strings + version: 1 +variables: +- name: request + type_name: google.rpc.context.AttributeContext.Request +- name: map_var + type_name: map + params: + - type_name: string + - type_name: string +functions: +- name: getOrDefault + overloads: + - id: getOrDefault_key_value + target: + type_name: map + params: + - type_name: K + is_type_param: true + - type_name: V + is_type_param: true + args: + - type_name: K + is_type_param: true + - type_name: V + is_type_param: true + return: + type_name: V + is_type_param: true +- name: coalesce + overloads: + - id: coalesce_null_int + target: + type_name: google.protobuf.Int64Value + args: + - type_name: int + return: + type_name: int