diff --git a/src/main/java/build/buf/protovalidate/ValidatorImpl.java b/src/main/java/build/buf/protovalidate/ValidatorImpl.java index 62fbd04d3..1e178d482 100644 --- a/src/main/java/build/buf/protovalidate/ValidatorImpl.java +++ b/src/main/java/build/buf/protovalidate/ValidatorImpl.java @@ -21,6 +21,7 @@ import dev.cel.bundle.Cel; import dev.cel.bundle.CelFactory; import dev.cel.common.CelOptions; +import dev.cel.extensions.CelExtensions; import java.util.ArrayList; import java.util.List; @@ -35,29 +36,13 @@ final class ValidatorImpl implements Validator { private final boolean failFast; ValidatorImpl(Config config) { - ValidateLibrary validateLibrary = new ValidateLibrary(); - Cel cel = - CelFactory.standardCelBuilder() - .addCompilerLibraries(validateLibrary) - .addRuntimeLibraries(validateLibrary) - .setOptions( - CelOptions.DEFAULT.toBuilder().evaluateCanonicalTypesToNativeValues(true).build()) - .build(); - this.evaluatorBuilder = new EvaluatorBuilder(cel, config); + this.evaluatorBuilder = new EvaluatorBuilder(newCel(), config); this.failFast = config.isFailFast(); } ValidatorImpl(Config config, List descriptors, boolean disableLazy) throws CompilationException { - ValidateLibrary validateLibrary = new ValidateLibrary(); - Cel cel = - CelFactory.standardCelBuilder() - .addCompilerLibraries(validateLibrary) - .addRuntimeLibraries(validateLibrary) - .setOptions( - CelOptions.DEFAULT.toBuilder().evaluateCanonicalTypesToNativeValues(true).build()) - .build(); - this.evaluatorBuilder = new EvaluatorBuilder(cel, config, descriptors, disableLazy); + this.evaluatorBuilder = new EvaluatorBuilder(newCel(), config, descriptors, disableLazy); this.failFast = config.isFailFast(); } @@ -78,4 +63,16 @@ public ValidationResult validate(Message msg) throws ValidationException { } return new ValidationResult(violations); } + + private static Cel newCel() { + ValidateLibrary validateLibrary = new ValidateLibrary(); + // NOTE: CelExtensions.strings() does not implement string.reverse() or strings.quote() which + // are available in protovalidate-go. + return CelFactory.standardCelBuilder() + .addCompilerLibraries(validateLibrary, CelExtensions.strings()) + .addRuntimeLibraries(validateLibrary, CelExtensions.strings()) + .setOptions( + CelOptions.DEFAULT.toBuilder().evaluateCanonicalTypesToNativeValues(true).build()) + .build(); + } } diff --git a/src/test/java/build/buf/protovalidate/ValidatorStringExtensionsTest.java b/src/test/java/build/buf/protovalidate/ValidatorStringExtensionsTest.java new file mode 100644 index 000000000..72f58fe55 --- /dev/null +++ b/src/test/java/build/buf/protovalidate/ValidatorStringExtensionsTest.java @@ -0,0 +1,46 @@ +// Copyright 2023-2025 Buf Technologies, Inc. +// +// 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 +// +// http://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 build.buf.protovalidate; + +import static org.assertj.core.api.Assertions.assertThat; + +import build.buf.protovalidate.exceptions.ValidationException; +import build.buf.validate.Violation; +import com.example.noimports.validationtest.ExampleStringExtensions; +import org.junit.jupiter.api.Test; + +public class ValidatorStringExtensionsTest { + @Test + public void testStringExtensionsPathMatchesParent() throws ValidationException { + // Test for https://github.com/bufbuild/protovalidate-java/issues/438. + Validator validator = ValidatorFactory.newBuilder().build(); + + ExampleStringExtensions validMsg = + ExampleStringExtensions.newBuilder().setParent("foo/bar").setPath("foo/bar/baz").build(); + ValidationResult result = validator.validate(validMsg); + assertThat(result.isSuccess()).isTrue(); + + ExampleStringExtensions invalidMsg = + ExampleStringExtensions.newBuilder().setParent("foo/bar").setPath("foo/other/baz").build(); + result = validator.validate(invalidMsg); + assertThat(result.isSuccess()).isFalse(); + Violation expectedViolation = + Violation.newBuilder() + .setRuleId("path_matches_parent") + .setMessage("path must be a child of parent") + .build(); + assertThat(result.toProto().getViolationsList()).containsExactly(expectedViolation); + } +} diff --git a/src/test/resources/proto/validationtest/validationtest.proto b/src/test/resources/proto/validationtest/validationtest.proto index 2774145be..1d9007267 100644 --- a/src/test/resources/proto/validationtest/validationtest.proto +++ b/src/test/resources/proto/validationtest/validationtest.proto @@ -109,6 +109,19 @@ message ExampleImportMessageInMap { map imported_submessage = 1; } +message ExampleStringExtensions { + string parent = 1; + string path = 2; + + option (buf.validate.message).cel = { + id: "path_matches_parent" + message: "path must be a child of parent" + expression: + "this.path.lastIndexOf('/') >= 0 && " + "this.path.substring(0, this.path.lastIndexOf('/')) == this.parent" + }; +} + message ExampleImportMessageInMapFieldRule { ExampleImportMessageInMap message_with_import = 1 [ (buf.validate.field).cel = {