Skip to content

Commit 3bc58f7

Browse files
committed
unified matcher and CEL
1 parent 9d5842b commit 3bc58f7

19 files changed

Lines changed: 2679 additions & 4 deletions

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ checkstyle = "com.puppycrawl.tools:checkstyle:10.26.1"
3434
# checkstyle 10.0+ requires Java 11+
3535
# See https://checkstyle.sourceforge.io/releasenotes_old_8-35_10-26.html#Release_10.0
3636
# checkForUpdates: checkstylejava8:9.+
37+
cel-runtime = "dev.cel:runtime:0.11.1"
38+
cel-compiler = "dev.cel:compiler:0.11.1"
3739
checkstylejava8 = "com.puppycrawl.tools:checkstyle:9.3"
3840
commons-math3 = "org.apache.commons:commons-math3:3.6.1"
3941
conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2"

xds/build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ dependencies {
5656
libraries.re2j,
5757
libraries.auto.value.annotations,
5858
libraries.protobuf.java.util
59+
implementation(libraries.cel.runtime) {
60+
exclude group: 'com.google.protobuf', module: 'protobuf-java'
61+
}
62+
implementation(libraries.cel.compiler) {
63+
exclude group: 'com.google.protobuf', module: 'protobuf-java'
64+
}
5965
def nettyDependency = implementation project(':grpc-netty')
6066

6167
testImplementation project(':grpc-api')
@@ -175,13 +181,15 @@ tasks.named("javadoc").configure {
175181
exclude 'io/grpc/xds/XdsNameResolverProvider.java'
176182
exclude 'io/grpc/xds/internal/**'
177183
exclude 'io/grpc/xds/Internal*'
184+
exclude 'dev/cel/**'
178185
}
179186

180187
def prefixName = 'io.grpc.xds'
181188
tasks.named("shadowJar").configure {
182189
archiveClassifier = null
183190
dependencies {
184191
include(project(':grpc-xds'))
192+
include(dependency('dev.cel:.*'))
185193
}
186194
// Relocated packages commonly need exclusions in jacocoTestReport and javadoc
187195
// Keep in sync with BUILD.bazel's JAR_JAR_RULES
@@ -198,6 +206,8 @@ tasks.named("shadowJar").configure {
198206
// TODO: missing java_package option in .proto
199207
relocate 'udpa.annotations', "${prefixName}.shaded.udpa.annotations"
200208
relocate 'xds.annotations', "${prefixName}.shaded.xds.annotations"
209+
relocate 'dev.cel', "${prefixName}.shaded.dev.cel"
210+
relocate 'cel', "${prefixName}.shaded.cel"
201211
exclude "**/*.proto"
202212
}
203213

xds/src/main/java/io/grpc/xds/internal/MatcherParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public static Matchers.StringMatcher parseStringMatcher(
9090
return Matchers.StringMatcher.forSafeRegEx(
9191
Pattern.compile(proto.getSafeRegex().getRegex()));
9292
case CONTAINS:
93-
return Matchers.StringMatcher.forContains(proto.getContains());
93+
return Matchers.StringMatcher.forContains(proto.getContains(), proto.getIgnoreCase());
9494
case MATCHPATTERN_NOT_SET:
9595
default:
9696
throw new IllegalArgumentException(

xds/src/main/java/io/grpc/xds/internal/Matchers.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,15 @@ public static StringMatcher forSafeRegEx(Pattern regEx) {
257257
}
258258

259259
/** The input string should contain this substring. */
260-
public static StringMatcher forContains(String contains) {
260+
public static StringMatcher forContains(String contains, boolean ignoreCase) {
261261
checkNotNull(contains, "contains");
262262
return StringMatcher.create(null, null, null, null, contains,
263-
false/* doesn't matter */);
263+
ignoreCase);
264+
}
265+
266+
/** The input string should contain this substring. */
267+
public static StringMatcher forContains(String contains) {
268+
return forContains(contains, false);
264269
}
265270

266271
/** Returns the matching result for this string. */
@@ -281,7 +286,9 @@ public boolean matches(String args) {
281286
? args.toLowerCase(Locale.ROOT).endsWith(suffix().toLowerCase(Locale.ROOT))
282287
: args.endsWith(suffix());
283288
} else if (contains() != null) {
284-
return args.contains(contains());
289+
return ignoreCase()
290+
? args.toLowerCase(Locale.ROOT).contains(contains().toLowerCase(Locale.ROOT))
291+
: args.contains(contains());
285292
}
286293
return regEx().matches(args);
287294
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2026 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.xds.internal.matcher;
18+
19+
import com.google.common.annotations.VisibleForTesting;
20+
import dev.cel.common.CelAbstractSyntaxTree;
21+
import dev.cel.common.CelOptions;
22+
import dev.cel.common.CelValidationException;
23+
import dev.cel.common.types.SimpleType;
24+
import dev.cel.compiler.CelCompiler;
25+
import dev.cel.compiler.CelCompilerFactory;
26+
import dev.cel.runtime.CelEvaluationException;
27+
import dev.cel.runtime.CelRuntime;
28+
import dev.cel.runtime.CelRuntimeFactory;
29+
30+
31+
/**
32+
* Executes compiled CEL expressions.
33+
*/
34+
public final class CelMatcher {
35+
36+
private static final CelOptions CEL_OPTIONS = CelOptions.newBuilder()
37+
.enableComprehension(false)
38+
.enableStringConversion(false)
39+
.enableStringConcatenation(false)
40+
.enableListConcatenation(false)
41+
.maxRegexProgramSize(100)
42+
.build();
43+
44+
private static final CelCompiler COMPILER = CelCompilerFactory.standardCelCompilerBuilder()
45+
.addVar("request", SimpleType.DYN)
46+
.setOptions(CEL_OPTIONS)
47+
.build();
48+
49+
private static final CelRuntime RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder()
50+
.setOptions(CEL_OPTIONS)
51+
.build();
52+
53+
private final CelRuntime.Program program;
54+
55+
private CelMatcher(CelRuntime.Program program) {
56+
this.program = program;
57+
}
58+
59+
/**
60+
* Compiles the AST into a CelMatcher.
61+
*/
62+
public static CelMatcher compile(CelAbstractSyntaxTree ast)
63+
throws CelValidationException, CelEvaluationException {
64+
if (ast.getResultType() != SimpleType.BOOL) {
65+
throw new IllegalArgumentException(
66+
"CEL expression must evaluate to boolean, got: " + ast.getResultType());
67+
}
68+
checkAllowedVariables(ast);
69+
CelRuntime.Program program = RUNTIME.createProgram(ast);
70+
return new CelMatcher(program);
71+
}
72+
73+
/**
74+
* Compiles the CEL expression string into a CelMatcher.
75+
*/
76+
@VisibleForTesting
77+
public static CelMatcher compile(String expression)
78+
throws CelValidationException, CelEvaluationException {
79+
CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst();
80+
return compile(ast);
81+
}
82+
83+
/**
84+
* Evaluates the CEL expression against the input activation.
85+
*/
86+
public boolean match(Object input) throws CelEvaluationException {
87+
Object result;
88+
if (input instanceof dev.cel.runtime.CelVariableResolver) {
89+
result = program.eval((dev.cel.runtime.CelVariableResolver) input);
90+
} else if (input instanceof java.util.Map) {
91+
@SuppressWarnings("unchecked")
92+
java.util.Map<String, ?> mapInput = (java.util.Map<String, ?>) input;
93+
result = program.eval(mapInput);
94+
} else {
95+
throw new CelEvaluationException(
96+
"Unsupported input type for CEL evaluation: " + input.getClass().getName());
97+
}
98+
// Validated to be boolean during compile check ideally, or we cast safely
99+
if (result instanceof Boolean) {
100+
return (Boolean) result;
101+
}
102+
throw new CelEvaluationException(
103+
"CEL expression must evaluate to boolean, got: " + result.getClass().getName());
104+
}
105+
106+
private static void checkAllowedVariables(CelAbstractSyntaxTree ast) {
107+
// Basic validation to ensure only supported variables (request) are used.
108+
// This iterates over the reference map generated by the type checker.
109+
for (java.util.Map.Entry<Long, dev.cel.common.ast.CelReference> entry :
110+
ast.getReferenceMap().entrySet()) {
111+
dev.cel.common.ast.CelReference ref = entry.getValue();
112+
// If overload_id is empty, it's a variable reference or type name.
113+
// We only support "request".
114+
if (ref.value() == null && ref.overloadIds().isEmpty()) {
115+
if (!"request".equals(ref.name())) {
116+
throw new IllegalArgumentException(
117+
"CEL expression references unknown variable: " + ref.name());
118+
}
119+
}
120+
}
121+
}
122+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2026 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.xds.internal.matcher;
18+
19+
import dev.cel.common.CelAbstractSyntaxTree;
20+
import dev.cel.common.CelOptions;
21+
import dev.cel.common.CelValidationException;
22+
import dev.cel.common.types.SimpleType;
23+
import dev.cel.compiler.CelCompiler;
24+
import dev.cel.compiler.CelCompilerFactory;
25+
import dev.cel.runtime.CelEvaluationException;
26+
import dev.cel.runtime.CelRuntime;
27+
import dev.cel.runtime.CelRuntimeFactory;
28+
29+
/**
30+
* Executes compiled CEL expressions that extract a string.
31+
*/
32+
public final class CelStringExtractor {
33+
34+
private static final CelOptions CEL_OPTIONS = CelOptions.newBuilder()
35+
.enableComprehension(false)
36+
.enableStringConversion(false)
37+
.enableStringConcatenation(false)
38+
.enableListConcatenation(false)
39+
.maxRegexProgramSize(100)
40+
.build();
41+
42+
private static final CelCompiler COMPILER = CelCompilerFactory.standardCelCompilerBuilder()
43+
.addVar("request", SimpleType.DYN)
44+
.setOptions(CEL_OPTIONS)
45+
.build();
46+
47+
private static final CelRuntime RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder()
48+
.setOptions(CEL_OPTIONS)
49+
.build();
50+
51+
private final CelRuntime.Program program;
52+
53+
private CelStringExtractor(CelRuntime.Program program) {
54+
this.program = program;
55+
}
56+
57+
/**
58+
* Compiles the AST into a CelStringExtractor.
59+
*/
60+
public static CelStringExtractor compile(CelAbstractSyntaxTree ast)
61+
throws CelValidationException, CelEvaluationException {
62+
if (ast.getResultType() != SimpleType.STRING && ast.getResultType() != SimpleType.DYN) {
63+
throw new IllegalArgumentException(
64+
"CEL expression must evaluate to string, got: " + ast.getResultType());
65+
}
66+
checkAllowedVariables(ast);
67+
CelRuntime.Program program = RUNTIME.createProgram(ast);
68+
return new CelStringExtractor(program);
69+
}
70+
71+
/**
72+
* Compiles the CEL expression string into a CelStringExtractor.
73+
*/
74+
public static CelStringExtractor compile(String expression)
75+
throws CelValidationException, CelEvaluationException {
76+
CelAbstractSyntaxTree ast = COMPILER.compile(expression).getAst();
77+
return compile(ast);
78+
}
79+
80+
/**
81+
* Evaluates the CEL expression against the input activation and returns the string result.
82+
* Returns null if the result is not a string.
83+
*/
84+
public String extract(Object input) throws CelEvaluationException {
85+
Object result;
86+
if (input instanceof dev.cel.runtime.CelVariableResolver) {
87+
result = program.eval((dev.cel.runtime.CelVariableResolver) input);
88+
} else if (input instanceof java.util.Map) {
89+
@SuppressWarnings("unchecked")
90+
java.util.Map<String, ?> mapInput = (java.util.Map<String, ?>) input;
91+
result = program.eval(mapInput);
92+
} else {
93+
throw new CelEvaluationException(
94+
"Unsupported input type for CEL evaluation: " + input.getClass().getName());
95+
}
96+
97+
if (result instanceof String) {
98+
return (String) result;
99+
}
100+
// Return null key for non-string results (which will likely match nothing or be handled)
101+
return null;
102+
}
103+
104+
private static void checkAllowedVariables(CelAbstractSyntaxTree ast) {
105+
for (java.util.Map.Entry<Long, dev.cel.common.ast.CelReference> entry :
106+
ast.getReferenceMap().entrySet()) {
107+
dev.cel.common.ast.CelReference ref = entry.getValue();
108+
if (ref.value() == null && ref.overloadIds().isEmpty()) {
109+
if (!"request".equals(ref.name())) {
110+
throw new IllegalArgumentException(
111+
"CEL expression references unknown variable: " + ref.name());
112+
}
113+
}
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)