Skip to content

Commit e36c49f

Browse files
l46kokcopybara-github
authored andcommitted
Add json_name protobuf field option support
PiperOrigin-RevId: 866159844
1 parent 72a1207 commit e36c49f

21 files changed

Lines changed: 455 additions & 81 deletions

File tree

bundle/src/test/java/dev/cel/bundle/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ java_library(
3434
"//common:proto_ast",
3535
"//common:source_location",
3636
"//common/ast",
37-
"//common/internal:proto_time_utils",
3837
"//common/resources/testdata/proto3:standalone_global_enum_java_proto",
3938
"//common/testing",
4039
"//common/types",
@@ -55,6 +54,7 @@ java_library(
5554
"//runtime:evaluation_listener",
5655
"//runtime:function_binding",
5756
"//runtime:unknown_attributes",
57+
"//testing/protos:single_file_java_proto",
5858
"@cel_spec//proto/cel/expr:checked_java_proto",
5959
"@cel_spec//proto/cel/expr:syntax_java_proto",
6060
"@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto",

bundle/src/test/java/dev/cel/bundle/CelImplTest.java

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import dev.cel.common.CelIssue;
7272
import dev.cel.common.CelOptions;
7373
import dev.cel.common.CelProtoAbstractSyntaxTree;
74+
import dev.cel.common.CelSource.Extension;
7475
import dev.cel.common.CelSourceLocation;
7576
import dev.cel.common.CelValidationException;
7677
import dev.cel.common.CelValidationResult;
@@ -112,6 +113,7 @@
112113
import dev.cel.runtime.CelUnknownSet;
113114
import dev.cel.runtime.CelVariableResolver;
114115
import dev.cel.runtime.UnknownContext;
116+
import dev.cel.testing.testdata.SingleFileProto.SingleFile;
115117
import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum;
116118
import java.time.Instant;
117119
import java.util.ArrayList;
@@ -743,7 +745,7 @@ public void program_withThrowingFunction() throws Exception {
743745
CelFunctionBinding.from(
744746
"throws",
745747
ImmutableList.of(),
746-
(args) -> {
748+
(unused) -> {
747749
throw new CelEvaluationException("this method always throws");
748750
}))
749751
.setResultType(SimpleType.BOOL)
@@ -771,7 +773,7 @@ public void program_withThrowingFunctionShortcircuited() throws Exception {
771773
CelFunctionBinding.from(
772774
"throws",
773775
ImmutableList.of(),
774-
(args) -> {
776+
(unused) -> {
775777
throw CelEvaluationExceptionBuilder.newBuilder("this method always throws")
776778
.setCause(new RuntimeException("reason"))
777779
.build();
@@ -1143,7 +1145,7 @@ public void program_customVarResolver() throws Exception {
11431145
program.eval(
11441146
(name) -> name.equals("variable") ? Optional.of("hello") : Optional.empty()))
11451147
.isEqualTo(true);
1146-
assertThat(program.eval((name) -> Optional.of(""))).isEqualTo(false);
1148+
assertThat(program.eval((unused) -> Optional.of(""))).isEqualTo(false);
11471149
}
11481150

11491151
@Test
@@ -2193,6 +2195,83 @@ public void toBuilder_isImmutable() {
21932195
assertThat(newRuntimeBuilder).isNotEqualTo(celImpl.toRuntimeBuilder());
21942196
}
21952197

2198+
@Test
2199+
public void eval_withJsonFieldName() throws Exception {
2200+
Cel cel =
2201+
standardCelBuilderWithMacros()
2202+
.addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName()))
2203+
.addMessageTypes(SingleFile.getDescriptor())
2204+
.setOptions(CelOptions.current().enableJsonFieldNames(true).build())
2205+
.build();
2206+
CelAbstractSyntaxTree ast = cel.compile("file.camelCased").getAst();
2207+
2208+
Object result =
2209+
cel.createProgram(ast)
2210+
.eval(ImmutableMap.of("file", SingleFile.newBuilder().setSnakeCased("foo").build()));
2211+
2212+
assertThat(result).isEqualTo("foo");
2213+
}
2214+
2215+
@Test
2216+
public void eval_withJsonFieldName_runtimeOptionDisabled_throws() throws Exception {
2217+
CelCompiler celCompiler =
2218+
CelCompilerFactory.standardCelCompilerBuilder()
2219+
.addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName()))
2220+
.addMessageTypes(SingleFile.getDescriptor())
2221+
.setOptions(CelOptions.current().enableJsonFieldNames(true).build())
2222+
.build();
2223+
CelRuntime celRuntime =
2224+
CelRuntimeFactory.standardCelRuntimeBuilder()
2225+
.addMessageTypes(SingleFile.getDescriptor())
2226+
.setOptions(CelOptions.current().enableJsonFieldNames(false).build())
2227+
.build();
2228+
CelAbstractSyntaxTree ast = celCompiler.compile("file.camelCased").getAst();
2229+
2230+
CelEvaluationException e =
2231+
assertThrows(
2232+
CelEvaluationException.class,
2233+
() ->
2234+
celRuntime
2235+
.createProgram(ast)
2236+
.eval(ImmutableMap.of("file", SingleFile.getDefaultInstance())));
2237+
assertThat(e)
2238+
.hasMessageThat()
2239+
.contains(
2240+
"field 'camelCased' is not declared in message 'dev.cel.testing.testdata.SingleFile");
2241+
}
2242+
2243+
@Test
2244+
public void compile_withJsonFieldName_astTagged() throws Exception {
2245+
Cel cel =
2246+
standardCelBuilderWithMacros()
2247+
.addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName()))
2248+
.addMessageTypes(SingleFile.getDescriptor())
2249+
.setOptions(CelOptions.current().enableJsonFieldNames(true).build())
2250+
.build();
2251+
CelAbstractSyntaxTree ast = cel.compile("file.camelCased").getAst();
2252+
2253+
assertThat(ast.getSource().getExtensions())
2254+
.contains(
2255+
Extension.create(
2256+
"json_name", Extension.Version.of(1L, 1L), Extension.Component.COMPONENT_RUNTIME));
2257+
}
2258+
2259+
@Test
2260+
public void compile_withJsonFieldName_protoFieldNameComparison_throws() throws Exception {
2261+
Cel cel =
2262+
standardCelBuilderWithMacros()
2263+
.addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName()))
2264+
.addMessageTypes(SingleFile.getDescriptor())
2265+
.setOptions(CelOptions.current().enableJsonFieldNames(true).build())
2266+
.build();
2267+
2268+
CelValidationException e =
2269+
assertThrows(
2270+
CelValidationException.class,
2271+
() -> cel.compile("file.camelCased == file.snake_cased").getAst());
2272+
assertThat(e).hasMessageThat().contains("undefined field 'snake_cased'");
2273+
}
2274+
21962275
private static TypeProvider aliasingProvider(ImmutableMap<String, Type> typeAliases) {
21972276
return new TypeProvider() {
21982277
@Override

checker/src/main/java/dev/cel/checker/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ java_library(
177177
":standard_decl",
178178
"//:auto_value",
179179
"//common:cel_ast",
180+
"//common:cel_source",
180181
"//common:compiler_common",
181182
"//common:container",
182183
"//common:mutable_ast",

checker/src/main/java/dev/cel/checker/CelCheckerLegacyImpl.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,9 +456,12 @@ public CelCheckerLegacyImpl build() {
456456
}
457457

458458
CelTypeProvider messageTypeProvider =
459-
new ProtoMessageTypeProvider(
460-
CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(
461-
fileTypeSet, celOptions.resolveTypeDependencies()));
459+
ProtoMessageTypeProvider.newBuilder()
460+
.setAllowJsonFieldNames(celOptions.enableJsonFieldNames())
461+
.setResolveTypeDependencies(celOptions.resolveTypeDependencies())
462+
.addFileDescriptors(fileTypeSet)
463+
.build();
464+
462465
if (celTypeProvider != null && fileTypeSet.isEmpty()) {
463466
messageTypeProvider = celTypeProvider;
464467
} else if (celTypeProvider != null) {

checker/src/main/java/dev/cel/checker/ExprChecker.java

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import dev.cel.common.CelMutableAst;
3333
import dev.cel.common.CelOverloadDecl;
3434
import dev.cel.common.CelProtoAbstractSyntaxTree;
35+
import dev.cel.common.CelSource;
3536
import dev.cel.common.Operator;
3637
import dev.cel.common.annotations.Internal;
3738
import dev.cel.common.ast.CelConstant;
@@ -52,12 +53,14 @@
5253
import dev.cel.common.types.ListType;
5354
import dev.cel.common.types.MapType;
5455
import dev.cel.common.types.OptionalType;
56+
import dev.cel.common.types.ProtoMessageType;
5557
import dev.cel.common.types.SimpleType;
5658
import dev.cel.common.types.TypeType;
5759
import java.util.ArrayList;
5860
import java.util.HashSet;
5961
import java.util.List;
6062
import java.util.Map;
63+
import java.util.Set;
6164
import org.jspecify.annotations.Nullable;
6265

6366
/**
@@ -70,6 +73,11 @@
7073
@Internal
7174
@Deprecated
7275
public final class ExprChecker {
76+
private static final CelSource.Extension JSON_NAME_EXTENSION =
77+
CelSource.Extension.create(
78+
"json_name",
79+
CelSource.Extension.Version.of(1, 1),
80+
CelSource.Extension.Component.COMPONENT_RUNTIME);
7381

7482
/**
7583
* Deprecated type-check API.
@@ -152,7 +160,10 @@ public static CelAbstractSyntaxTree typecheck(
152160

153161
CelAbstractSyntaxTree parsedAst = mutableAst.toParsedAst(/* retainSourcePositions= */ true);
154162
return CelAbstractSyntaxTree.newCheckedAst(
155-
parsedAst.getExpr(), parsedAst.getSource(), env.getRefMap(), typeMap);
163+
parsedAst.getExpr(),
164+
parsedAst.getSource().toBuilder().addAllExtensions(checker.extensions).build(),
165+
env.getRefMap(),
166+
typeMap);
156167
}
157168

158169
private final Env env;
@@ -163,6 +174,7 @@ public static CelAbstractSyntaxTree typecheck(
163174
private final boolean compileTimeOverloadResolution;
164175
private final boolean homogeneousLiterals;
165176
private final boolean namespacedDeclarations;
177+
private final Set<CelSource.Extension> extensions;
166178

167179
private ExprChecker(
168180
Env env,
@@ -180,6 +192,7 @@ private ExprChecker(
180192
this.compileTimeOverloadResolution = compileTimeOverloadResolution;
181193
this.homogeneousLiterals = homogeneousLiterals;
182194
this.namespacedDeclarations = namespacedDeclarations;
195+
this.extensions = new HashSet<>();
183196
}
184197

185198
/** Visit the {@code expr} value, routing to overloads based on the kind of expression. */
@@ -370,13 +383,13 @@ private void visit(CelMutableExpr expr, CelMutableStruct struct) {
370383

371384
env.setRef(expr, CelReference.newBuilder().setName(decl.name()).build());
372385
CelType type = decl.type();
373-
if (type.kind() != CelKind.ERROR) {
374-
if (type.kind() != CelKind.TYPE) {
386+
if (!type.kind().equals(CelKind.ERROR)) {
387+
if (!type.kind().equals(CelKind.TYPE)) {
375388
// expected type of types
376389
env.reportError(expr.id(), getPosition(expr), "'%s' is not a type", CelTypes.format(type));
377390
} else {
378391
messageType = ((TypeType) type).type();
379-
if (messageType.kind() != CelKind.STRUCT) {
392+
if (!messageType.kind().equals(CelKind.STRUCT)) {
380393
env.reportError(
381394
expr.id(),
382395
getPosition(expr),
@@ -677,14 +690,18 @@ private CelType visitSelectField(
677690
}
678691

679692
if (!Types.isDynOrError(operandType)) {
680-
if (operandType.kind() == CelKind.STRUCT) {
693+
if (operandType.kind().equals(CelKind.STRUCT)) {
681694
TypeProvider.FieldType fieldType =
682695
getFieldType(expr.id(), getPosition(expr), operandType, field);
696+
ProtoMessageType protoMessageType = resolveProtoMessageType(operandType);
697+
if (protoMessageType != null && protoMessageType.isJsonName(field)) {
698+
extensions.add(JSON_NAME_EXTENSION);
699+
}
683700
// Type of the field
684701
resultType = fieldType.celType();
685-
} else if (operandType.kind() == CelKind.MAP) {
702+
} else if (operandType.kind().equals(CelKind.MAP)) {
686703
resultType = ((MapType) operandType).valueType();
687-
} else if (operandType.kind() == CelKind.TYPE_PARAM) {
704+
} else if (operandType.kind().equals(CelKind.TYPE_PARAM)) {
688705
// Mark the operand as type DYN to avoid cases where the free type variable might take on
689706
// an incorrect type if used in multiple locations.
690707
//
@@ -714,6 +731,33 @@ private CelType visitSelectField(
714731
return resultType;
715732
}
716733

734+
private @Nullable ProtoMessageType resolveProtoMessageType(CelType operandType) {
735+
if (operandType instanceof ProtoMessageType) {
736+
return (ProtoMessageType) operandType;
737+
}
738+
739+
if (operandType.kind().equals(CelKind.STRUCT)) {
740+
// This is either a StructTypeReference or just a Struct. Attempt to search for
741+
// ProtoMessageType that may exist in in the type provider.
742+
TypeType typeDef =
743+
typeProvider
744+
.lookupCelType(operandType.name())
745+
.filter(t -> t instanceof TypeType)
746+
.map(TypeType.class::cast)
747+
.orElse(null);
748+
if (typeDef == null || typeDef.parameters().size() != 1) {
749+
return null;
750+
}
751+
752+
CelType maybeProtoMessageType = typeDef.parameters().get(0);
753+
if (maybeProtoMessageType instanceof ProtoMessageType) {
754+
return (ProtoMessageType) maybeProtoMessageType;
755+
}
756+
}
757+
758+
return null;
759+
}
760+
717761
private void visitOptionalCall(CelMutableExpr expr, CelMutableCall call) {
718762
CelMutableExpr operand = call.args().get(0);
719763
CelMutableExpr field = call.args().get(1);

common/src/main/java/dev/cel/common/CelOptions.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ public enum ProtoUnsetFieldOptions {
8383

8484
public abstract boolean enableNamespacedDeclarations();
8585

86+
public abstract boolean enableJsonFieldNames();
87+
8688
// Evaluation related options
8789

8890
public abstract boolean disableCelStandardEquality();
@@ -150,6 +152,7 @@ public static Builder newBuilder() {
150152
.enableTimestampEpoch(false)
151153
.enableHeterogeneousNumericComparisons(false)
152154
.enableNamespacedDeclarations(true)
155+
.enableJsonFieldNames(false)
153156
// Evaluation options
154157
.disableCelStandardEquality(true)
155158
.evaluateCanonicalTypesToNativeValues(false)
@@ -529,6 +532,16 @@ public abstract static class Builder {
529532
*/
530533
public abstract Builder maxRegexProgramSize(int value);
531534

535+
/**
536+
* Use the `json_name` field option on a protobuf message as the name of the field.
537+
*
538+
* <p>If enabled, the compiler will only accept the `json_name` and no longer recognize the
539+
* original protobuf field name. Use with caution as this may break existing expressions during
540+
* compilation. The runtime continues to support both names for maintaining backwards
541+
* compatibility.
542+
*/
543+
public abstract Builder enableJsonFieldNames(boolean value);
544+
532545
public abstract CelOptions build();
533546
}
534547
}

common/src/main/java/dev/cel/common/types/ProtoMessageType.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@
2929
public final class ProtoMessageType extends StructType {
3030

3131
private final StructType.FieldResolver extensionResolver;
32+
private final JsonNameResolver jsonNameResolver;
3233

3334
ProtoMessageType(
3435
String name,
3536
ImmutableSet<String> fieldNames,
3637
StructType.FieldResolver fieldResolver,
37-
StructType.FieldResolver extensionResolver) {
38+
StructType.FieldResolver extensionResolver,
39+
JsonNameResolver jsonNameResolver) {
3840
super(name, fieldNames, fieldResolver);
3941
this.extensionResolver = extensionResolver;
42+
this.jsonNameResolver = jsonNameResolver;
4043
}
4144

4245
/** Find an {@code Extension} by its fully-qualified {@code extensionName}. */
@@ -46,20 +49,35 @@ public Optional<Extension> findExtension(String extensionName) {
4649
.map(type -> Extension.of(extensionName, type, this));
4750
}
4851

52+
/** Returns true if the field name is a json name. */
53+
public boolean isJsonName(String fieldName) {
54+
return jsonNameResolver.isJsonName(fieldName);
55+
}
56+
4957
/**
5058
* Create a new instance of the {@code ProtoMessageType} using the {@code visibleFields} set as a
5159
* mask of the fields from the backing proto.
5260
*/
5361
public ProtoMessageType withVisibleFields(ImmutableSet<String> visibleFields) {
54-
return new ProtoMessageType(name, visibleFields, fieldResolver, extensionResolver);
62+
return new ProtoMessageType(
63+
name, visibleFields, fieldResolver, extensionResolver, jsonNameResolver);
5564
}
5665

5766
public static ProtoMessageType create(
5867
String name,
5968
ImmutableSet<String> fieldNames,
6069
FieldResolver fieldResolver,
61-
FieldResolver extensionResolver) {
62-
return new ProtoMessageType(name, fieldNames, fieldResolver, extensionResolver);
70+
FieldResolver extensionResolver,
71+
JsonNameResolver jsonNameResolver) {
72+
return new ProtoMessageType(
73+
name, fieldNames, fieldResolver, extensionResolver, jsonNameResolver);
74+
}
75+
76+
/** Functional interface for resolving whether a field name is a json name. */
77+
@FunctionalInterface
78+
@Immutable
79+
public interface JsonNameResolver {
80+
boolean isJsonName(String fieldName);
6381
}
6482

6583
/** {@code Extension} contains the name, type, and target message type of the extension. */

0 commit comments

Comments
 (0)