Skip to content

Commit 25684da

Browse files
authored
Register string extension functions to match pv-go (#439)
Currently, protovalidate-java doesn't register the same string extension functions at protovalidate-go, making custom CEL rules not supported across platforms. Register the CEL string extension functions available in cel-java. These are identical to the ones available in protovalidate-go except it is missing a `quote` and `reverse` function. We could implement custom versions but it would be better to get this addressed upstream for consistency across CEL runtimes. Fixes #438.
1 parent 0593835 commit 25684da

File tree

3 files changed

+74
-18
lines changed

3 files changed

+74
-18
lines changed

src/main/java/build/buf/protovalidate/ValidatorImpl.java

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import dev.cel.bundle.Cel;
2222
import dev.cel.bundle.CelFactory;
2323
import dev.cel.common.CelOptions;
24+
import dev.cel.extensions.CelExtensions;
2425
import java.util.ArrayList;
2526
import java.util.List;
2627

@@ -35,29 +36,13 @@ final class ValidatorImpl implements Validator {
3536
private final boolean failFast;
3637

3738
ValidatorImpl(Config config) {
38-
ValidateLibrary validateLibrary = new ValidateLibrary();
39-
Cel cel =
40-
CelFactory.standardCelBuilder()
41-
.addCompilerLibraries(validateLibrary)
42-
.addRuntimeLibraries(validateLibrary)
43-
.setOptions(
44-
CelOptions.DEFAULT.toBuilder().evaluateCanonicalTypesToNativeValues(true).build())
45-
.build();
46-
this.evaluatorBuilder = new EvaluatorBuilder(cel, config);
39+
this.evaluatorBuilder = new EvaluatorBuilder(newCel(), config);
4740
this.failFast = config.isFailFast();
4841
}
4942

5043
ValidatorImpl(Config config, List<Descriptor> descriptors, boolean disableLazy)
5144
throws CompilationException {
52-
ValidateLibrary validateLibrary = new ValidateLibrary();
53-
Cel cel =
54-
CelFactory.standardCelBuilder()
55-
.addCompilerLibraries(validateLibrary)
56-
.addRuntimeLibraries(validateLibrary)
57-
.setOptions(
58-
CelOptions.DEFAULT.toBuilder().evaluateCanonicalTypesToNativeValues(true).build())
59-
.build();
60-
this.evaluatorBuilder = new EvaluatorBuilder(cel, config, descriptors, disableLazy);
45+
this.evaluatorBuilder = new EvaluatorBuilder(newCel(), config, descriptors, disableLazy);
6146
this.failFast = config.isFailFast();
6247
}
6348

@@ -78,4 +63,16 @@ public ValidationResult validate(Message msg) throws ValidationException {
7863
}
7964
return new ValidationResult(violations);
8065
}
66+
67+
private static Cel newCel() {
68+
ValidateLibrary validateLibrary = new ValidateLibrary();
69+
// NOTE: CelExtensions.strings() does not implement string.reverse() or strings.quote() which
70+
// are available in protovalidate-go.
71+
return CelFactory.standardCelBuilder()
72+
.addCompilerLibraries(validateLibrary, CelExtensions.strings())
73+
.addRuntimeLibraries(validateLibrary, CelExtensions.strings())
74+
.setOptions(
75+
CelOptions.DEFAULT.toBuilder().evaluateCanonicalTypesToNativeValues(true).build())
76+
.build();
77+
}
8178
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2023-2025 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package build.buf.protovalidate;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
19+
import build.buf.protovalidate.exceptions.ValidationException;
20+
import build.buf.validate.Violation;
21+
import com.example.noimports.validationtest.ExampleStringExtensions;
22+
import org.junit.jupiter.api.Test;
23+
24+
public class ValidatorStringExtensionsTest {
25+
@Test
26+
public void testStringExtensionsPathMatchesParent() throws ValidationException {
27+
// Test for https://github.com/bufbuild/protovalidate-java/issues/438.
28+
Validator validator = ValidatorFactory.newBuilder().build();
29+
30+
ExampleStringExtensions validMsg =
31+
ExampleStringExtensions.newBuilder().setParent("foo/bar").setPath("foo/bar/baz").build();
32+
ValidationResult result = validator.validate(validMsg);
33+
assertThat(result.isSuccess()).isTrue();
34+
35+
ExampleStringExtensions invalidMsg =
36+
ExampleStringExtensions.newBuilder().setParent("foo/bar").setPath("foo/other/baz").build();
37+
result = validator.validate(invalidMsg);
38+
assertThat(result.isSuccess()).isFalse();
39+
Violation expectedViolation =
40+
Violation.newBuilder()
41+
.setRuleId("path_matches_parent")
42+
.setMessage("path must be a child of parent")
43+
.build();
44+
assertThat(result.toProto().getViolationsList()).containsExactly(expectedViolation);
45+
}
46+
}

src/test/resources/proto/validationtest/validationtest.proto

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,19 @@ message ExampleImportMessageInMap {
109109
map<int64, ExampleImportedMessage> imported_submessage = 1;
110110
}
111111

112+
message ExampleStringExtensions {
113+
string parent = 1;
114+
string path = 2;
115+
116+
option (buf.validate.message).cel = {
117+
id: "path_matches_parent"
118+
message: "path must be a child of parent"
119+
expression:
120+
"this.path.lastIndexOf('/') >= 0 && "
121+
"this.path.substring(0, this.path.lastIndexOf('/')) == this.parent"
122+
};
123+
}
124+
112125
message ExampleImportMessageInMapFieldRule {
113126
ExampleImportMessageInMap message_with_import = 1 [
114127
(buf.validate.field).cel = {

0 commit comments

Comments
 (0)