diff --git a/conformance/src/main/java/build/buf/protovalidate/conformance/Main.java b/conformance/src/main/java/build/buf/protovalidate/conformance/Main.java index 9c2cd915..d0240912 100644 --- a/conformance/src/main/java/build/buf/protovalidate/conformance/Main.java +++ b/conformance/src/main/java/build/buf/protovalidate/conformance/Main.java @@ -17,6 +17,7 @@ import build.buf.protovalidate.Config; import build.buf.protovalidate.ValidationResult; import build.buf.protovalidate.Validator; +import build.buf.protovalidate.ValidatorFactory; import build.buf.protovalidate.exceptions.CompilationException; import build.buf.protovalidate.exceptions.ExecutionException; import build.buf.validate.ValidateProto; @@ -60,12 +61,13 @@ static TestConformanceResponse testConformance(TestConformanceRequest request) { TypeRegistry typeRegistry = FileDescriptorUtil.createTypeRegistry(fileDescriptorMap.values()); ExtensionRegistry extensionRegistry = FileDescriptorUtil.createExtensionRegistry(fileDescriptorMap.values()); - Validator validator = - new Validator( - Config.newBuilder() - .setTypeRegistry(typeRegistry) - .setExtensionRegistry(extensionRegistry) - .build()); + Config cfg = + Config.newBuilder() + .setTypeRegistry(typeRegistry) + .setExtensionRegistry(extensionRegistry) + .build(); + Validator validator = ValidatorFactory.newBuilder().withConfig(cfg).build(); + TestConformanceResponse.Builder responseBuilder = TestConformanceResponse.newBuilder(); Map resultsMap = new HashMap<>(); for (Map.Entry entry : request.getCasesMap().entrySet()) { diff --git a/conformance/src/test/java/build/buf/protovalidate/ValidatorTest.java b/conformance/src/test/java/build/buf/protovalidate/ValidatorTest.java index e9929c91..d7d55453 100644 --- a/conformance/src/test/java/build/buf/protovalidate/ValidatorTest.java +++ b/conformance/src/test/java/build/buf/protovalidate/ValidatorTest.java @@ -60,7 +60,7 @@ public class ValidatorTest { @BeforeEach public void setUp() { Config config = Config.newBuilder().build(); - validator = new Validator(config); + validator = ValidatorFactory.newBuilder().withConfig(config).build(); } @Test diff --git a/src/main/java/build/buf/protovalidate/Config.java b/src/main/java/build/buf/protovalidate/Config.java index 7c9f3ea7..8c1214b9 100644 --- a/src/main/java/build/buf/protovalidate/Config.java +++ b/src/main/java/build/buf/protovalidate/Config.java @@ -24,19 +24,16 @@ public final class Config { ExtensionRegistry.getEmptyRegistry(); private final boolean failFast; - private final boolean disableLazy; private final TypeRegistry typeRegistry; private final ExtensionRegistry extensionRegistry; private final boolean allowUnknownFields; private Config( boolean failFast, - boolean disableLazy, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry, boolean allowUnknownFields) { this.failFast = failFast; - this.disableLazy = disableLazy; this.typeRegistry = typeRegistry; this.extensionRegistry = extensionRegistry; this.allowUnknownFields = allowUnknownFields; @@ -60,15 +57,6 @@ public boolean isFailFast() { return failFast; } - /** - * Checks if the configuration for disabling lazy evaluation is enabled. - * - * @return if disabling lazy evaluation is enabled - */ - public boolean isDisableLazy() { - return disableLazy; - } - /** * Gets the type registry used for reparsing protobuf messages. * @@ -99,7 +87,6 @@ public boolean isAllowingUnknownFields() { /** Builder for configuration. Provides a forward compatible API for users. */ public static final class Builder { private boolean failFast; - private boolean disableLazy; private TypeRegistry typeRegistry = DEFAULT_TYPE_REGISTRY; private ExtensionRegistry extensionRegistry = DEFAULT_EXTENSION_REGISTRY; private boolean allowUnknownFields; @@ -117,17 +104,6 @@ public Builder setFailFast(boolean failFast) { return this; } - /** - * Set the configuration for disabling lazy evaluation. - * - * @param disableLazy the boolean for enabling - * @return this builder - */ - public Builder setDisableLazy(boolean disableLazy) { - this.disableLazy = disableLazy; - return this; - } - /** * Set the type registry for reparsing protobuf messages. This option should be set alongside * setExtensionRegistry to allow dynamic resolution of predefined rule extensions. It should be @@ -187,7 +163,7 @@ public Builder setAllowUnknownFields(boolean allowUnknownFields) { * @return the configuration. */ public Config build() { - return new Config(failFast, disableLazy, typeRegistry, extensionRegistry, allowUnknownFields); + return new Config(failFast, typeRegistry, extensionRegistry, allowUnknownFields); } } } diff --git a/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java b/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java index 201448b0..b2e05efb 100644 --- a/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java +++ b/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java @@ -59,12 +59,30 @@ class EvaluatorBuilder { * @param env The CEL environment for evaluation. * @param config The configuration to use for the evaluation. */ - public EvaluatorBuilder(Env env, Config config) { + EvaluatorBuilder(Env env, Config config) { this.env = env; - this.disableLazy = config.isDisableLazy(); + this.disableLazy = false; this.rules = new RuleCache(env, config); } + /** + * Constructs a new {@link EvaluatorBuilder}. + * + * @param env The CEL environment for evaluation. + * @param config The configuration to use for the evaluation. + */ + EvaluatorBuilder(Env env, Config config, List descriptors, boolean disableLazy) + throws CompilationException { + Objects.requireNonNull(descriptors, "descriptors must not be null"); + this.env = env; + this.disableLazy = disableLazy; + this.rules = new RuleCache(env, config); + + for (Descriptor descriptor : descriptors) { + this.build(descriptor); + } + } + /** * Returns a pre-cached {@link Evaluator} for the given descriptor or, if the descriptor is * unknown, returns an evaluator that always throws a {@link CompilationException}. @@ -73,7 +91,7 @@ public EvaluatorBuilder(Env env, Config config) { * @return An evaluator for the descriptor type. * @throws CompilationException If an evaluator can't be created for the specified descriptor. */ - public Evaluator load(Descriptor desc) throws CompilationException { + Evaluator load(Descriptor desc) throws CompilationException { Evaluator evaluator = evaluatorCache.get(desc); if (evaluator == null && disableLazy) { return new UnknownDescriptorEvaluator(desc); diff --git a/src/main/java/build/buf/protovalidate/Validator.java b/src/main/java/build/buf/protovalidate/Validator.java index bc6880dc..53cbe2ee 100644 --- a/src/main/java/build/buf/protovalidate/Validator.java +++ b/src/main/java/build/buf/protovalidate/Validator.java @@ -17,43 +17,10 @@ import build.buf.protovalidate.exceptions.CompilationException; import build.buf.protovalidate.exceptions.ExecutionException; import build.buf.protovalidate.exceptions.ValidationException; -import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Message; -import java.util.ArrayList; -import java.util.List; -import org.projectnessie.cel.Env; -import org.projectnessie.cel.Library; - -/** Performs validation on any proto.Message values. The Validator is safe for concurrent use. */ -public class Validator { - /** evaluatorBuilder is the builder used to construct the evaluator for a given message. */ - private final EvaluatorBuilder evaluatorBuilder; - - /** - * failFast indicates whether the validator should stop evaluating rules after the first - * violation. - */ - private final boolean failFast; - - /** - * Constructs a new {@link Validator}. - * - * @param config specified configuration. - */ - public Validator(Config config) { - Env env = Env.newEnv(Library.Lib(new ValidateLibrary())); - this.evaluatorBuilder = new EvaluatorBuilder(env, config); - this.failFast = config.isFailFast(); - } - - /** Constructs a new {@link Validator} with a default configuration. */ - public Validator() { - Config config = Config.newBuilder().build(); - Env env = Env.newEnv(Library.Lib(new ValidateLibrary())); - this.evaluatorBuilder = new EvaluatorBuilder(env, config); - this.failFast = config.isFailFast(); - } +/** A validator that can be used to validate messages */ +public interface Validator { /** * Checks that message satisfies its rules. Rules are defined within the Protobuf file as options * from the buf.validate package. A {@link ValidationResult} is returned which contains a list of @@ -67,47 +34,5 @@ public Validator() { * @return the {@link ValidationResult} from the evaluation. * @throws ValidationException if there are any compilation or validation execution errors. */ - public ValidationResult validate(Message msg) throws ValidationException { - if (msg == null) { - return ValidationResult.EMPTY; - } - Descriptor descriptor = msg.getDescriptorForType(); - Evaluator evaluator = evaluatorBuilder.load(descriptor); - List result = evaluator.evaluate(new MessageValue(msg), failFast); - if (result.isEmpty()) { - return ValidationResult.EMPTY; - } - List violations = new ArrayList<>(result.size()); - for (RuleViolation.Builder builder : result) { - violations.add(builder.build()); - } - return new ValidationResult(violations); - } - - /** - * Loads messages that are expected to be validated, allowing the {@link Validator} to warm up. - * Messages included transitively (i.e., fields with message values) are automatically handled. - * - * @param messages the list of {@link Message} to load. - * @throws CompilationException if there are any compilation errors during warm-up. - */ - public void loadMessages(Message... messages) throws CompilationException { - for (Message message : messages) { - this.evaluatorBuilder.load(message.getDescriptorForType()); - } - } - - /** - * Loads message descriptors that are expected to be validated, allowing the {@link Validator} to - * warm up. Messages included transitively (i.e., fields with message values) are automatically - * handled. - * - * @param descriptors the list of {@link Descriptor} to load. - * @throws CompilationException if there are any compilation errors during warm-up. - */ - public void loadDescriptors(Descriptor... descriptors) throws CompilationException { - for (Descriptor descriptor : descriptors) { - this.evaluatorBuilder.load(descriptor); - } - } + ValidationResult validate(Message msg) throws ValidationException; } diff --git a/src/main/java/build/buf/protovalidate/ValidatorFactory.java b/src/main/java/build/buf/protovalidate/ValidatorFactory.java new file mode 100644 index 00000000..99595e9d --- /dev/null +++ b/src/main/java/build/buf/protovalidate/ValidatorFactory.java @@ -0,0 +1,102 @@ +// Copyright 2023-2024 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 build.buf.protovalidate.exceptions.CompilationException; +import com.google.protobuf.Descriptors.Descriptor; +import java.util.List; +import org.jspecify.annotations.Nullable; + +/** + * ValidatorFactory is used to create a validator. + * + *

Validators can be created with an optional {@link Config} to customize behavior. They can also + * be created with a list of seed descriptors to warmup the validator cache ahead of time as well as + * an indicator to lazily-load any descriptors not provided into the cache. + */ +public final class ValidatorFactory { + // Prevent instantiation + private ValidatorFactory() {} + + /** A builder class used for building a validator. */ + public static class ValidatorBuilder { + /** The config object to use for instantiating a validator. */ + @Nullable private Config config; + + /** + * Create a validator with the given config + * + * @param config The {@link Config} to configure the validator. + * @return The builder instance + */ + public ValidatorBuilder withConfig(Config config) { + this.config = config; + return this; + } + + // Prevent instantiation + private ValidatorBuilder() {} + + /** + * Build a new validator + * + * @return A new {@link Validator} instance. + */ + public Validator build() { + Config cfg = this.config; + if (cfg == null) { + cfg = Config.newBuilder().build(); + } + return new ValidatorImpl(cfg); + } + + /** + * Build the validator, warming up the cache with any provided descriptors. + * + * @param descriptors the list of descriptors to warm up the cache. + * @param disableLazy whether to disable lazy loading of validation rules. When validation is + * performed, a message's rules will be looked up in a cache. If they are not found, by + * default they will be processed and lazily-loaded into the cache. Setting this to false + * will not attempt to lazily-load descriptor information not found in the cache and + * essentially makes the entire cache read-only, eliminating thread contention. + * @return A new {@link Validator} instance. + * @throws CompilationException If any of the given descriptors' validation rules fail + * processing while warming up the cache. + * @throws IllegalStateException If disableLazy is set to true and no descriptors are passed. + */ + public Validator buildWithDescriptors(List descriptors, boolean disableLazy) + throws CompilationException, IllegalStateException { + if (disableLazy && (descriptors == null || descriptors.isEmpty())) { + throw new IllegalStateException( + "a list of descriptors is required when disableLazy is true"); + } + + Config cfg = this.config; + if (cfg == null) { + cfg = Config.newBuilder().build(); + } + return new ValidatorImpl(cfg, descriptors, disableLazy); + } + } + + /** + * Creates a new builder for a validator. + * + * @return A Validator builder + */ + public static ValidatorBuilder newBuilder() { + return new ValidatorBuilder(); + } +} diff --git a/src/main/java/build/buf/protovalidate/ValidatorImpl.java b/src/main/java/build/buf/protovalidate/ValidatorImpl.java new file mode 100644 index 00000000..d40304a9 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/ValidatorImpl.java @@ -0,0 +1,67 @@ +// Copyright 2023-2024 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 build.buf.protovalidate.exceptions.CompilationException; +import build.buf.protovalidate.exceptions.ValidationException; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Message; +import java.util.ArrayList; +import java.util.List; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Library; + +class ValidatorImpl implements Validator { + /** evaluatorBuilder is the builder used to construct the evaluator for a given message. */ + private final EvaluatorBuilder evaluatorBuilder; + + /** + * failFast indicates whether the validator should stop evaluating rules after the first + * violation. + */ + private final boolean failFast; + + ValidatorImpl(Config config) { + Env env = Env.newEnv(Library.Lib(new ValidateLibrary())); + this.evaluatorBuilder = new EvaluatorBuilder(env, config); + this.failFast = config.isFailFast(); + } + + ValidatorImpl(Config config, List descriptors, boolean disableLazy) + throws CompilationException { + Env env = Env.newEnv(Library.Lib(new ValidateLibrary())); + this.evaluatorBuilder = new EvaluatorBuilder(env, config, descriptors, disableLazy); + this.failFast = config.isFailFast(); + } + + /** {@inheritDoc} */ + @Override + public ValidationResult validate(Message msg) throws ValidationException { + if (msg == null) { + return ValidationResult.EMPTY; + } + Descriptor descriptor = msg.getDescriptorForType(); + Evaluator evaluator = evaluatorBuilder.load(descriptor); + List result = evaluator.evaluate(new MessageValue(msg), this.failFast); + if (result.isEmpty()) { + return ValidationResult.EMPTY; + } + List violations = new ArrayList<>(result.size()); + for (RuleViolation.Builder builder : result) { + violations.add(builder.build()); + } + return new ValidationResult(violations); + } +} diff --git a/src/test/java/build/buf/protovalidate/ValidatorCelExpressionTest.java b/src/test/java/build/buf/protovalidate/ValidatorCelExpressionTest.java index b709a680..44288f69 100644 --- a/src/test/java/build/buf/protovalidate/ValidatorCelExpressionTest.java +++ b/src/test/java/build/buf/protovalidate/ValidatorCelExpressionTest.java @@ -75,7 +75,7 @@ public void testFieldExpressionRepeatedMessage() throws Exception { .setMessage("test message field_expression.repeated.message") .build(); - Validator validator = new Validator(); + Validator validator = ValidatorFactory.newBuilder().build(); // Valid message checks ValidationResult validResult = validator.validate(validMsg); @@ -144,7 +144,7 @@ public void testFieldExpressionRepeatedMessageItems() throws Exception { .setMessage("test message field_expression.repeated.message.items") .build(); - Validator validator = new Validator(); + Validator validator = ValidatorFactory.newBuilder().build(); // Valid message checks ValidationResult validResult = validator.validate(validMsg); diff --git a/src/test/java/build/buf/protovalidate/ValidatorConstructionTest.java b/src/test/java/build/buf/protovalidate/ValidatorConstructionTest.java new file mode 100644 index 00000000..85177646 --- /dev/null +++ b/src/test/java/build/buf/protovalidate/ValidatorConstructionTest.java @@ -0,0 +1,266 @@ +// Copyright 2023-2024 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 static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.fail; + +import build.buf.protovalidate.exceptions.ValidationException; +import com.example.imports.validationtest.ExampleFieldRules; +import com.example.imports.validationtest.FieldExpressionMapInt32; +import com.example.imports.validationtest.FieldExpressionMultiple; +import com.google.protobuf.Descriptors.Descriptor; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class ValidatorConstructionTest { + + // Tests validation works as planned with default builder. + @Test + public void testDefaultBuilder() { + Map testMap = new HashMap(); + testMap.put(42, 42); + FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); + + Validator validator = ValidatorFactory.newBuilder().build(); + try { + ValidationResult result = validator.validate(msg); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getViolations().size()).isEqualTo(1); + assertThat(result.getViolations().get(0).toProto().getMessage()) + .isEqualTo("all map values must equal 1"); + } catch (ValidationException e) { + fail("unexpected exception thrown", e); + } + } + + // Tests validation works as planned with default builder and config + @Test + public void testDefaultBuilderWithConfig() { + Map testMap = new HashMap(); + testMap.put(42, 42); + FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); + + Config cfg = Config.newBuilder().setFailFast(true).build(); + Validator validator = ValidatorFactory.newBuilder().withConfig(cfg).build(); + try { + ValidationResult result = validator.validate(msg); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getViolations().size()).isEqualTo(1); + assertThat(result.getViolations().get(0).toProto().getMessage()) + .isEqualTo("all map values must equal 1"); + } catch (ValidationException e) { + fail("unexpected exception thrown", e); + } + } + + // Tests that if the correct seed descriptors are provided and lazy is disabled, + // validation works as planned. + @Test + public void testSeedDescriptorsLazyDisabled() { + Map testMap = new HashMap(); + testMap.put(42, 42); + FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); + + List seedDescriptors = new ArrayList(); + FieldExpressionMapInt32 reg = FieldExpressionMapInt32.newBuilder().build(); + seedDescriptors.add(reg.getDescriptorForType()); + + Config cfg = Config.newBuilder().setFailFast(true).build(); + + // Note that buildWithDescriptors throws the exception so the validator builder + // can be created ahead of time without having to catch an exception. + ValidatorFactory.ValidatorBuilder bldr = ValidatorFactory.newBuilder().withConfig(cfg); + try { + Validator validator = bldr.buildWithDescriptors(seedDescriptors, true); + ValidationResult result = validator.validate(msg); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getViolations().size()).isEqualTo(1); + assertThat(result.getViolations().get(0).toProto().getMessage()) + .isEqualTo("all map values must equal 1"); + } catch (ValidationException e) { + fail("unexpected exception thrown", e); + } + } + + // Tests that the seed descriptor list is immutable inside the validator and that if + // a descriptor is removed after the validator is created, validation still works as planned. + @Test + public void testSeedDescriptorsImmutable() { + Map testMap = new HashMap(); + testMap.put(42, 42); + FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); + + List seedDescriptors = new ArrayList(); + FieldExpressionMapInt32 reg = FieldExpressionMapInt32.newBuilder().build(); + seedDescriptors.add(reg.getDescriptorForType()); + + Config cfg = Config.newBuilder().setFailFast(true).build(); + try { + Validator validator = + ValidatorFactory.newBuilder().withConfig(cfg).buildWithDescriptors(seedDescriptors, true); + + // Remove descriptor from list after the validator is created to verify validation still works + seedDescriptors.clear(); + + ValidationResult result = validator.validate(msg); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getViolations().size()).isEqualTo(1); + assertThat(result.getViolations().get(0).toProto().getMessage()) + .isEqualTo("all map values must equal 1"); + } catch (ValidationException e) { + fail("unexpected exception thrown", e); + } + } + + // Tests that if a message is attempted to be validated and it wasn't in the initial + // list of seed descriptors AND lazy is disabled, that a message is returned that + // no evaluator is available. + @Test + public void testSeedDescriptorsWithWrongDescriptorAndLazyDisabled() { + Map testMap = new HashMap(); + testMap.put(42, 42); + FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); + + List seedDescriptors = new ArrayList(); + ExampleFieldRules wrong = ExampleFieldRules.newBuilder().build(); + seedDescriptors.add(wrong.getDescriptorForType()); + + Config cfg = Config.newBuilder().setFailFast(true).build(); + try { + Validator validator = + ValidatorFactory.newBuilder().withConfig(cfg).buildWithDescriptors(seedDescriptors, true); + ValidationResult result = validator.validate(msg); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getViolations().size()).isEqualTo(1); + assertThat(result.getViolations().get(0).toProto().getMessage()) + .isEqualTo("No evaluator available for " + msg.getDescriptorForType().getFullName()); + } catch (ValidationException e) { + fail("unexpected exception thrown", e); + } + } + + // Tests that an IllegalStateException is thrown if an empty descriptor list is given + // and lazy is disabled. + @Test + public void testEmptySeedDescriptorsInvalidState() { + List seedDescriptors = new ArrayList(); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy( + () -> { + ValidatorFactory.newBuilder().buildWithDescriptors(seedDescriptors, true); + }); + } + + // Tests that an IllegalStateException is thrown if a null descriptor list is given + // and lazy is disabled. + @Test + public void testNullSeedDescriptorsInvalidState() { + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy( + () -> { + ValidatorFactory.newBuilder().buildWithDescriptors(null, true); + }); + } + + // Tests that when an empty list of seed descriptors is provided and lazy is enabled + // that the missing message descriptor is successfully built and validation works as planned. + @Test + public void testEmptySeedDescriptorsLazyEnabled() { + Map testMap = new HashMap(); + testMap.put(42, 42); + FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); + + List seedDescriptors = new ArrayList(); + Config cfg = Config.newBuilder().setFailFast(true).build(); + try { + Validator validator = + ValidatorFactory.newBuilder() + .withConfig(cfg) + .buildWithDescriptors(seedDescriptors, false); + ValidationResult result = validator.validate(msg); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getViolations().size()).isEqualTo(1); + assertThat(result.getViolations().get(0).toProto().getMessage()) + .isEqualTo("all map values must equal 1"); + } catch (ValidationException e) { + fail("unexpected exception thrown", e); + } + } + + // Tests that when a null list of seed descriptors is provided, a NullPointerException + // is thrown with a message that descriptors cannot be null. + @Test + public void testNullSeedDescriptorsLazyEnabled() { + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy( + () -> { + ValidatorFactory.newBuilder().buildWithDescriptors(null, false); + }) + .withMessageContaining("descriptors must not be null"); + ; + } + + // Tests that the config is applied when building a validator. + @Test + public void testConfigApplied() { + // Value must be at most 5 characters and must be lowercase alpha chars or numbers. + FieldExpressionMultiple msg = FieldExpressionMultiple.newBuilder().setVal("INVALID").build(); + + // Set fail fast to true, so we exit after the first validation failure. + Config cfg = Config.newBuilder().setFailFast(true).build(); + try { + Validator validator = ValidatorFactory.newBuilder().withConfig(cfg).build(); + ValidationResult result = validator.validate(msg); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getViolations().size()).isEqualTo(1); + assertThat(result.getViolations().get(0).toProto().getMessage()) + .isEqualTo("value length must be at most 5 characters"); + } catch (ValidationException e) { + fail("unexpected exception thrown", e); + } + } + + // Tests that the config is applied when building a validator with seed descriptors. + @Test + public void testConfigAppliedWithSeedDescriptors() { + // Value must be at most 5 characters and must be lowercase alpha chars or numbers. + FieldExpressionMultiple msg = FieldExpressionMultiple.newBuilder().setVal("INVALID").build(); + + FieldExpressionMultiple desc = FieldExpressionMultiple.newBuilder().build(); + List seedDescriptors = new ArrayList(); + seedDescriptors.add(desc.getDescriptorForType()); + + // Set fail fast to true, so we exit after the first validation failure. + Config cfg = Config.newBuilder().setFailFast(true).build(); + try { + Validator validator = + ValidatorFactory.newBuilder() + .withConfig(cfg) + .buildWithDescriptors(seedDescriptors, false); + ValidationResult result = validator.validate(msg); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getViolations().size()).isEqualTo(1); + assertThat(result.getViolations().get(0).toProto().getMessage()) + .isEqualTo("value length must be at most 5 characters"); + } catch (ValidationException e) { + fail("unexpected exception thrown", e); + } + } +} diff --git a/src/test/java/build/buf/protovalidate/ValidatorDifferentJavaPackagesTest.java b/src/test/java/build/buf/protovalidate/ValidatorDifferentJavaPackagesTest.java index 917f9c43..35b2642e 100644 --- a/src/test/java/build/buf/protovalidate/ValidatorDifferentJavaPackagesTest.java +++ b/src/test/java/build/buf/protovalidate/ValidatorDifferentJavaPackagesTest.java @@ -186,7 +186,7 @@ private void expectViolation(Message msg, Violation violation) throws Validation } private void expectViolations(Message msg, List expected) throws ValidationException { - Validator validator = new Validator(); + Validator validator = ValidatorFactory.newBuilder().build(); List violations = validator.validate(msg).toProto().getViolationsList(); assertThat(violations).containsExactlyInAnyOrderElementsOf(expected); } diff --git a/src/test/java/build/buf/protovalidate/ValidatorDynamicMessageTest.java b/src/test/java/build/buf/protovalidate/ValidatorDynamicMessageTest.java index 1b0a542d..fc7fd73b 100644 --- a/src/test/java/build/buf/protovalidate/ValidatorDynamicMessageTest.java +++ b/src/test/java/build/buf/protovalidate/ValidatorDynamicMessageTest.java @@ -78,7 +78,8 @@ public void testFieldRuleDynamicMessage() throws Exception { .setRuleId("string.pattern") .setMessage("value does not match regex pattern `^[a-z0-9]{1,9}$`") .build(); - ValidationResult result = new Validator().validate(messageBuilder.build()); + ValidationResult result = + ValidatorFactory.newBuilder().build().validate(messageBuilder.build()); assertThat(result.toProto().getViolationsList()).containsExactly(expectedViolation); assertThat(result.getViolations().get(0).getFieldValue().getValue()).isEqualTo("0123456789"); assertThat(result.getViolations().get(0).getRuleValue().getValue()) @@ -97,7 +98,12 @@ public void testOneofRuleDynamicMessage() throws Exception { .setRuleId("required") .setMessage("exactly one field is required in oneof") .build(); - assertThat(new Validator().validate(messageBuilder.build()).toProto().getViolationsList()) + assertThat( + ValidatorFactory.newBuilder() + .build() + .validate(messageBuilder.build()) + .toProto() + .getViolationsList()) .containsExactly(expectedViolation); } @@ -113,7 +119,12 @@ public void testMessageRuleDynamicMessage() throws Exception { .setRuleId("secondary_email_depends_on_primary") .setMessage("cannot set a secondary email without setting a primary one") .build(); - assertThat(new Validator().validate(messageBuilder.build()).toProto().getViolationsList()) + assertThat( + ValidatorFactory.newBuilder() + .build() + .validate(messageBuilder.build()) + .toProto() + .getViolationsList()) .containsExactly(expectedViolation); } @@ -123,7 +134,9 @@ public void testRequiredFieldRuleDynamicMessage() throws Exception { createMessageWithUnknownOptions(ExampleRequiredFieldRules.getDefaultInstance()); messageBuilder.setField( messageBuilder.getDescriptorForType().findFieldByName("regex_string_field"), "abc123"); - assertThat(new Validator().validate(messageBuilder.build()).getViolations()).isEmpty(); + assertThat( + ValidatorFactory.newBuilder().build().validate(messageBuilder.build()).getViolations()) + .isEmpty(); } @Test @@ -154,7 +167,12 @@ public void testRequiredFieldRuleDynamicMessageInvalid() throws Exception { .setRuleId("string.pattern") .setMessage("value does not match regex pattern `^[a-z0-9]{1,9}$`") .build(); - assertThat(new Validator().validate(messageBuilder.build()).toProto().getViolationsList()) + assertThat( + ValidatorFactory.newBuilder() + .build() + .validate(messageBuilder.build()) + .toProto() + .getViolationsList()) .containsExactly(expectedViolation); } @@ -170,7 +188,13 @@ public void testPredefinedFieldRuleDynamicMessage() throws Exception { TypeRegistry.newBuilder().add(isIdent.getDescriptor().getContainingType()).build(); Config config = Config.newBuilder().setExtensionRegistry(registry).setTypeRegistry(typeRegistry).build(); - assertThat(new Validator(config).validate(messageBuilder.build()).getViolations()).isEmpty(); + assertThat( + ValidatorFactory.newBuilder() + .withConfig(config) + .build() + .validate(messageBuilder.build()) + .getViolations()) + .isEmpty(); } @Test @@ -203,7 +227,13 @@ public void testPredefinedFieldRuleDynamicMessageInvalid() throws Exception { TypeRegistry.newBuilder().add(isIdent.getDescriptor().getContainingType()).build(); Config config = Config.newBuilder().setExtensionRegistry(registry).setTypeRegistry(typeRegistry).build(); - assertThat(new Validator(config).validate(messageBuilder.build()).toProto().getViolationsList()) + assertThat( + ValidatorFactory.newBuilder() + .withConfig(config) + .build() + .validate(messageBuilder.build()) + .toProto() + .getViolationsList()) .containsExactly(expectedViolation); } diff --git a/src/test/resources/proto/validationtest/validationtest.proto b/src/test/resources/proto/validationtest/validationtest.proto index 68d5c0d4..b73409b4 100644 --- a/src/test/resources/proto/validationtest/validationtest.proto +++ b/src/test/resources/proto/validationtest/validationtest.proto @@ -51,3 +51,18 @@ message ExampleMessageRules { string primary_email = 1; string secondary_email = 2; } + +message FieldExpressionMultiple { + string val = 1 [ + (buf.validate.field).string.max_len = 5, + (buf.validate.field).string.pattern = "^[a-z0-9]$" + ]; +} + +message FieldExpressionMapInt32 { + map val = 1 [(buf.validate.field).cel = { + id: "field_expression.map.int32" + message: "all map values must equal 1" + expression: "this.all(k, this[k] == 1)" + }]; +}