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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bundle/src/test/java/dev/cel/bundle/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ java_library(
"//common:proto_ast",
"//common:source_location",
"//common/ast",
"//common/internal:proto_time_utils",
"//common/resources/testdata/proto3:standalone_global_enum_java_proto",
"//common/testing",
"//common/types",
Expand All @@ -55,6 +54,7 @@ java_library(
"//runtime:evaluation_listener",
"//runtime:function_binding",
"//runtime:unknown_attributes",
"//testing/protos:single_file_java_proto",
"@cel_spec//proto/cel/expr:checked_java_proto",
"@cel_spec//proto/cel/expr:syntax_java_proto",
"@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto",
Expand Down
85 changes: 82 additions & 3 deletions bundle/src/test/java/dev/cel/bundle/CelImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import dev.cel.common.CelIssue;
import dev.cel.common.CelOptions;
import dev.cel.common.CelProtoAbstractSyntaxTree;
import dev.cel.common.CelSource.Extension;
import dev.cel.common.CelSourceLocation;
import dev.cel.common.CelValidationException;
import dev.cel.common.CelValidationResult;
Expand Down Expand Up @@ -112,6 +113,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;
Expand Down Expand Up @@ -743,7 +745,7 @@ public void program_withThrowingFunction() throws Exception {
CelFunctionBinding.from(
"throws",
ImmutableList.of(),
(args) -> {
(unused) -> {
throw new CelEvaluationException("this method always throws");
}))
.setResultType(SimpleType.BOOL)
Expand Down Expand Up @@ -771,7 +773,7 @@ public void program_withThrowingFunctionShortcircuited() throws Exception {
CelFunctionBinding.from(
"throws",
ImmutableList.of(),
(args) -> {
(unused) -> {
throw CelEvaluationExceptionBuilder.newBuilder("this method always throws")
.setCause(new RuntimeException("reason"))
.build();
Expand Down Expand Up @@ -1143,7 +1145,7 @@ public void program_customVarResolver() throws Exception {
program.eval(
(name) -> name.equals("variable") ? Optional.of("hello") : Optional.empty()))
.isEqualTo(true);
assertThat(program.eval((name) -> Optional.of(""))).isEqualTo(false);
assertThat(program.eval((unused) -> Optional.of(""))).isEqualTo(false);
}

@Test
Expand Down Expand Up @@ -2193,6 +2195,83 @@ 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");
}

@Test
public void eval_withJsonFieldName_runtimeOptionDisabled_throws() throws Exception {
CelCompiler celCompiler =
CelCompilerFactory.standardCelCompilerBuilder()
.addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName()))
.addMessageTypes(SingleFile.getDescriptor())
.setOptions(CelOptions.current().enableJsonFieldNames(true).build())
.build();
CelRuntime celRuntime =
CelRuntimeFactory.standardCelRuntimeBuilder()
.addMessageTypes(SingleFile.getDescriptor())
.setOptions(CelOptions.current().enableJsonFieldNames(false).build())
.build();
CelAbstractSyntaxTree ast = celCompiler.compile("file.camelCased").getAst();

CelEvaluationException e =
assertThrows(
CelEvaluationException.class,
() ->
celRuntime
.createProgram(ast)
.eval(ImmutableMap.of("file", SingleFile.getDefaultInstance())));
assertThat(e)
.hasMessageThat()
.contains(
"field 'camelCased' is not declared in message 'dev.cel.testing.testdata.SingleFile");
}

@Test
public void compile_withJsonFieldName_astTagged() 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();

assertThat(ast.getSource().getExtensions())
.contains(
Extension.create(
"json_name", Extension.Version.of(1L, 1L), Extension.Component.COMPONENT_RUNTIME));
}

@Test
public void compile_withJsonFieldName_protoFieldNameComparison_throws() throws Exception {
Cel cel =
standardCelBuilderWithMacros()
.addVar("file", StructTypeReference.create(SingleFile.getDescriptor().getFullName()))
.addMessageTypes(SingleFile.getDescriptor())
.setOptions(CelOptions.current().enableJsonFieldNames(true).build())
.build();

CelValidationException e =
assertThrows(
CelValidationException.class,
() -> cel.compile("file.camelCased == file.snake_cased").getAst());
assertThat(e).hasMessageThat().contains("undefined field 'snake_cased'");
}

private static TypeProvider aliasingProvider(ImmutableMap<String, Type> typeAliases) {
return new TypeProvider() {
@Override
Expand Down
1 change: 1 addition & 0 deletions checker/src/main/java/dev/cel/checker/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ java_library(
":standard_decl",
"//:auto_value",
"//common:cel_ast",
"//common:cel_source",
"//common:compiler_common",
"//common:container",
"//common:operator",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,9 +456,12 @@ public CelCheckerLegacyImpl build() {
}

CelTypeProvider messageTypeProvider =
new ProtoMessageTypeProvider(
CelDescriptorUtil.getAllDescriptorsFromFileDescriptor(
fileTypeSet, celOptions.resolveTypeDependencies()));
ProtoMessageTypeProvider.newBuilder()
.setAllowJsonFieldNames(celOptions.enableJsonFieldNames())
.setResolveTypeDependencies(celOptions.resolveTypeDependencies())
.addFileDescriptors(fileTypeSet)
.build();

if (celTypeProvider != null && fileTypeSet.isEmpty()) {
messageTypeProvider = celTypeProvider;
} else if (celTypeProvider != null) {
Expand Down
59 changes: 52 additions & 7 deletions checker/src/main/java/dev/cel/checker/ExprChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import dev.cel.common.CelFunctionDecl;
import dev.cel.common.CelOverloadDecl;
import dev.cel.common.CelProtoAbstractSyntaxTree;
import dev.cel.common.CelSource;
import dev.cel.common.Operator;
import dev.cel.common.annotations.Internal;
import dev.cel.common.ast.CelConstant;
Expand All @@ -43,12 +44,14 @@
import dev.cel.common.types.ListType;
import dev.cel.common.types.MapType;
import dev.cel.common.types.OptionalType;
import dev.cel.common.types.ProtoMessageType;
import dev.cel.common.types.SimpleType;
import dev.cel.common.types.TypeType;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jspecify.annotations.Nullable;

/**
Expand All @@ -61,6 +64,11 @@
@Internal
@Deprecated
public final class ExprChecker {
private static final CelSource.Extension JSON_NAME_EXTENSION =
CelSource.Extension.create(
"json_name",
CelSource.Extension.Version.of(1, 1),
CelSource.Extension.Component.COMPONENT_RUNTIME);

/**
* Deprecated type-check API.
Expand Down Expand Up @@ -139,7 +147,11 @@ public static CelAbstractSyntaxTree typecheck(
Map<Long, CelType> typeMap =
Maps.transformValues(env.getTypeMap(), checker.inferenceContext::finalize);

return CelAbstractSyntaxTree.newCheckedAst(expr, ast.getSource(), env.getRefMap(), typeMap);
return CelAbstractSyntaxTree.newCheckedAst(
expr,
ast.getSource().toBuilder().addAllExtensions(checker.extensions).build(),
env.getRefMap(),
typeMap);
}

private final Env env;
Expand All @@ -150,6 +162,7 @@ public static CelAbstractSyntaxTree typecheck(
private final boolean compileTimeOverloadResolution;
private final boolean homogeneousLiterals;
private final boolean namespacedDeclarations;
private final Set<CelSource.Extension> extensions;

private ExprChecker(
Env env,
Expand All @@ -167,6 +180,7 @@ private ExprChecker(
this.compileTimeOverloadResolution = compileTimeOverloadResolution;
this.homogeneousLiterals = homogeneousLiterals;
this.namespacedDeclarations = namespacedDeclarations;
this.extensions = new HashSet<>();
}

/** Visit the {@code expr} value, routing to overloads based on the kind of expression. */
Expand Down Expand Up @@ -376,13 +390,13 @@ private CelExpr visit(CelExpr expr, CelExpr.CelStruct struct) {

env.setRef(expr, CelReference.newBuilder().setName(decl.name()).build());
CelType type = decl.type();
if (type.kind() != CelKind.ERROR) {
if (type.kind() != CelKind.TYPE) {
if (!type.kind().equals(CelKind.ERROR)) {
if (!type.kind().equals(CelKind.TYPE)) {
// expected type of types
env.reportError(expr.id(), getPosition(expr), "'%s' is not a type", CelTypes.format(type));
} else {
messageType = ((TypeType) type).type();
if (messageType.kind() != CelKind.STRUCT) {
if (!messageType.kind().equals(CelKind.STRUCT)) {
env.reportError(
expr.id(),
getPosition(expr),
Expand Down Expand Up @@ -726,14 +740,18 @@ private CelType visitSelectField(
}

if (!Types.isDynOrError(operandType)) {
if (operandType.kind() == CelKind.STRUCT) {
if (operandType.kind().equals(CelKind.STRUCT)) {
TypeProvider.FieldType fieldType =
getFieldType(expr.id(), getPosition(expr), operandType, field);
ProtoMessageType protoMessageType = resolveProtoMessageType(operandType);
if (protoMessageType != null && protoMessageType.isJsonName(field)) {
extensions.add(JSON_NAME_EXTENSION);
}
// Type of the field
resultType = fieldType.celType();
} else if (operandType.kind() == CelKind.MAP) {
} else if (operandType.kind().equals(CelKind.MAP)) {
resultType = ((MapType) operandType).valueType();
} else if (operandType.kind() == CelKind.TYPE_PARAM) {
} else if (operandType.kind().equals(CelKind.TYPE_PARAM)) {
// Mark the operand as type DYN to avoid cases where the free type variable might take on
// an incorrect type if used in multiple locations.
//
Expand Down Expand Up @@ -763,6 +781,33 @@ private CelType visitSelectField(
return resultType;
}

private @Nullable ProtoMessageType resolveProtoMessageType(CelType operandType) {
if (operandType instanceof ProtoMessageType) {
return (ProtoMessageType) operandType;
}

if (operandType.kind().equals(CelKind.STRUCT)) {
// This is either a StructTypeReference or just a Struct. Attempt to search for
// ProtoMessageType that may exist in in the type provider.
TypeType typeDef =
typeProvider
.lookupCelType(operandType.name())
.filter(t -> t instanceof TypeType)
.map(TypeType.class::cast)
.orElse(null);
if (typeDef == null || typeDef.parameters().size() != 1) {
return null;
}

CelType maybeProtoMessageType = typeDef.parameters().get(0);
if (maybeProtoMessageType instanceof ProtoMessageType) {
return (ProtoMessageType) maybeProtoMessageType;
}
}

return null;
}

private CelExpr visitOptionalCall(CelExpr expr, CelExpr.CelCall call) {
CelExpr operand = call.args().get(0);
CelExpr field = call.args().get(1);
Expand Down
13 changes: 13 additions & 0 deletions common/src/main/java/dev/cel/common/CelOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ public enum ProtoUnsetFieldOptions {

public abstract boolean enableNamespacedDeclarations();

public abstract boolean enableJsonFieldNames();

// Evaluation related options

public abstract boolean disableCelStandardEquality();
Expand Down Expand Up @@ -150,6 +152,7 @@ public static Builder newBuilder() {
.enableTimestampEpoch(false)
.enableHeterogeneousNumericComparisons(false)
.enableNamespacedDeclarations(true)
.enableJsonFieldNames(false)
// Evaluation options
.disableCelStandardEquality(true)
.evaluateCanonicalTypesToNativeValues(false)
Expand Down Expand Up @@ -529,6 +532,16 @@ public abstract static class Builder {
*/
public abstract Builder maxRegexProgramSize(int value);

/**
* Use the `json_name` field option on a protobuf message as the name of the field.
*
* <p>If enabled, the compiler will only accept the `json_name` and no longer recognize the
* original protobuf field name. Use with caution as this may break existing expressions during
* compilation. The runtime continues to support both names for maintaining backwards
* compatibility.
*/
public abstract Builder enableJsonFieldNames(boolean value);

public abstract CelOptions build();
}
}
26 changes: 22 additions & 4 deletions common/src/main/java/dev/cel/common/types/ProtoMessageType.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@
public final class ProtoMessageType extends StructType {

private final StructType.FieldResolver extensionResolver;
private final JsonNameResolver jsonNameResolver;

ProtoMessageType(
String name,
ImmutableSet<String> fieldNames,
StructType.FieldResolver fieldResolver,
StructType.FieldResolver extensionResolver) {
StructType.FieldResolver extensionResolver,
JsonNameResolver jsonNameResolver) {
super(name, fieldNames, fieldResolver);
this.extensionResolver = extensionResolver;
this.jsonNameResolver = jsonNameResolver;
}

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

/** Returns true if the field name is a json name. */
public boolean isJsonName(String fieldName) {
return jsonNameResolver.isJsonName(fieldName);
}

/**
* Create a new instance of the {@code ProtoMessageType} using the {@code visibleFields} set as a
* mask of the fields from the backing proto.
*/
public ProtoMessageType withVisibleFields(ImmutableSet<String> visibleFields) {
return new ProtoMessageType(name, visibleFields, fieldResolver, extensionResolver);
return new ProtoMessageType(
name, visibleFields, fieldResolver, extensionResolver, jsonNameResolver);
}

public static ProtoMessageType create(
String name,
ImmutableSet<String> fieldNames,
FieldResolver fieldResolver,
FieldResolver extensionResolver) {
return new ProtoMessageType(name, fieldNames, fieldResolver, extensionResolver);
FieldResolver extensionResolver,
JsonNameResolver jsonNameResolver) {
return new ProtoMessageType(
name, fieldNames, fieldResolver, extensionResolver, jsonNameResolver);
}

/** Functional interface for resolving whether a field name is a json name. */
@FunctionalInterface
@Immutable
public interface JsonNameResolver {
boolean isJsonName(String fieldName);
}

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