From 5c7d8c7a05521120aee3816b47f28b629f63b863 Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Fri, 23 Jan 2026 13:22:40 +0000 Subject: [PATCH] POC: JsonFieldName support --- .../src/test/java/dev/cel/bundle/BUILD.bazel | 1 + .../test/java/dev/cel/bundle/CelImplTest.java | 19 +++ .../dev/cel/checker/CelCheckerLegacyImpl.java | 10 +- .../main/java/dev/cel/common/CelOptions.java | 12 +- .../types/ProtoMessageTypeProvider.java | 140 ++++++++++++++---- .../java/dev/cel/common/types/BUILD.bazel | 1 + .../types/ProtoMessageTypeProviderTest.java | 20 +++ .../runtime/DescriptorMessageProvider.java | 9 ++ .../test/resources/protos/single_file.proto | 1 + 9 files changed, 180 insertions(+), 33 deletions(-) diff --git a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel index 26cbe392d..bc40f5014 100644 --- a/bundle/src/test/java/dev/cel/bundle/BUILD.bazel +++ b/bundle/src/test/java/dev/cel/bundle/BUILD.bazel @@ -67,6 +67,7 @@ java_library( "@maven//:com_google_truth_extensions_truth_proto_extension", "@maven//:junit_junit", "@maven//:org_jspecify_jspecify", + "//testing/protos:single_file_java_proto", ], ) diff --git a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java index 13c3392d3..ffe34e222 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelImplTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelImplTest.java @@ -112,6 +112,7 @@ import dev.cel.runtime.CelUnknownSet; import dev.cel.runtime.CelVariableResolver; import dev.cel.runtime.UnknownContext; +import dev.cel.testing.testdata.SingleFileProto.SingleFile; import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum; import java.time.Instant; import java.util.ArrayList; @@ -2193,6 +2194,24 @@ public void toBuilder_isImmutable() { assertThat(newRuntimeBuilder).isNotEqualTo(celImpl.toRuntimeBuilder()); } + @Test + public void eval_withJsonFieldName() + throws Exception { + Cel cel = + standardCelBuilderWithMacros() + .addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName())) + .addMessageTypes(SingleFile.getDescriptor()) + .setOptions(CelOptions.current().enableJsonFieldNames(true).build()) + .build(); + CelAbstractSyntaxTree ast = + cel.compile("file.camelCased").getAst(); + + Object result = cel.createProgram(ast).eval(ImmutableMap.of("file", SingleFile.newBuilder().setSnakeCased("foo").build())); + + assertThat(result).isEqualTo("foo"); + } + + private static TypeProvider aliasingProvider(ImmutableMap typeAliases) { return new TypeProvider() { @Override diff --git a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java index 41d1ca073..b8a490e65 100644 --- a/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java +++ b/checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java @@ -456,9 +456,13 @@ public CelCheckerLegacyImpl build() { } CelTypeProvider messageTypeProvider = - new ProtoMessageTypeProvider( - CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - fileTypeSet, celOptions.resolveTypeDependencies())); + ProtoMessageTypeProvider.newBuilder() + .setAllowJsonFieldNames(celOptions.enableJsonFieldNames()) + .setCelDescriptors( + CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( + fileTypeSet, celOptions.resolveTypeDependencies())) + .build(); + if (celTypeProvider != null && fileTypeSet.isEmpty()) { messageTypeProvider = celTypeProvider; } else if (celTypeProvider != null) { diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index d39d53803..f1948bddb 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -83,6 +83,8 @@ public enum ProtoUnsetFieldOptions { public abstract boolean enableNamespacedDeclarations(); + public abstract boolean enableJsonFieldNames(); + // Evaluation related options public abstract boolean disableCelStandardEquality(); @@ -150,6 +152,7 @@ public static Builder newBuilder() { .enableTimestampEpoch(false) .enableHeterogeneousNumericComparisons(false) .enableNamespacedDeclarations(true) + .enableJsonFieldNames(false) // Evaluation options .disableCelStandardEquality(true) .evaluateCanonicalTypesToNativeValues(false) @@ -170,7 +173,8 @@ public static Builder newBuilder() { .enableStringConcatenation(true) .enableListConcatenation(true) .enableComprehension(true) - .maxRegexProgramSize(-1); + .maxRegexProgramSize(-1) + ; } /** @@ -529,6 +533,12 @@ public abstract static class Builder { */ public abstract Builder maxRegexProgramSize(int value); + /** + * TODO + */ + public abstract Builder enableJsonFieldNames(boolean value); + + public abstract CelOptions build(); } } diff --git a/common/src/main/java/dev/cel/common/types/ProtoMessageTypeProvider.java b/common/src/main/java/dev/cel/common/types/ProtoMessageTypeProvider.java index 4b97178d0..7ab964680 100644 --- a/common/src/main/java/dev/cel/common/types/ProtoMessageTypeProvider.java +++ b/common/src/main/java/dev/cel/common/types/ProtoMessageTypeProvider.java @@ -14,11 +14,9 @@ package dev.cel.common.types; -import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import com.google.common.collect.ImmutableCollection; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; @@ -34,11 +32,11 @@ import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelDescriptors; import dev.cel.common.internal.FileDescriptorSetConverter; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.function.Function; /** * The {@code ProtoMessageTypeProvider} implements the {@link CelTypeProvider} interface to provide @@ -68,35 +66,35 @@ public final class ProtoMessageTypeProvider implements CelTypeProvider { .buildOrThrow(); private final ImmutableMap allTypes; + private final boolean allowJsonFieldNames; + /** Returns a new builder for {@link ProtoMessageTypeProvider}. */ + public static Builder newBuilder() { + return new Builder(); + } + + @Deprecated public ProtoMessageTypeProvider() { - this(CelDescriptors.builder().build()); + this(CelDescriptors.builder().build(), false); } + @Deprecated public ProtoMessageTypeProvider(FileDescriptorSet descriptorSet) { this( CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - FileDescriptorSetConverter.convert(descriptorSet))); + FileDescriptorSetConverter.convert(descriptorSet)), false); } + @Deprecated public ProtoMessageTypeProvider(Iterable descriptors) { this( CelDescriptorUtil.getAllDescriptorsFromFileDescriptor( - ImmutableSet.copyOf(Iterables.transform(descriptors, Descriptor::getFile)))); + ImmutableSet.copyOf(Iterables.transform(descriptors, Descriptor::getFile))), false); } + @Deprecated public ProtoMessageTypeProvider(ImmutableSet fileDescriptors) { - this(CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptors)); - } - - public ProtoMessageTypeProvider(CelDescriptors celDescriptors) { - this.allTypes = - ImmutableMap.builder() - .putAll(createEnumTypes(celDescriptors.enumDescriptors())) - .putAll( - createProtoMessageTypes( - celDescriptors.messageTypeDescriptors(), celDescriptors.extensionDescriptors())) - .buildOrThrow(); + this(CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptors), false); } @Override @@ -120,8 +118,14 @@ private ImmutableMap createProtoMessageTypes( if (protoMessageTypes.containsKey(descriptor.getFullName())) { continue; } - ImmutableList fieldNames = - descriptor.getFields().stream().map(FieldDescriptor::getName).collect(toImmutableList()); + + ImmutableSet.Builder fieldNamesBuilder = ImmutableSet.builder() ; + for (FieldDescriptor fd : descriptor.getFields()) { + fieldNamesBuilder.add(fd.getName()); + if (allowJsonFieldNames) { + fieldNamesBuilder.add(fd.getJsonName()); + } + } Map extensionFields = new HashMap<>(); for (FieldDescriptor extension : extensionMap.get(descriptor.getFullName())) { @@ -133,7 +137,7 @@ private ImmutableMap createProtoMessageTypes( descriptor.getFullName(), ProtoMessageType.create( descriptor.getFullName(), - ImmutableSet.copyOf(fieldNames), + fieldNamesBuilder.build(), new FieldResolver(this, descriptor)::findField, new FieldResolver(this, extensions)::findField)); } @@ -158,19 +162,35 @@ private ImmutableMap createEnumTypes( } private static class FieldResolver { - private final CelTypeProvider celTypeProvider; + private final ProtoMessageTypeProvider protoMessageTypeProvider; private final ImmutableMap fields; - private FieldResolver(CelTypeProvider celTypeProvider, Descriptor descriptor) { + private static ImmutableMap collectFieldDescriptorMap(ProtoMessageTypeProvider protoMessageTypeProvider, Descriptor descriptor) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (FieldDescriptor fd : descriptor.getFields()) { + builder.put(fd.getName(), fd); + if (protoMessageTypeProvider.allowJsonFieldNames) { + builder.put(fd.getJsonName(), fd); + } + } + + return builder.buildKeepingLast(); + } + + private FieldResolver( + ProtoMessageTypeProvider protoMessageTypeProvider, + Descriptor descriptor + ) { this( - celTypeProvider, - descriptor.getFields().stream() - .collect(toImmutableMap(FieldDescriptor::getName, Function.identity()))); + protoMessageTypeProvider, + collectFieldDescriptorMap(protoMessageTypeProvider, descriptor) + ); } private FieldResolver( - CelTypeProvider celTypeProvider, ImmutableMap fields) { - this.celTypeProvider = celTypeProvider; + ProtoMessageTypeProvider protoMessageTypeProvider, + ImmutableMap fields) { + this.protoMessageTypeProvider = protoMessageTypeProvider; this.fields = fields; } @@ -203,11 +223,11 @@ private Optional findFieldInternal(FieldDescriptor fieldDescriptor) { String messageName = descriptor.getFullName(); fieldType = CelTypes.getWellKnownCelType(messageName) - .orElse(celTypeProvider.findType(descriptor.getFullName()).orElse(null)); + .orElse(protoMessageTypeProvider.findType(descriptor.getFullName()).orElse(null)); break; case ENUM: EnumDescriptor enumDescriptor = fieldDescriptor.getEnumType(); - fieldType = celTypeProvider.findType(enumDescriptor.getFullName()).orElse(null); + fieldType = protoMessageTypeProvider.findType(enumDescriptor.getFullName()).orElse(null); break; default: fieldType = PROTO_TYPE_TO_CEL_TYPE.get(fieldDescriptor.getType()); @@ -222,4 +242,66 @@ private Optional findFieldInternal(FieldDescriptor fieldDescriptor) { return Optional.of(fieldType); } } + + /** Builder for {@link ProtoMessageTypeProvider}. */ + public static final class Builder { + private final ImmutableSet.Builder fileDescriptors = ImmutableSet.builder(); + private boolean allowJsonFieldNames; + private CelDescriptors celDescriptors; + + /** Adds a {@link FileDescriptor} to the provider. */ + public Builder addFileDescriptors(FileDescriptor... fileDescriptors) { + return addFileDescriptors(Arrays.asList(fileDescriptors)); + } + + /** Adds a collection of {@link FileDescriptor}s to the provider. */ + public Builder addFileDescriptors(Iterable fileDescriptors) { + this.fileDescriptors.addAll(fileDescriptors); + return this; + } + + /** Adds a collection of {@link Descriptor}s. The parent file of each descriptor is added. */ + public Builder addDescriptors(Iterable descriptors) { + this.fileDescriptors.addAll(Iterables.transform(descriptors, Descriptor::getFile)); + return this; + } + + /** Adds a {@link FileDescriptorSet} to the provider. */ + public Builder addFileDescriptorSet(FileDescriptorSet fileDescriptorSet) { + this.fileDescriptors.addAll(FileDescriptorSetConverter.convert(fileDescriptorSet)); + return this; + } + + public Builder setAllowJsonFieldNames(boolean allowJsonFieldNames) { + this.allowJsonFieldNames = allowJsonFieldNames; + return this; + } + + public Builder setCelDescriptors(CelDescriptors celDescriptors) { + this.celDescriptors = celDescriptors; + return this; + } + + /** Builds the {@link ProtoMessageTypeProvider}. */ + public ProtoMessageTypeProvider build() { + // CelDescriptors celDescriptors = CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(fileDescriptors.build()); + return new ProtoMessageTypeProvider(celDescriptors, allowJsonFieldNames); + } + + private Builder() {} + } + + private ProtoMessageTypeProvider( + CelDescriptors celDescriptors, + boolean allowJsonFieldNames + ) { + this.allowJsonFieldNames = allowJsonFieldNames; + this.allTypes = + ImmutableMap.builder() + .putAll(createEnumTypes(celDescriptors.enumDescriptors())) + .putAll( + createProtoMessageTypes( + celDescriptors.messageTypeDescriptors(), celDescriptors.extensionDescriptors())) + .buildOrThrow(); + } } diff --git a/common/src/test/java/dev/cel/common/types/BUILD.bazel b/common/src/test/java/dev/cel/common/types/BUILD.bazel index 600a63940..0c8121bbd 100644 --- a/common/src/test/java/dev/cel/common/types/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/types/BUILD.bazel @@ -16,6 +16,7 @@ java_library( "//common/types:cel_types", "//common/types:message_type_provider", "//common/types:type_providers", + "//testing/protos:single_file_java_proto", "@cel_spec//proto/cel/expr:checked_java_proto", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", diff --git a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java index d774903e1..20d5cb916 100644 --- a/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java +++ b/common/src/test/java/dev/cel/common/types/ProtoMessageTypeProviderTest.java @@ -20,8 +20,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import dev.cel.common.types.CelTypeProvider.CombinedCelTypeProvider; +import dev.cel.common.types.StructType.Field; import dev.cel.expr.conformance.proto2.TestAllTypes; import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; +import dev.cel.testing.testdata.SingleFileProto.SingleFile; import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @@ -254,4 +256,22 @@ public void types_combinedDuplicateProviderIsSameAsFirst() { CombinedCelTypeProvider combined = new CombinedCelTypeProvider(proto3Provider, proto3Provider); assertThat(combined.types()).hasSize(proto3Provider.types().size()); } + + @Test + public void findField_withJsonNameOption() { + ProtoMessageTypeProvider typeProvider = + ProtoMessageTypeProvider.newBuilder() + .addFileDescriptors(SingleFile.getDescriptor().getFile()) + .setAllowJsonFieldNames(true) + .build(); + + ProtoMessageType msgType = (ProtoMessageType) typeProvider.findType(SingleFile.getDescriptor().getFullName()).get(); + + // Note that these are the same fields, with json_name option set + Optional snakeCasedField = msgType.findField("snake_cased"); + Optional jsonNameField = msgType.findField("camelCased"); + + assertThat(snakeCasedField).hasValue(Field.of("snake_cased", SimpleType.STRING)); + assertThat(jsonNameField).hasValue(Field.of("camelCased", SimpleType.STRING)); + } } diff --git a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java index f2ea85b9b..eac5c8c02 100644 --- a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java @@ -180,6 +180,15 @@ private FieldDescriptor findField(Descriptor descriptor, String fieldName) { } } + if (fieldDescriptor == null && celOptions.enableJsonFieldNames()) { + for (FieldDescriptor fd : descriptor.getFields()) { + if (fd.getJsonName().equals(fieldName)) { + fieldDescriptor = fd; + break; + } + } + } + if (fieldDescriptor == null) { throw new IllegalArgumentException( String.format( diff --git a/testing/src/test/resources/protos/single_file.proto b/testing/src/test/resources/protos/single_file.proto index 0fcf270a1..b5ce518e0 100644 --- a/testing/src/test/resources/protos/single_file.proto +++ b/testing/src/test/resources/protos/single_file.proto @@ -26,4 +26,5 @@ message SingleFile { string name = 1; Path path = 2; + string snake_cased = 3 [json_name = "camelCased"]; }