Skip to content

Refactor validator creation to support various configurations #283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 14, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, TestResult> resultsMap = new HashMap<>();
for (Map.Entry<String, Any> entry : request.getCasesMap().entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 1 addition & 25 deletions src/main/java/build/buf/protovalidate/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
}
}
24 changes: 21 additions & 3 deletions src/main/java/build/buf/protovalidate/EvaluatorBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Descriptor> 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}.
Expand All @@ -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);
Expand Down
81 changes: 3 additions & 78 deletions src/main/java/build/buf/protovalidate/Validator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<RuleViolation.Builder> result = evaluator.evaluate(new MessageValue(msg), failFast);
if (result.isEmpty()) {
return ValidationResult.EMPTY;
}
List<Violation> 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;
}
102 changes: 102 additions & 0 deletions src/main/java/build/buf/protovalidate/ValidatorFactory.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<Descriptor> 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();
}
}
Loading