Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bundle/src/main/java/dev/cel/bundle/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ java_library(
name = "environment_yaml_parser",
srcs = [
"CelEnvironmentYamlParser.java",
"CelEnvironmentYamlSerializer.java",
],
tags = [
],
Expand Down
2 changes: 1 addition & 1 deletion bundle/src/main/java/dev/cel/bundle/CelEnvironment.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
146 changes: 146 additions & 0 deletions bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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());
}
}
}
1 change: 1 addition & 0 deletions bundle/src/test/java/dev/cel/bundle/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ java_library(
testonly = True,
srcs = glob(["*Test.java"]),
resources = [
"//testing/environment:dump_env",
"//testing/environment:extended_env",
],
deps = [
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
5 changes: 5 additions & 0 deletions testing/environment/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions testing/src/test/resources/environment/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ package(
],
)

filegroup(
name = "dump_env",
srcs = ["dump_env.yaml"],
)

filegroup(
name = "extended_env",
srcs = ["extended_env.yaml"],
Expand Down
63 changes: 63 additions & 0 deletions testing/src/test/resources/environment/dump_env.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading