Skip to content
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