Skip to content

Commit f52b5e2

Browse files
committed
POC: JsonFieldName support
1 parent ff8cd30 commit f52b5e2

26 files changed

Lines changed: 443 additions & 79 deletions

File tree

.bazelversion

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
8.5.0

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ java_library(
6767
"@maven//:com_google_truth_extensions_truth_proto_extension",
6868
"@maven//:junit_junit",
6969
"@maven//:org_jspecify_jspecify",
70+
"//testing/protos:single_file_java_proto",
7071
],
7172
)
7273

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

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration;
2020
import static dev.cel.common.CelOverloadDecl.newGlobalOverload;
2121
import static dev.cel.common.CelOverloadDecl.newMemberOverload;
22+
import static dev.cel.common.CelSource.Extension;
2223
import static org.junit.Assert.assertThrows;
2324

25+
import dev.cel.common.CelValidationException;
2426
import dev.cel.expr.CheckedExpr;
2527
import dev.cel.expr.Constant;
28+
import dev.cel.runtime.CelEvaluationException;
2629
import dev.cel.expr.Decl;
2730
import dev.cel.expr.Decl.FunctionDecl;
2831
import dev.cel.expr.Decl.FunctionDecl.Overload;
@@ -47,15 +50,13 @@
4750
import com.google.protobuf.Descriptors.FileDescriptor;
4851
import com.google.protobuf.Duration;
4952
import com.google.protobuf.DynamicMessage;
50-
import com.google.protobuf.Empty;
5153
import com.google.protobuf.ExtensionRegistry;
5254
import com.google.protobuf.FieldMask;
5355
import com.google.protobuf.Message;
5456
import com.google.protobuf.Struct;
5557
import com.google.protobuf.TextFormat;
5658
import com.google.protobuf.Timestamp;
5759
import com.google.protobuf.TypeRegistry;
58-
import com.google.protobuf.WrappersProto;
5960
import com.google.rpc.context.AttributeContext;
6061
import com.google.testing.junit.testparameterinjector.TestParameter;
6162
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
@@ -72,7 +73,6 @@
7273
import dev.cel.common.CelOptions;
7374
import dev.cel.common.CelProtoAbstractSyntaxTree;
7475
import dev.cel.common.CelSourceLocation;
75-
import dev.cel.common.CelValidationException;
7676
import dev.cel.common.CelValidationResult;
7777
import dev.cel.common.CelVarDecl;
7878
import dev.cel.common.ast.CelExpr;
@@ -102,7 +102,6 @@
102102
import dev.cel.runtime.CelAttribute;
103103
import dev.cel.runtime.CelAttribute.Qualifier;
104104
import dev.cel.runtime.CelAttributePattern;
105-
import dev.cel.runtime.CelEvaluationException;
106105
import dev.cel.runtime.CelEvaluationExceptionBuilder;
107106
import dev.cel.runtime.CelFunctionBinding;
108107
import dev.cel.runtime.CelRuntime;
@@ -112,6 +111,7 @@
112111
import dev.cel.runtime.CelUnknownSet;
113112
import dev.cel.runtime.CelVariableResolver;
114113
import dev.cel.runtime.UnknownContext;
114+
import dev.cel.testing.testdata.SingleFileProto.SingleFile;
115115
import dev.cel.testing.testdata.proto3.StandaloneGlobalEnum;
116116
import java.time.Instant;
117117
import java.util.ArrayList;
@@ -2193,6 +2193,74 @@ public void toBuilder_isImmutable() {
21932193
assertThat(newRuntimeBuilder).isNotEqualTo(celImpl.toRuntimeBuilder());
21942194
}
21952195

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

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,9 +456,13 @@ 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+
.setCelDescriptors(
462+
CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(
463+
fileTypeSet, celOptions.resolveTypeDependencies()))
464+
.build();
465+
462466
if (celTypeProvider != null && fileTypeSet.isEmpty()) {
463467
messageTypeProvider = celTypeProvider;
464468
} else if (celTypeProvider != null) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package dev.cel.checker;
1616

17+
import dev.cel.common.types.TypeType;
1718
import dev.cel.expr.Constant;
1819
import dev.cel.expr.Decl;
1920
import dev.cel.expr.Decl.FunctionDecl.Overload;
@@ -491,7 +492,7 @@ public Env add(String name, Type type) {
491492

492493
// Next try to import the name as a reference to a message type.
493494
// This is done via the type provider.
494-
Optional<CelType> type = typeProvider.lookupCelType(cand);
495+
Optional<TypeType> type = typeProvider.lookupCelType(cand);
495496
if (type.isPresent()) {
496497
decl = CelIdentDecl.newIdentDeclaration(cand, type.get());
497498
decls.get(0).putIdent(decl);

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

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import static com.google.common.base.Preconditions.checkNotNull;
1818

19+
import dev.cel.common.types.StructType;
1920
import dev.cel.expr.CheckedExpr;
2021
import dev.cel.expr.ParsedExpr;
2122
import dev.cel.expr.Type;
@@ -31,6 +32,7 @@
3132
import dev.cel.common.CelFunctionDecl;
3233
import dev.cel.common.CelOverloadDecl;
3334
import dev.cel.common.CelProtoAbstractSyntaxTree;
35+
import dev.cel.common.CelSource;
3436
import dev.cel.common.Operator;
3537
import dev.cel.common.annotations.Internal;
3638
import dev.cel.common.ast.CelConstant;
@@ -44,9 +46,11 @@
4446
import dev.cel.common.types.MapType;
4547
import dev.cel.common.types.OptionalType;
4648
import dev.cel.common.types.SimpleType;
49+
import dev.cel.common.types.ProtoMessageType;
4750
import dev.cel.common.types.TypeType;
4851
import java.util.ArrayList;
4952
import java.util.HashSet;
53+
import java.util.Set;
5054
import java.util.List;
5155
import java.util.Map;
5256
import org.jspecify.annotations.Nullable;
@@ -139,7 +143,7 @@ public static CelAbstractSyntaxTree typecheck(
139143
Map<Long, CelType> typeMap =
140144
Maps.transformValues(env.getTypeMap(), checker.inferenceContext::finalize);
141145

142-
return CelAbstractSyntaxTree.newCheckedAst(expr, ast.getSource(), env.getRefMap(), typeMap);
146+
return CelAbstractSyntaxTree.newCheckedAst(expr, ast.getSource().toBuilder().addAllExtensions(checker.extensions).build(), env.getRefMap(), typeMap);
143147
}
144148

145149
private final Env env;
@@ -150,6 +154,7 @@ public static CelAbstractSyntaxTree typecheck(
150154
private final boolean compileTimeOverloadResolution;
151155
private final boolean homogeneousLiterals;
152156
private final boolean namespacedDeclarations;
157+
private final Set<CelSource.Extension> extensions = new HashSet<>();
153158

154159
private ExprChecker(
155160
Env env,
@@ -376,13 +381,13 @@ private CelExpr visit(CelExpr expr, CelExpr.CelStruct struct) {
376381

377382
env.setRef(expr, CelReference.newBuilder().setName(decl.name()).build());
378383
CelType type = decl.type();
379-
if (type.kind() != CelKind.ERROR) {
380-
if (type.kind() != CelKind.TYPE) {
384+
if (!type.kind().equals(CelKind.ERROR)) {
385+
if (!type.kind().equals(CelKind.TYPE)) {
381386
// expected type of types
382387
env.reportError(expr.id(), getPosition(expr), "'%s' is not a type", CelTypes.format(type));
383388
} else {
384389
messageType = ((TypeType) type).type();
385-
if (messageType.kind() != CelKind.STRUCT) {
390+
if (!messageType.kind().equals(CelKind.STRUCT)) {
386391
env.reportError(
387392
expr.id(),
388393
getPosition(expr),
@@ -726,14 +731,18 @@ private CelType visitSelectField(
726731
}
727732

728733
if (!Types.isDynOrError(operandType)) {
729-
if (operandType.kind() == CelKind.STRUCT) {
734+
if (operandType.kind().equals(CelKind.STRUCT)) {
730735
TypeProvider.FieldType fieldType =
731736
getFieldType(expr.id(), getPosition(expr), operandType, field);
737+
ProtoMessageType protoMessageType = resolveProtoMessageType(operandType);
738+
if (protoMessageType != null && protoMessageType.isJsonName(field)) {
739+
extensions.add(CelSource.Extension.create("json_name", CelSource.Extension.Version.of(1, 1), CelSource.Extension.Component.COMPONENT_RUNTIME));
740+
}
732741
// Type of the field
733742
resultType = fieldType.celType();
734-
} else if (operandType.kind() == CelKind.MAP) {
743+
} else if (operandType.kind().equals(CelKind.MAP)) {
735744
resultType = ((MapType) operandType).valueType();
736-
} else if (operandType.kind() == CelKind.TYPE_PARAM) {
745+
} else if (operandType.kind().equals(CelKind.TYPE_PARAM)) {
737746
// Mark the operand as type DYN to avoid cases where the free type variable might take on
738747
// an incorrect type if used in multiple locations.
739748
//
@@ -763,6 +772,28 @@ private CelType visitSelectField(
763772
return resultType;
764773
}
765774

775+
private @Nullable ProtoMessageType resolveProtoMessageType(CelType operandType) {
776+
if (operandType instanceof ProtoMessageType) {
777+
return (ProtoMessageType) operandType;
778+
}
779+
780+
if (operandType.kind().equals(CelKind.STRUCT)) {
781+
// This is either a StructTypeReference or just a Struct. Attempt to search for ProtoMessageType that may exist in
782+
// in the type provider.
783+
TypeType typeDef = typeProvider.lookupCelType(operandType.name()).orElse(null);
784+
if (typeDef == null || typeDef.parameters().size() != 1) {
785+
return null;
786+
}
787+
788+
CelType maybeProtoMessageType = typeDef.parameters().get(0);
789+
if (maybeProtoMessageType instanceof ProtoMessageType) {
790+
return (ProtoMessageType) maybeProtoMessageType;
791+
}
792+
}
793+
794+
return null;
795+
}
796+
766797
private CelExpr visitOptionalCall(CelExpr expr, CelExpr.CelCall call) {
767798
CelExpr operand = call.args().get(0);
768799
CelExpr field = call.args().get(1);

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package dev.cel.checker;
1616

17+
import dev.cel.common.types.TypeType;
1718
import dev.cel.expr.Type;
1819
import com.google.auto.value.AutoValue;
1920
import com.google.common.collect.ImmutableList;
@@ -36,9 +37,9 @@ public interface TypeProvider {
3637
@Nullable Type lookupType(String typeName);
3738

3839
/** Lookup the a {@link CelType} given a qualified {@code typeName}. Returns null if not found. */
39-
default Optional<CelType> lookupCelType(String typeName) {
40+
default Optional<TypeType> lookupCelType(String typeName) {
4041
Type type = lookupType(typeName);
41-
return Optional.ofNullable(type).map(CelProtoTypes::typeToCelType);
42+
return Optional.ofNullable(type).map(CelProtoTypes::typeToCelType).filter(t -> t instanceof TypeType).map(TypeType.class::cast);
4243
}
4344

4445
/** Lookup the {@code Integer} enum value given an {@code enumName}. Returns null if not found. */

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ final class TypeProviderLegacyImpl implements TypeProvider {
4949
}
5050

5151
@Override
52-
public Optional<CelType> lookupCelType(String typeName) {
52+
public Optional<TypeType> lookupCelType(String typeName) {
5353
return celTypeProvider.findType(typeName).map(TypeType::create);
5454
}
5555

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

Lines changed: 16 additions & 1 deletion
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)
@@ -170,7 +173,8 @@ public static Builder newBuilder() {
170173
.enableStringConcatenation(true)
171174
.enableListConcatenation(true)
172175
.enableComprehension(true)
173-
.maxRegexProgramSize(-1);
176+
.maxRegexProgramSize(-1)
177+
;
174178
}
175179

176180
/**
@@ -529,6 +533,17 @@ public abstract static class Builder {
529533
*/
530534
public abstract Builder maxRegexProgramSize(int value);
531535

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

0 commit comments

Comments
 (0)