Skip to content
Merged
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
26 changes: 26 additions & 0 deletions runtime/src/main/java/dev/cel/runtime/planner/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ java_library(
":eval_create_map",
":eval_create_struct",
":eval_or",
":eval_test_only",
":eval_unary",
":eval_var_args_call",
":eval_zero_arity",
Expand Down Expand Up @@ -131,6 +132,16 @@ java_library(
],
)

java_library(
name = "presence_test_qualifier",
srcs = ["PresenceTestQualifier.java"],
deps = [
":attribute",
":qualifier",
"//common/values",
],
)

java_library(
name = "string_qualifier",
srcs = ["StringQualifier.java"],
Expand All @@ -156,6 +167,21 @@ java_library(
],
)

java_library(
name = "eval_test_only",
srcs = ["EvalTestOnly.java"],
deps = [
":interpretable_attribute",
":presence_test_qualifier",
":qualifier",
"//runtime:evaluation_exception",
"//runtime:evaluation_listener",
"//runtime:function_resolver",
"//runtime:interpretable",
"@maven//:com_google_errorprone_error_prone_annotations",
],
)

java_library(
name = "eval_zero_arity",
srcs = ["EvalZeroArity.java"],
Expand Down
68 changes: 68 additions & 0 deletions runtime/src/main/java/dev/cel/runtime/planner/EvalTestOnly.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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.runtime.planner;

import com.google.errorprone.annotations.Immutable;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelEvaluationListener;
import dev.cel.runtime.CelFunctionResolver;
import dev.cel.runtime.GlobalResolver;

@Immutable
final class EvalTestOnly extends InterpretableAttribute {

private final InterpretableAttribute attr;

@Override
public Object eval(GlobalResolver resolver) throws CelEvaluationException {
return attr.eval(resolver);
}

@Override
public Object eval(GlobalResolver resolver, CelEvaluationListener listener) {
// TODO: Implement support
throw new UnsupportedOperationException("Not yet supported");
}

@Override
public Object eval(GlobalResolver resolver, CelFunctionResolver lateBoundFunctionResolver) {
// TODO: Implement support
throw new UnsupportedOperationException("Not yet supported");
}

@Override
public Object eval(
GlobalResolver resolver,
CelFunctionResolver lateBoundFunctionResolver,
CelEvaluationListener listener) {
// TODO: Implement support
throw new UnsupportedOperationException("Not yet supported");
}

@Override
public EvalTestOnly addQualifier(long exprId, Qualifier qualifier) {
PresenceTestQualifier presenceTestQualifier = PresenceTestQualifier.create(qualifier.value());
return new EvalTestOnly(exprId(), attr.addQualifier(exprId, presenceTestQualifier));
}

static EvalTestOnly create(long exprId, InterpretableAttribute attr) {
return new EvalTestOnly(exprId, attr);
}

private EvalTestOnly(long exprId, InterpretableAttribute attr) {
super(exprId);
this.attr = attr;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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.runtime.planner;

import static dev.cel.runtime.planner.MissingAttribute.newMissingAttribute;

import dev.cel.common.values.SelectableValue;
import java.util.Map;

/** A qualifier for presence testing a field or a map key. */
final class PresenceTestQualifier implements Qualifier {

@SuppressWarnings("Immutable")
private final Object value;

@Override
public Object value() {
return value;
}

@Override
@SuppressWarnings("unchecked") // SelectableValue cast is safe
public Object qualify(Object obj) {
if (obj instanceof SelectableValue) {
return ((SelectableValue<Object>) obj).find(value).isPresent();
} else if (obj instanceof Map) {
Map<?, ?> map = (Map<?, ?>) obj;
return map.containsKey(value);
}

return newMissingAttribute(value.toString());
}

static PresenceTestQualifier create(Object value) {
return new PresenceTestQualifier(value);
}

private PresenceTestQualifier(Object value) {
this.value = value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ private PlannedInterpretable planSelect(CelExpr celExpr, PlannerContext ctx) {
}

if (select.testOnly()) {
throw new UnsupportedOperationException("Presence tests not supported yet");
attribute = EvalTestOnly.create(celExpr.id(), attribute);
}

Qualifier qualifier = StringQualifier.create(select.field());
Expand Down
2 changes: 2 additions & 0 deletions runtime/src/test/java/dev/cel/runtime/planner/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ java_library(
"//compiler",
"//compiler:compiler_builder",
"//extensions",
"//parser:macro",
"//runtime",
"//runtime:dispatcher",
"//runtime:function_binding",
Expand All @@ -48,6 +49,7 @@ java_library(
"//runtime/planner:program_planner",
"//runtime/standard:add",
"//runtime/standard:divide",
"//runtime/standard:dyn",
"//runtime/standard:equals",
"//runtime/standard:greater",
"//runtime/standard:greater_equals",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import dev.cel.expr.conformance.proto3.TestAllTypes;
import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage;
import dev.cel.extensions.CelExtensions;
import dev.cel.parser.CelStandardMacro;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelFunctionBinding;
import dev.cel.runtime.CelFunctionOverload;
Expand All @@ -76,6 +77,7 @@
import dev.cel.runtime.standard.AddOperator;
import dev.cel.runtime.standard.CelStandardFunction;
import dev.cel.runtime.standard.DivideOperator;
import dev.cel.runtime.standard.DynFunction;
import dev.cel.runtime.standard.EqualsOperator;
import dev.cel.runtime.standard.GreaterEqualsOperator;
import dev.cel.runtime.standard.GreaterOperator;
Expand Down Expand Up @@ -118,6 +120,7 @@ public final class ProgramPlannerTest {

private static final CelCompiler CEL_COMPILER =
CelCompilerFactory.standardCelCompilerBuilder()
.setStandardMacros(CelStandardMacro.STANDARD_MACROS)
.addVar("msg", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName()))
.addVar("map_var", MapType.create(SimpleType.STRING, SimpleType.DYN))
.addVar("int_var", SimpleType.INT)
Expand Down Expand Up @@ -175,6 +178,10 @@ private static DefaultDispatcher newDispatcher() {
builder,
Operator.NOT_STRICTLY_FALSE.getFunction(),
fromStandardFunction(NotStrictlyFalseFunction.create()));
addBindings(
builder,
"dyn",
fromStandardFunction(DynFunction.create()));

// Custom functions
addBindings(
Expand Down Expand Up @@ -742,6 +749,32 @@ public void plan_select_stringQualificationFail_throws() throws Exception {
+ " performed on messages or maps.");
}

@Test
public void plan_select_presenceTest(@TestParameter PresenceTestCase testCase) throws Exception {
CelAbstractSyntaxTree ast = compile(testCase.expression);
Program program = PLANNER.plan(ast);

boolean result =
(boolean)
program.eval(
ImmutableMap.of("msg", testCase.inputParam, "map_var", testCase.inputParam));

assertThat(result).isEqualTo(testCase.expected);
}

@Test
public void plan_select_badPresenceTest_throws() throws Exception {
CelAbstractSyntaxTree ast = compile("has(dyn([]).invalid)");
Program program = PLANNER.plan(ast);

CelEvaluationException e = assertThrows(CelEvaluationException.class, program::eval);
assertThat(e)
.hasMessageThat()
.contains(
"Error resolving field 'invalid'. Field selections must be performed on messages or"
+ " maps.");
}

private CelAbstractSyntaxTree compile(String expression) throws Exception {
CelAbstractSyntaxTree ast = CEL_COMPILER.parse(expression).getAst();
if (isParseOnly) {
Expand Down Expand Up @@ -814,4 +847,32 @@ private enum TypeLiteralTestCase {
this.type = TypeType.create(type);
}
}


@SuppressWarnings("Immutable") // Test only
private enum PresenceTestCase {
PROTO_FIELD_PRESENT(
"has(msg.single_string)", TestAllTypes.newBuilder().setSingleString("foo").build(), true),
PROTO_FIELD_ABSENT("has(msg.single_string)", TestAllTypes.newBuilder().build(), false),
PROTO_NESTED_FIELD_PRESENT(
"has(msg.single_nested_message.bb)",
TestAllTypes.newBuilder()
.setSingleNestedMessage(NestedMessage.newBuilder().setBb(42).build())
.build(),
true),
PROTO_NESTED_FIELD_ABSENT(
"has(msg.single_nested_message.bb)", TestAllTypes.newBuilder().build(), false),
PROTO_MAP_KEY_PRESENT("has(map_var.foo)", ImmutableMap.of("foo", "1"), true),
PROTO_MAP_KEY_ABSENT("has(map_var.bar)", ImmutableMap.of(), false);

private final String expression;
private final Object inputParam;
private final Object expected;

PresenceTestCase(String expression, Object inputParam, Object expected) {
this.expression = expression;
this.inputParam = inputParam;
this.expected = expected;
}
}
}
Loading