From cc1fd3c59411bb62d8caad740a491d895f80a06a Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 14 Mar 2025 07:57:38 -0700 Subject: [PATCH] Implement codegen for CelLiteDescriptor PiperOrigin-RevId: 736863075 --- WORKSPACE | 4 +- common/internal/BUILD.bazel | 6 +- .../java/dev/cel/common/internal/BUILD.bazel | 1 + .../internal/ProtoJavaQualifiedNames.java | 14 +- java_lite_proto_cel_library.bzl | 99 +++++ protobuf/BUILD.bazel | 17 + .../main/java/dev/cel/protobuf/BUILD.bazel | 77 ++++ .../dev/cel/protobuf/CelLiteDescriptor.java | 410 ++++++++++++++++++ .../protobuf/CelLiteDescriptorGenerator.java | 156 +++++++ .../java/dev/cel/protobuf/DebugPrinter.java | 36 ++ .../dev/cel/protobuf/JavaFileGenerator.java | 96 ++++ .../protobuf/ProtoDescriptorCollector.java | 129 ++++++ .../cel_lite_descriptor_template.txt | 70 +++ .../test/java/dev/cel/protobuf/BUILD.bazel | 36 ++ .../cel/protobuf/CelLiteDescriptorTest.java | 307 +++++++++++++ 15 files changed, 1453 insertions(+), 5 deletions(-) create mode 100644 java_lite_proto_cel_library.bzl create mode 100644 protobuf/BUILD.bazel create mode 100644 protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel create mode 100644 protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java create mode 100644 protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptorGenerator.java create mode 100644 protobuf/src/main/java/dev/cel/protobuf/DebugPrinter.java create mode 100644 protobuf/src/main/java/dev/cel/protobuf/JavaFileGenerator.java create mode 100644 protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java create mode 100644 protobuf/src/main/java/dev/cel/protobuf/templates/cel_lite_descriptor_template.txt create mode 100644 protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel create mode 100644 protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java diff --git a/WORKSPACE b/WORKSPACE index 15fdf8f20..1c1e03c59 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -242,11 +242,11 @@ http_archive( ) # cel-spec api/expr canonical protos -CEL_SPEC_VERSION = "0.20.0" +CEL_SPEC_VERSION = "0.22.1" http_archive( name = "cel_spec", - sha256 = "9f4acb83116f68af8a6b6acf700561a22a1bd8a9ad2f49bf642b7f9b8f285043", + sha256 = "1f1ad32bce5d31cf82e9c8f40685b1902de3ab07c78403601e7a43c3fb4de9a6", strip_prefix = "cel-spec-" + CEL_SPEC_VERSION, urls = [ "https://github.com/google/cel-spec/archive/" + diff --git a/common/internal/BUILD.bazel b/common/internal/BUILD.bazel index 63bff51d9..d93121150 100644 --- a/common/internal/BUILD.bazel +++ b/common/internal/BUILD.bazel @@ -95,6 +95,10 @@ java_library( cel_android_library( name = "internal_android", - visibility = ["//:android_allow_list"], exports = ["//common/src/main/java/dev/cel/common/internal:internal_android"], ) + +java_library( + name = "proto_java_qualified_names", + exports = ["//common/src/main/java/dev/cel/common/internal:proto_java_qualified_names"], +) diff --git a/common/src/main/java/dev/cel/common/internal/BUILD.bazel b/common/src/main/java/dev/cel/common/internal/BUILD.bazel index bca6ec303..6aa3c4de8 100644 --- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/internal/BUILD.bazel @@ -309,6 +309,7 @@ java_library( tags = [ ], deps = [ + "//common/annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], diff --git a/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java b/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java index 9c2ba049e..a16abb8fc 100644 --- a/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java +++ b/common/src/main/java/dev/cel/common/internal/ProtoJavaQualifiedNames.java @@ -24,10 +24,16 @@ import com.google.protobuf.Descriptors.FileDescriptor; import com.google.protobuf.Descriptors.GenericDescriptor; import com.google.protobuf.Descriptors.ServiceDescriptor; +import dev.cel.common.annotations.Internal; import java.util.ArrayDeque; -/** Helper class for constructing a fully qualified Java class name from a protobuf descriptor. */ -final class ProtoJavaQualifiedNames { +/** + * Helper class for constructing a fully qualified Java class name from a protobuf descriptor. * * + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class ProtoJavaQualifiedNames { // Controls how many times we should recursively inspect a nested message for building fully // qualified java class name before aborting. private static final int SAFE_RECURSE_LIMIT = 50; @@ -45,6 +51,10 @@ public static String getFullyQualifiedJavaClassName(Descriptor descriptor) { return getFullyQualifiedJavaClassNameImpl(descriptor); } + public static String getFullyQualifiedJavaClassName(EnumDescriptor descriptor) { + return getFullyQualifiedJavaClassNameImpl(descriptor); + } + private static String getFullyQualifiedJavaClassNameImpl(GenericDescriptor descriptor) { StringBuilder fullClassName = new StringBuilder(); diff --git a/java_lite_proto_cel_library.bzl b/java_lite_proto_cel_library.bzl new file mode 100644 index 000000000..2f7f6a876 --- /dev/null +++ b/java_lite_proto_cel_library.bzl @@ -0,0 +1,99 @@ +# 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. + +"""Starlark rule for generating descriptors that is compatible with Protolite Messages.""" + +load("@rules_java//java:defs.bzl", "java_library") +load("@rules_proto//proto:defs.bzl", "proto_descriptor_set") +load("//publish:cel_version.bzl", "CEL_VERSION") + +def java_lite_proto_cel_library( + name, + java_descriptor_class_prefix, + deps, + debug = False): + """Generates a CelLiteDescriptor + + Args: + name: name of this target. + java_descriptor_class_prefix: Prefix name for the generated descriptor java class (ex: 'TestAllTypes' generates 'TestAllTypesCelLiteDescriptor.java'). + deps: Name of the proto_library target. Only a single proto_library is supported at this time. + debug: (optional) If true, prints additional information during codegen for debugging purposes. + """ + if not name: + fail("You must provide a name.") + + if not java_descriptor_class_prefix: + fail("You must provide a descriptor_class_prefix.") + + if not deps: + fail("You must provide a proto_library dependency.") + + if len(deps) > 1: + fail("You must provide only one proto_library dependency.") + + _generate_cel_lite_descriptor_class( + name, + java_descriptor_class_prefix + "CelLiteDescriptor", + deps[0], + debug, + ) + + descriptor_codegen_deps = [ + "//protobuf:cel_lite_descriptor", + ] + + java_library( + name = name, + srcs = [":" + name + "_cel_lite_descriptor"], + deps = deps + descriptor_codegen_deps, + ) + +def _generate_cel_lite_descriptor_class( + name, + descriptor_class_name, + proto_src, + debug): + outfile = "%s.java" % descriptor_class_name + + transitive_descriptor_set_name = "%s_transitive_descriptor_set" % name + proto_descriptor_set( + name = transitive_descriptor_set_name, + deps = [proto_src], + ) + + direct_descriptor_set_name = proto_src + + debug_flag = "--debug" if debug else "" + + cmd = ( + "$(location //protobuf:cel_lite_descriptor_generator) " + + "--descriptor $(location %s) " % direct_descriptor_set_name + + "--transitive_descriptor_set $(location %s) " % transitive_descriptor_set_name + + "--descriptor_class_name %s " % descriptor_class_name + + "--out $(location %s) " % outfile + + "--version %s " % CEL_VERSION + + debug_flag + ) + + native.genrule( + name = name + "_cel_lite_descriptor", + srcs = [ + transitive_descriptor_set_name, + direct_descriptor_set_name, + ], + cmd = cmd, + outs = [outfile], + tools = ["//protobuf:cel_lite_descriptor_generator"], + ) diff --git a/protobuf/BUILD.bazel b/protobuf/BUILD.bazel new file mode 100644 index 000000000..8674d578b --- /dev/null +++ b/protobuf/BUILD.bazel @@ -0,0 +1,17 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//:internal"], # TODO: Expose when ready +) + +java_library( + name = "cel_lite_descriptor", + exports = ["//protobuf/src/main/java/dev/cel/protobuf:cel_lite_descriptor"], +) + +alias( + name = "cel_lite_descriptor_generator", + actual = "//protobuf/src/main/java/dev/cel/protobuf:cel_lite_descriptor_generator", + visibility = ["//:internal"], +) diff --git a/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel b/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel new file mode 100644 index 000000000..9df0bf2fe --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/BUILD.bazel @@ -0,0 +1,77 @@ +load("@rules_java//java:defs.bzl", "java_binary", "java_library") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//protobuf:__pkg__"], +) + +filegroup( + name = "cel_lite_descriptor_template_file", + srcs = ["templates/cel_lite_descriptor_template.txt"], + visibility = ["//visibility:private"], +) + +java_binary( + name = "cel_lite_descriptor_generator", + srcs = ["CelLiteDescriptorGenerator.java"], + main_class = "dev.cel.protobuf.CelLiteDescriptorGenerator", + deps = [ + ":debug_printer", + ":java_file_generator", + ":proto_descriptor_collector", + "//common:cel_descriptors", + "//common/internal:proto_java_qualified_names", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:info_picocli_picocli", + ], +) + +java_library( + name = "proto_descriptor_collector", + srcs = ["ProtoDescriptorCollector.java"], + deps = [ + ":cel_lite_descriptor", + ":debug_printer", + "//common:cel_descriptors", + "//common/internal:proto_java_qualified_names", + "//common/internal:well_known_proto", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) + +java_library( + name = "java_file_generator", + srcs = ["JavaFileGenerator.java"], + resources = [ + ":cel_lite_descriptor_template_file", + ], + deps = [ + ":cel_lite_descriptor", + "//:auto_value", + "@maven//:com_google_guava_guava", + "@maven//:org_freemarker_freemarker", + ], +) + +java_library( + name = "debug_printer", + srcs = ["DebugPrinter.java"], + deps = [ + "@maven//:info_picocli_picocli", + ], +) + +java_library( + name = "cel_lite_descriptor", + srcs = ["CelLiteDescriptor.java"], + tags = [ + ], + deps = [ + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + ], +) diff --git a/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java new file mode 100644 index 000000000..16fe66f7b --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java @@ -0,0 +1,410 @@ +// 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.protobuf; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.Math.ceil; + +import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.ByteString; +import dev.cel.common.annotations.Internal; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Base class for code generated CEL lite descriptors to extend from. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@Immutable +public abstract class CelLiteDescriptor { + @SuppressWarnings("Immutable") // Copied to unmodifiable map + private final Map protoFqnToDescriptors; + + @SuppressWarnings("Immutable") // Copied to unmodifiable map + private final Map protoJavaClassNameToDescriptors; + + public Map getProtoTypeNamesToDescriptors() { + return protoFqnToDescriptors; + } + + public Map getProtoJavaClassNameToDescriptors() { + return protoJavaClassNameToDescriptors; + } + + /** + * Contains a collection of classes which describe protobuf messagelite types. + * + *

CEL Library Internals. Do Not Use. + */ + @Internal + @Immutable + public static final class MessageLiteDescriptor { + private final String fullyQualifiedProtoTypeName; + private final String fullyQualifiedProtoJavaClassName; + + @SuppressWarnings("Immutable") // Copied to unmodifiable map + private final Map fieldInfoMap; + + public String getFullyQualifiedProtoTypeName() { + return fullyQualifiedProtoTypeName; + } + + public String getFullyQualifiedProtoJavaClassName() { + return fullyQualifiedProtoJavaClassName; + } + + public Map getFieldInfoMap() { + return fieldInfoMap; + } + + public MessageLiteDescriptor( + String fullyQualifiedProtoTypeName, + String fullyQualifiedProtoJavaClassName, + Map fieldInfoMap) { + this.fullyQualifiedProtoTypeName = checkNotNull(fullyQualifiedProtoTypeName); + this.fullyQualifiedProtoJavaClassName = checkNotNull(fullyQualifiedProtoJavaClassName); + // This is a cheap operation. View over the existing map with mutators disabled. + this.fieldInfoMap = checkNotNull(Collections.unmodifiableMap(fieldInfoMap)); + } + } + + /** + * Describes a field of a protobuf messagelite type. + * + *

CEL Library Internals. Do Not Use. + */ + @Internal + @Immutable + public static final class FieldDescriptor { + private final JavaType javaType; + private final String fieldJavaClassName; + private final String fieldProtoTypeName; + private final String fullyQualifiedProtoFieldName; + private final String methodSuffixName; + private final Type protoFieldType; + private final CelFieldValueType celFieldValueType; + private final boolean hasHasser; + + /** + * Enumeration of the CEL field value type. This is analogous to the following from field + * descriptors: + * + *

+ */ + public enum CelFieldValueType { + SCALAR, + LIST, + MAP + } + + /** + * Enumeration of the java type. + * + *

This is exactly the same as com.google.protobuf.Descriptors#JavaType + */ + public enum JavaType { + INT, + LONG, + FLOAT, + DOUBLE, + BOOLEAN, + STRING, + BYTE_STRING, + ENUM, + MESSAGE + } + + /** + * Enumeration of the protobuf type. + * + *

This is exactly the same as com.google.protobuf.Descriptors#Type + */ + public enum Type { + DOUBLE, + FLOAT, + INT64, + UINT64, + INT32, + FIXED64, + FIXED32, + BOOL, + STRING, + GROUP, + MESSAGE, + BYTES, + UINT32, + ENUM, + SFIXED32, + SFIXED64, + SINT32, + SINT64 + } + + // Lazily-loaded field + @SuppressWarnings("Immutable") + private volatile Class fieldJavaClass; + + /** + * Returns the {@link Class} object for this field. In case of protobuf messages, the class + * object is lazily loaded then memoized. + */ + public Class getFieldJavaClass() { + if (fieldJavaClass == null) { + synchronized (this) { + if (fieldJavaClass == null) { + fieldJavaClass = loadNonPrimitiveFieldTypeClass(); + } + } + } + return fieldJavaClass; + } + + /** + * Gets the field's java type. + * + *

This is exactly the same as com.google.protobuf.Descriptors#JavaType + */ + public JavaType getJavaType() { + return javaType; + } + + /** + * Returns the method suffix name as part of getters or setters of the field in the protobuf + * message's builder. (Ex: for a field named single_string, "SingleString" is returned). + */ + public String getMethodSuffixName() { + return methodSuffixName; + } + + /** + * Returns the setter name for the field used in protobuf message's builder (Ex: + * setSingleString). + */ + public String getSetterName() { + String prefix = ""; + switch (celFieldValueType) { + case SCALAR: + prefix = "set"; + break; + case LIST: + prefix = "addAll"; + break; + case MAP: + prefix = "putAll"; + break; + } + return prefix + getMethodSuffixName(); + } + + /** + * Returns the getter name for the field used in protobuf message's builder (Ex: + * getSingleString). + */ + public String getGetterName() { + String suffix = ""; + switch (celFieldValueType) { + case SCALAR: + break; + case LIST: + suffix = "List"; + break; + case MAP: + suffix = "Map"; + break; + } + return "get" + getMethodSuffixName() + suffix; + } + + /** + * Returns the hasser name for the field (Ex: hasSingleString). + * + * @throws IllegalArgumentException If the message does not have a hasser. + */ + public String getHasserName() { + if (!getHasHasser()) { + throw new IllegalArgumentException("This message does not have a hasser."); + } + return "has" + getMethodSuffixName(); + } + + /** + * Returns the fully qualified java class name for the underlying field. (Ex: + * com.google.protobuf.StringValue). Returns an empty string for primitives . + */ + public String getFieldJavaClassName() { + return fieldJavaClassName; + } + + public CelFieldValueType getCelFieldValueType() { + return celFieldValueType; + } + + /** + * Gets the field's protobuf type. + * + *

This is exactly the same as com.google.protobuf.Descriptors#Type + */ + public Type getProtoFieldType() { + return protoFieldType; + } + + public boolean getHasHasser() { + return hasHasser && celFieldValueType.equals(CelFieldValueType.SCALAR); + } + + /** + * Gets the fully qualified protobuf message field name, including its package name (ex: + * cel.expr.conformance.proto3.TestAllTypes.single_string) + */ + public String getFullyQualifiedProtoFieldName() { + return fullyQualifiedProtoFieldName; + } + + /** + * Gets the fully qualified protobuf type name for the field, including its package name (ex: + * cel.expr.conformance.proto3.TestAllTypes.SingleStringWrapper). Returns an empty string for + * primitives. + */ + public String getFieldProtoTypeName() { + return fieldProtoTypeName; + } + + /** + * Must be public, used for codegen only. Do not use. + * + * @param fullyQualifiedProtoTypeName Fully qualified protobuf type name including the namespace + * (ex: cel.expr.conformance.proto3.TestAllTypes) + * @param javaTypeName Canonical Java type name (ex: Long, Double, Float, Message... see + * Descriptors#JavaType) + * @param methodSuffixName Suffix used to decorate the getters/setters (eg: "foo" in "setFoo" + * and "getFoo") + * @param celFieldValueType Describes whether the field is a scalar, list or a map with respect + * to CEL. + * @param protoFieldType Protobuf Field Type (ex: INT32, SINT32, GROUP, MESSAGE... see + * Descriptors#Type) + * @param hasHasser True if the message has a presence test method (ex: wrappers). + * @param fieldJavaClassName Fully qualified Java class name for the field, including its + * package name. Empty if the field is a primitive. + * @param fieldProtoTypeName Fully qualified protobuf type name for the field. Empty if the + * field is a primitive. + */ + @Internal + public FieldDescriptor( + String fullyQualifiedProtoTypeName, + String javaTypeName, + String methodSuffixName, + String celFieldValueType, // LIST, MAP, SCALAR + String protoFieldType, // INT32, SINT32, GROUP, MESSAGE... (See Descriptors#Type) + String hasHasser, // + String fieldJavaClassName, + String fieldProtoTypeName) { + this.fullyQualifiedProtoFieldName = checkNotNull(fullyQualifiedProtoTypeName); + this.javaType = JavaType.valueOf(javaTypeName); + this.methodSuffixName = checkNotNull(methodSuffixName); + this.fieldJavaClassName = checkNotNull(fieldJavaClassName); + this.celFieldValueType = CelFieldValueType.valueOf(checkNotNull(celFieldValueType)); + this.protoFieldType = Type.valueOf(protoFieldType); + this.hasHasser = Boolean.parseBoolean(hasHasser); + this.fieldProtoTypeName = checkNotNull(fieldProtoTypeName); + this.fieldJavaClass = getPrimitiveFieldTypeClass(); + } + + @SuppressWarnings("ReturnMissingNullable") // Avoid taking a dependency on jspecify.nullable. + private Class getPrimitiveFieldTypeClass() { + switch (celFieldValueType) { + case LIST: + return Iterable.class; + case MAP: + return Map.class; + case SCALAR: + return getScalarFieldTypeClass(); + } + + throw new IllegalStateException("Unexpected celFieldValueType: " + celFieldValueType); + } + + @SuppressWarnings("ReturnMissingNullable") // Avoid taking a dependency on jspecify.nullable. + private Class getScalarFieldTypeClass() { + switch (javaType) { + case INT: + return int.class; + case LONG: + return long.class; + case FLOAT: + return float.class; + case DOUBLE: + return double.class; + case BOOLEAN: + return boolean.class; + case STRING: + return String.class; + case BYTE_STRING: + return ByteString.class; + default: + // Non-primitives must be lazily loaded during instantiation of the runtime environment, + // where the generated messages are linked into the binary via java_lite_proto_library. + return null; + } + } + + private Class loadNonPrimitiveFieldTypeClass() { + if (!javaType.equals(JavaType.ENUM) && !javaType.equals(JavaType.MESSAGE)) { + throw new IllegalArgumentException("Unexpected java type name for " + javaType); + } + + try { + return Class.forName(fieldJavaClassName); + } catch (ClassNotFoundException e) { + throw new LinkageError(String.format("Could not find class %s", fieldJavaClassName), e); + } + } + } + + protected CelLiteDescriptor(List messageInfoList) { + Map protoFqnMap = + new HashMap<>(getMapInitialCapacity(messageInfoList.size())); + Map protoJavaClassNameMap = + new HashMap<>(getMapInitialCapacity(messageInfoList.size())); + for (MessageLiteDescriptor msgInfo : messageInfoList) { + protoFqnMap.put(msgInfo.getFullyQualifiedProtoTypeName(), msgInfo); + protoJavaClassNameMap.put(msgInfo.getFullyQualifiedProtoJavaClassName(), msgInfo); + } + + this.protoFqnToDescriptors = Collections.unmodifiableMap(protoFqnMap); + this.protoJavaClassNameToDescriptors = Collections.unmodifiableMap(protoJavaClassNameMap); + } + + /** + * Returns a capacity that is sufficient to keep the map from being resized as long as it grows no + * larger than expectedSize and the load factor is ≥ its default (0.75). + */ + private static int getMapInitialCapacity(int expectedSize) { + if (expectedSize < 3) { + return expectedSize + 1; + } + + // See https://github.com/openjdk/jdk/commit/3e393047e12147a81e2899784b943923fc34da8e. 0.75 is + // used as a load factor. + return (int) ceil(expectedSize / 0.75); + } +} diff --git a/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptorGenerator.java b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptorGenerator.java new file mode 100644 index 000000000..479c69c63 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptorGenerator.java @@ -0,0 +1,156 @@ +// 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.protobuf; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.io.Files; +import com.google.protobuf.DescriptorProtos.FileDescriptorProto; +import com.google.protobuf.DescriptorProtos.FileDescriptorSet; +import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.ExtensionRegistry; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.internal.ProtoJavaQualifiedNames; +import dev.cel.protobuf.JavaFileGenerator.JavaFileGeneratorOption; +import java.io.File; +import java.io.IOException; +import java.util.concurrent.Callable; +import picocli.CommandLine; +import picocli.CommandLine.Model.OptionSpec; +import picocli.CommandLine.Option; + +final class CelLiteDescriptorGenerator implements Callable { + + @Option( + names = {"--out"}, + description = "Outpath for the CelLiteDescriptor") + private String outPath = ""; + + @Option( + names = {"--descriptor"}, + description = + "Path to the descriptor (from proto_library) that the CelLiteDescriptor is to be" + + " generated from") + private String targetDescriptorPath = ""; + + @Option( + names = {"--transitive_descriptor_set"}, + description = "Path to the transitive set of descriptors") + private String transitiveDescriptorSetPath = ""; + + @Option( + names = {"--descriptor_class_name"}, + description = "Class name for the CelLiteDescriptor") + private String descriptorClassName = ""; + + @Option( + names = {"--version"}, + description = "CEL-Java version") + private String version = ""; + + @Option( + names = {"--debug"}, + description = "Prints debug output") + private boolean debug = false; + + private DebugPrinter debugPrinter; + + @Override + public Integer call() throws Exception { + String targetDescriptorProtoPath = extractProtoPath(targetDescriptorPath); + debugPrinter.print("Target descriptor proto path: " + targetDescriptorProtoPath); + + FileDescriptor targetFileDescriptor = null; + ImmutableSet transitiveFileDescriptors = + CelDescriptorUtil.getFileDescriptorsFromFileDescriptorSet( + load(transitiveDescriptorSetPath)); + for (FileDescriptor fd : transitiveFileDescriptors) { + if (fd.getFullName().equals(targetDescriptorProtoPath)) { + debugPrinter.print("Transitive Descriptor Path: " + fd.getFullName()); + targetFileDescriptor = fd; + break; + } + } + + if (targetFileDescriptor == null) { + throw new IllegalArgumentException( + String.format( + "Target descriptor %s not found from transitive set of descriptors!", + targetDescriptorProtoPath)); + } + + codegenCelLiteDescriptor(targetFileDescriptor); + + return 0; + } + + private void codegenCelLiteDescriptor(FileDescriptor targetFileDescriptor) throws Exception { + String javaPackageName = ProtoJavaQualifiedNames.getJavaPackageName(targetFileDescriptor); + ProtoDescriptorCollector descriptorCollector = + ProtoDescriptorCollector.newInstance(debugPrinter); + + JavaFileGenerator.createFile( + outPath, + JavaFileGeneratorOption.newBuilder() + .setVersion(version) + .setDescriptorClassName(descriptorClassName) + .setPackageName(javaPackageName) + .setMessageInfoList(descriptorCollector.collectMessageInfo(targetFileDescriptor)) + .build()); + } + + private String extractProtoPath(String descriptorPath) { + FileDescriptorSet fds = load(descriptorPath); + FileDescriptorProto fileDescriptorProto = Iterables.getOnlyElement(fds.getFileList()); + return fileDescriptorProto.getName(); + } + + private FileDescriptorSet load(String descriptorSetPath) { + try { + byte[] descriptorBytes = Files.toByteArray(new File(descriptorSetPath)); + // TODO: Implement ProtoExtensions + return FileDescriptorSet.parseFrom(descriptorBytes, ExtensionRegistry.getEmptyRegistry()); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to load FileDescriptorSet from path: " + descriptorSetPath, e); + } + } + + private void printAllFlags(CommandLine cmd) { + debugPrinter.print("Flag values:"); + debugPrinter.print("-------------------------------------------------------------"); + for (OptionSpec option : cmd.getCommandSpec().options()) { + debugPrinter.print(option.longestName() + ": " + option.getValue()); + } + debugPrinter.print("-------------------------------------------------------------"); + } + + private void initializeDebugPrinter() { + this.debugPrinter = DebugPrinter.newInstance(debug); + } + + public static void main(String[] args) { + CelLiteDescriptorGenerator celLiteDescriptorGenerator = new CelLiteDescriptorGenerator(); + CommandLine cmd = new CommandLine(celLiteDescriptorGenerator); + cmd.parseArgs(args); + celLiteDescriptorGenerator.initializeDebugPrinter(); + celLiteDescriptorGenerator.printAllFlags(cmd); + + int exitCode = cmd.execute(args); + System.exit(exitCode); + } + + CelLiteDescriptorGenerator() {} +} diff --git a/protobuf/src/main/java/dev/cel/protobuf/DebugPrinter.java b/protobuf/src/main/java/dev/cel/protobuf/DebugPrinter.java new file mode 100644 index 000000000..34a09ce98 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/DebugPrinter.java @@ -0,0 +1,36 @@ +// 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.protobuf; + +import picocli.CommandLine.Help.Ansi; + +final class DebugPrinter { + + private final boolean debug; + + static DebugPrinter newInstance(boolean debug) { + return new DebugPrinter(debug); + } + + void print(String message) { + if (debug) { + System.out.println(Ansi.ON.string("@|cyan [CelLiteDescriptorGenerator] |@" + message)); + } + } + + private DebugPrinter(boolean debug) { + this.debug = debug; + } +} diff --git a/protobuf/src/main/java/dev/cel/protobuf/JavaFileGenerator.java b/protobuf/src/main/java/dev/cel/protobuf/JavaFileGenerator.java new file mode 100644 index 000000000..ff6966a69 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/JavaFileGenerator.java @@ -0,0 +1,96 @@ +// 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.protobuf; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +// CEL-Internal-5 +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapperBuilder; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.Version; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; + +final class JavaFileGenerator { + + private static final String HELPER_CLASS_TEMPLATE_FILE = "cel_lite_descriptor_template.txt"; + + public static void createFile(String filePath, JavaFileGeneratorOption option) + throws IOException, TemplateException { + Version version = Configuration.VERSION_2_3_32; + Configuration cfg = new Configuration(version); + cfg.setClassForTemplateLoading(JavaFileGenerator.class, "templates/"); + cfg.setDefaultEncoding("UTF-8"); + cfg.setBooleanFormat("c"); + cfg.setAPIBuiltinEnabled(true); + DefaultObjectWrapperBuilder wrapperBuilder = new DefaultObjectWrapperBuilder(version); + wrapperBuilder.setExposeFields(true); + cfg.setObjectWrapper(wrapperBuilder.build()); + + Template template = cfg.getTemplate(HELPER_CLASS_TEMPLATE_FILE); + Writer out = new StringWriter(); + + template.process(option.getTemplateMap(), out); + + Files.asCharSink(new File(filePath), UTF_8).write(out.toString()); + } + + @AutoValue + abstract static class JavaFileGeneratorOption { + abstract String packageName(); + + abstract String descriptorClassName(); + + abstract String version(); + + abstract ImmutableList messageInfoList(); + + ImmutableMap getTemplateMap() { + return ImmutableMap.of( + "package_name", packageName(), + "descriptor_class_name", descriptorClassName(), + "version", version(), + "message_info_list", messageInfoList()); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setPackageName(String packageName); + + abstract Builder setDescriptorClassName(String className); + + abstract Builder setVersion(String version); + + abstract Builder setMessageInfoList(ImmutableList messageInfo); + + abstract JavaFileGeneratorOption build(); + } + + static Builder newBuilder() { + return new AutoValue_JavaFileGenerator_JavaFileGeneratorOption.Builder(); + } + } + + private JavaFileGenerator() {} +} diff --git a/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java b/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java new file mode 100644 index 000000000..6ffa414e3 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/ProtoDescriptorCollector.java @@ -0,0 +1,129 @@ +// 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.protobuf; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.google.common.base.CaseFormat; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Descriptors; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FileDescriptor; +import dev.cel.common.CelDescriptorUtil; +import dev.cel.common.CelDescriptors; +import dev.cel.common.internal.ProtoJavaQualifiedNames; +import dev.cel.common.internal.WellKnownProto; +import dev.cel.protobuf.CelLiteDescriptor.FieldDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldDescriptor.CelFieldValueType; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; + +/** + * ProtoDescriptorCollector inspects a {@link FileDescriptor} to collect message information into + * {@link MessageLiteDescriptor}. + */ +final class ProtoDescriptorCollector { + + private final DebugPrinter debugPrinter; + + ImmutableList collectMessageInfo(FileDescriptor targetFileDescriptor) { + ImmutableList.Builder messageInfoListBuilder = ImmutableList.builder(); + CelDescriptors celDescriptors = + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( + ImmutableList.of(targetFileDescriptor), /* resolveTypeDependencies= */ false); + ImmutableSet messageTypes = + celDescriptors.messageTypeDescriptors().stream() + .filter(d -> WellKnownProto.getByTypeName(d.getFullName()) == null) + .collect(toImmutableSet()); + + for (Descriptor descriptor : messageTypes) { + ImmutableMap.Builder fieldMap = ImmutableMap.builder(); + for (Descriptors.FieldDescriptor fieldDescriptor : descriptor.getFields()) { + String methodSuffixName = + CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, fieldDescriptor.getName()); + + String javaType = fieldDescriptor.getJavaType().toString(); + String embeddedFieldJavaClassName = ""; + String embeddedFieldProtoTypeName = ""; + switch (javaType) { + case "ENUM": + embeddedFieldJavaClassName = + ProtoJavaQualifiedNames.getFullyQualifiedJavaClassName( + fieldDescriptor.getEnumType()); + embeddedFieldProtoTypeName = fieldDescriptor.getEnumType().getFullName(); + break; + case "MESSAGE": + embeddedFieldJavaClassName = + ProtoJavaQualifiedNames.getFullyQualifiedJavaClassName( + fieldDescriptor.getMessageType()); + embeddedFieldProtoTypeName = fieldDescriptor.getMessageType().getFullName(); + break; + default: + break; + } + + CelFieldValueType fieldValueType; + if (fieldDescriptor.isMapField()) { + fieldValueType = CelFieldValueType.MAP; + } else if (fieldDescriptor.isRepeated()) { + fieldValueType = CelFieldValueType.LIST; + } else { + fieldValueType = CelFieldValueType.SCALAR; + } + + fieldMap.put( + fieldDescriptor.getName(), + new FieldDescriptor( + /* fullyQualifiedProtoTypeName= */ fieldDescriptor.getFullName(), + /* javaTypeName= */ javaType, + /* methodSuffixName= */ methodSuffixName, + /* celFieldValueType= */ fieldValueType.toString(), + /* protoFieldType= */ fieldDescriptor.getType().toString(), + /* hasHasser= */ String.valueOf(fieldDescriptor.hasPresence()), + /* fieldJavaClassName= */ embeddedFieldJavaClassName, + /* fieldProtoTypeName= */ embeddedFieldProtoTypeName)); + + debugPrinter.print( + String.format( + "Method suffix name in %s, for field %s: %s", + descriptor.getFullName(), fieldDescriptor.getFullName(), methodSuffixName)); + debugPrinter.print(String.format("FieldType: %s", fieldValueType)); + if (!embeddedFieldJavaClassName.isEmpty()) { + debugPrinter.print( + String.format( + "Java class name for field %s: %s", + fieldDescriptor.getName(), embeddedFieldJavaClassName)); + } + } + + messageInfoListBuilder.add( + new MessageLiteDescriptor( + descriptor.getFullName(), + ProtoJavaQualifiedNames.getFullyQualifiedJavaClassName(descriptor), + fieldMap.buildOrThrow())); + } + + return messageInfoListBuilder.build(); + } + + static ProtoDescriptorCollector newInstance(DebugPrinter debugPrinter) { + return new ProtoDescriptorCollector(debugPrinter); + } + + private ProtoDescriptorCollector(DebugPrinter debugPrinter) { + this.debugPrinter = debugPrinter; + } +} diff --git a/protobuf/src/main/java/dev/cel/protobuf/templates/cel_lite_descriptor_template.txt b/protobuf/src/main/java/dev/cel/protobuf/templates/cel_lite_descriptor_template.txt new file mode 100644 index 000000000..9c65878d1 --- /dev/null +++ b/protobuf/src/main/java/dev/cel/protobuf/templates/cel_lite_descriptor_template.txt @@ -0,0 +1,70 @@ +// 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. + +/** + * Generated by CEL-Java library. DO NOT EDIT! + * Version: ${version} + */ + +package ${package_name}; + +import dev.cel.protobuf.CelLiteDescriptor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class ${descriptor_class_name} extends CelLiteDescriptor { + + private static final ${descriptor_class_name} DESCRIPTOR = new ${descriptor_class_name}(); + + public static ${descriptor_class_name} getDescriptor() { + return DESCRIPTOR; + } + + private static List newDescriptors() { + List descriptors = new ArrayList<>(${message_info_list?size}); + Map fieldDescriptors; + <#list message_info_list as message_info> + + fieldDescriptors = new HashMap<>(${message_info.fieldInfoMap?size}); + <#list message_info.fieldInfoMap as key, value> + fieldDescriptors.put("${key}", new FieldDescriptor( + "${value.fullyQualifiedProtoFieldName}", + "${value.javaType}", + "${value.methodSuffixName}", + "${value.celFieldValueType}", + "${value.protoFieldType}", + "${value.hasHasser}", + "${value.fieldJavaClassName}", + "${value.fieldProtoTypeName}" + )); + + + descriptors.add( + new MessageLiteDescriptor( + "${message_info.fullyQualifiedProtoTypeName}", + "${message_info.fullyQualifiedProtoJavaClassName}", + Collections.unmodifiableMap(fieldDescriptors)) + ); + + + return Collections.unmodifiableList(descriptors); + } + + private ${descriptor_class_name}() { + super(newDescriptors()); + } +} \ No newline at end of file diff --git a/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel b/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel new file mode 100644 index 000000000..9d6fe3699 --- /dev/null +++ b/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel @@ -0,0 +1,36 @@ +load("@rules_java//java:defs.bzl", "java_library") +load("//:java_lite_proto_cel_library.bzl", "java_lite_proto_cel_library") +load("//:testing.bzl", "junit4_test_suites") + +package(default_applicable_licenses = ["//:license"]) + +java_library( + name = "tests", + testonly = 1, + srcs = glob(["*.java"]), + deps = [ + ":test_all_types_proto3_java_lite_cel_proto", + "//:java_truth", + "//protobuf:cel_lite_descriptor", + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto_lite", + "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:junit_junit", + ], +) + +java_lite_proto_cel_library( + name = "test_all_types_proto3_java_lite_cel_proto", + java_descriptor_class_prefix = "TestAllTypes", + deps = [ + "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_proto", + ], +) + +junit4_test_suites( + name = "test_suites", + sizes = [ + "small", + ], + src_dir = "src/test/java", + deps = [":tests"], +) diff --git a/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java b/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java new file mode 100644 index 000000000..69f0b573e --- /dev/null +++ b/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java @@ -0,0 +1,307 @@ +// 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.protobuf; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypesCelLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldDescriptor.CelFieldValueType; +import dev.cel.protobuf.CelLiteDescriptor.FieldDescriptor.JavaType; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class CelLiteDescriptorTest { + + private static final TestAllTypesCelLiteDescriptor TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR = + TestAllTypesCelLiteDescriptor.getDescriptor(); + + @Test + public void getProtoTypeNamesToDescriptors_containsAllMessages() { + Map protoNamesToDescriptors = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR.getProtoTypeNamesToDescriptors(); + + assertThat(protoNamesToDescriptors).hasSize(3); + assertThat(protoNamesToDescriptors).containsKey("cel.expr.conformance.proto3.TestAllTypes"); + assertThat(protoNamesToDescriptors) + .containsKey("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); + assertThat(protoNamesToDescriptors) + .containsKey("cel.expr.conformance.proto3.NestedTestAllTypes"); + } + + @Test + public void getDescriptors_fromProtoTypeAndJavaClassNames_referenceEquals() { + Map protoNamesToDescriptors = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR.getProtoTypeNamesToDescriptors(); + Map javaClassNamesToDescriptors = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR.getProtoJavaClassNameToDescriptors(); + + assertThat(protoNamesToDescriptors.get("cel.expr.conformance.proto3.TestAllTypes")) + .isSameInstanceAs( + javaClassNamesToDescriptors.get("dev.cel.expr.conformance.proto3.TestAllTypes")); + assertThat( + protoNamesToDescriptors.get("cel.expr.conformance.proto3.TestAllTypes.NestedMessage")) + .isSameInstanceAs( + javaClassNamesToDescriptors.get( + "dev.cel.expr.conformance.proto3.TestAllTypes$NestedMessage")); + assertThat(protoNamesToDescriptors.get("cel.expr.conformance.proto3.NestedTestAllTypes")) + .isSameInstanceAs( + javaClassNamesToDescriptors.get("dev.cel.expr.conformance.proto3.NestedTestAllTypes")); + } + + @Test + public void testAllTypesMessageLiteDescriptor_fullyQualifiedNames() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(testAllTypesDescriptor.getFullyQualifiedProtoTypeName()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes"); + assertThat(testAllTypesDescriptor.getFullyQualifiedProtoJavaClassName()) + .isEqualTo("dev.cel.expr.conformance.proto3.TestAllTypes"); + } + + @Test + public void testAllTypesMessageLiteDescriptor_fieldInfoMap_containsAllEntries() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(testAllTypesDescriptor.getFieldInfoMap()).hasSize(243); + } + + @Test + public void fieldDescriptor_scalarField() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = testAllTypesDescriptor.getFieldInfoMap().get("single_string"); + + assertThat(fieldDescriptor.getCelFieldValueType()).isEqualTo(CelFieldValueType.SCALAR); + assertThat(fieldDescriptor.getJavaType()).isEqualTo(JavaType.STRING); + assertThat(fieldDescriptor.getProtoFieldType()).isEqualTo(FieldDescriptor.Type.STRING); + } + + @Test + public void fieldDescriptor_primitiveField_fullyQualifiedNames() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = testAllTypesDescriptor.getFieldInfoMap().get("single_string"); + + assertThat(fieldDescriptor.getFullyQualifiedProtoFieldName()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes.single_string"); + assertThat(fieldDescriptor.getFieldProtoTypeName()).isEmpty(); + } + + @Test + public void fieldDescriptor_primitiveField_getFieldJavaClass() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = testAllTypesDescriptor.getFieldInfoMap().get("single_string"); + + assertThat(fieldDescriptor.getFieldJavaClass()).isEqualTo(String.class); + assertThat(fieldDescriptor.getFieldJavaClassName()).isEmpty(); + } + + @Test + public void fieldDescriptor_scalarField_builderMethods() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = testAllTypesDescriptor.getFieldInfoMap().get("single_string"); + + assertThat(fieldDescriptor.getHasHasser()).isFalse(); + assertThat(fieldDescriptor.getGetterName()).isEqualTo("getSingleString"); + assertThat(fieldDescriptor.getSetterName()).isEqualTo("setSingleString"); + } + + @Test + public void fieldDescriptor_getHasserName_throwsIfNotWrapper() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = testAllTypesDescriptor.getFieldInfoMap().get("single_string"); + + assertThrows(IllegalArgumentException.class, fieldDescriptor::getHasserName); + } + + @Test + public void fieldDescriptor_getHasserName_success() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = + testAllTypesDescriptor.getFieldInfoMap().get("single_string_wrapper"); + + assertThat(fieldDescriptor.getHasHasser()).isTrue(); + assertThat(fieldDescriptor.getHasserName()).isEqualTo("hasSingleStringWrapper"); + } + + @Test + public void fieldDescriptor_mapField() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = + testAllTypesDescriptor.getFieldInfoMap().get("map_bool_string"); + + assertThat(fieldDescriptor.getCelFieldValueType()).isEqualTo(CelFieldValueType.MAP); + assertThat(fieldDescriptor.getJavaType()).isEqualTo(JavaType.MESSAGE); + assertThat(fieldDescriptor.getProtoFieldType()).isEqualTo(FieldDescriptor.Type.MESSAGE); + } + + @Test + public void fieldDescriptor_mapField_builderMethods() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = + testAllTypesDescriptor.getFieldInfoMap().get("map_bool_string"); + + assertThat(fieldDescriptor.getHasHasser()).isFalse(); + assertThat(fieldDescriptor.getGetterName()).isEqualTo("getMapBoolStringMap"); + assertThat(fieldDescriptor.getSetterName()).isEqualTo("putAllMapBoolString"); + } + + @Test + public void fieldDescriptor_mapField_getFieldJavaClass() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = + testAllTypesDescriptor.getFieldInfoMap().get("map_bool_string"); + + assertThat(fieldDescriptor.getFieldJavaClass()).isEqualTo(Map.class); + assertThat(fieldDescriptor.getFieldJavaClassName()) + .isEqualTo("dev.cel.expr.conformance.proto3.TestAllTypes$MapBoolStringEntry"); + } + + @Test + public void fieldDescriptor_repeatedField() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = + testAllTypesDescriptor.getFieldInfoMap().get("repeated_int64"); + + assertThat(fieldDescriptor.getCelFieldValueType()).isEqualTo(CelFieldValueType.LIST); + assertThat(fieldDescriptor.getJavaType()).isEqualTo(JavaType.LONG); + assertThat(fieldDescriptor.getProtoFieldType()).isEqualTo(FieldDescriptor.Type.INT64); + } + + @Test + public void fieldDescriptor_repeatedField_builderMethods() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = + testAllTypesDescriptor.getFieldInfoMap().get("repeated_int64"); + + assertThat(fieldDescriptor.getHasHasser()).isFalse(); + assertThat(fieldDescriptor.getGetterName()).isEqualTo("getRepeatedInt64List"); + assertThat(fieldDescriptor.getSetterName()).isEqualTo("addAllRepeatedInt64"); + } + + @Test + public void fieldDescriptor_repeatedField_primitives_getFieldJavaClass() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = + testAllTypesDescriptor.getFieldInfoMap().get("repeated_int64"); + + assertThat(fieldDescriptor.getFieldJavaClass()).isEqualTo(Iterable.class); + assertThat(fieldDescriptor.getFieldJavaClassName()).isEmpty(); + } + + @Test + public void fieldDescriptor_repeatedField_wrappers_getFieldJavaClass() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = + testAllTypesDescriptor.getFieldInfoMap().get("repeated_double_wrapper"); + + assertThat(fieldDescriptor.getFieldJavaClass()).isEqualTo(Iterable.class); + assertThat(fieldDescriptor.getFieldJavaClassName()) + .isEqualTo("com.google.protobuf.DoubleValue"); + } + + @Test + public void fieldDescriptor_nestedMessage() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = + testAllTypesDescriptor.getFieldInfoMap().get("standalone_message"); + + assertThat(fieldDescriptor.getCelFieldValueType()).isEqualTo(CelFieldValueType.SCALAR); + assertThat(fieldDescriptor.getJavaType()).isEqualTo(JavaType.MESSAGE); + assertThat(fieldDescriptor.getProtoFieldType()).isEqualTo(FieldDescriptor.Type.MESSAGE); + } + + @Test + public void fieldDescriptor_nestedMessage_getFieldJavaClass() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = + testAllTypesDescriptor.getFieldInfoMap().get("standalone_message"); + + assertThat(fieldDescriptor.getFieldJavaClass()).isEqualTo(TestAllTypes.NestedMessage.class); + assertThat(fieldDescriptor.getFieldJavaClassName()) + .isEqualTo("dev.cel.expr.conformance.proto3.TestAllTypes$NestedMessage"); + } + + @Test + public void fieldDescriptor_nestedMessage_fullyQualifiedNames() { + MessageLiteDescriptor testAllTypesDescriptor = + TEST_ALL_TYPES_CEL_LITE_DESCRIPTOR + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + FieldDescriptor fieldDescriptor = + testAllTypesDescriptor.getFieldInfoMap().get("standalone_message"); + + assertThat(fieldDescriptor.getFullyQualifiedProtoFieldName()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes.standalone_message"); + assertThat(fieldDescriptor.getFieldProtoTypeName()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); + } +}