Skip to content

Commit

Permalink
Documentation, allowUnknownFields, better TypeRegistry behavior, more…
Browse files Browse the repository at this point in the history
… tests
  • Loading branch information
jchadwick-buf committed Sep 24, 2024
1 parent 8d59611 commit 6d47d34
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 24 deletions.
57 changes: 51 additions & 6 deletions src/main/java/build/buf/protovalidate/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@ public final class Config {
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) {
ExtensionRegistry extensionRegistry,
boolean allowUnknownFields) {
this.failFast = failFast;
this.disableLazy = disableLazy;
this.typeRegistry = typeRegistry;
this.extensionRegistry = extensionRegistry;
this.allowUnknownFields = allowUnknownFields;
}

/**
Expand Down Expand Up @@ -67,7 +70,7 @@ public boolean isDisableLazy() {
}

/**
* Gets the registry used for resolving unknown protobuf fields and messages.
* Gets the type registry used for reparsing protobuf messages.
*
* @return a type registry
*/
Expand All @@ -76,20 +79,30 @@ public TypeRegistry getTypeRegistry() {
}

/**
* Gets the registry used for resolving unknown protobuf extensions.
* Gets the extension registry used for resolving unknown protobuf extensions.
*
* @return an extension registry
*/
public ExtensionRegistry getExtensionRegistry() {
return extensionRegistry;
}

/**
* Checks if the configuration for allowing unknown constraint fields is enabled.
*
* @return if allowing unknown constraint fields is enabled
*/
public boolean isAllowUnknownFieldsEnabled() {
return allowUnknownFields;
}

/** 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;

private Builder() {}

Expand All @@ -116,7 +129,18 @@ public Builder setDisableLazy(boolean disableLazy) {
}

/**
* Set the type registry for resolving unknown messages.
* 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
* set to a TypeRegistry with all the message types from your file descriptor set registered. By
* default, if any unknown field constraints are found, compilation of the constraints will
* fail; use setAllowUnknownFields to control this behavior.
*
* <p>Note that the message types for any extensions in setExtensionRegistry must be present in
* the typeRegistry, and have an exactly-equal Descriptor. If the type registry is not set, the
* extension types in the extension registry must have exactly-equal Descriptor types to the
* protovalidate built-in messages. If these conditions are not met, extensions will not be
* resolved as expected. These conditions will be met when constructing a TypeRegistry and
* ExtensionRegistry using information from the same file descriptor sets.
*
* @param typeRegistry the type registry to use
* @return this builder
Expand All @@ -127,7 +151,11 @@ public Builder setTypeRegistry(TypeRegistry typeRegistry) {
}

/**
* Set the extension registry for resolving unknown extensions.
* Set the extension registry for resolving unknown extensions. This option should be set
* alongside setTypeRegistry to allow dynamic resolution of predefined rule extensions. It
* should be set to an ExtensionRegistry with all the extension types from your file descriptor
* set registered. By default, if any unknown field constraints are found, compilation of the
* constraints will fail; use setAllowUnknownFields to control this behavior.
*
* @param extensionRegistry the extension registry to use
* @return this builder
Expand All @@ -137,13 +165,30 @@ public Builder setExtensionRegistry(ExtensionRegistry extensionRegistry) {
return this;
}

/**
* Set whether unknown constraint fields are allowed. If this setting is set to true, unknown
* standard predefined field constraints and predefined field constraint extensions will be
* ignored. This setting defaults to false, which will result in a CompilationException being
* thrown whenever an unknown field constraint is encountered. Setting this to true will cause
* some field constraints to be ignored; if the descriptor is dynamic, you can instead use
* setExtensionRegistry to provide dynamic type information that protovalidate can use to
* resolve the unknown fields.
*
* @param allowUnknownFields setting to apply
* @return this builder
*/
public Builder setAllowUnknownFields(boolean allowUnknownFields) {
this.allowUnknownFields = allowUnknownFields;
return this;
}

/**
* Build the corresponding {@link Config}.
*
* @return the configuration.
*/
public Config build() {
return new Config(failFast, disableLazy, typeRegistry, extensionRegistry);
return new Config(failFast, disableLazy, typeRegistry, extensionRegistry, allowUnknownFields);
}
}
}
12 changes: 10 additions & 2 deletions src/main/java/build/buf/protovalidate/Validator.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ public Validator(Config config) {
Env env = Env.newEnv(Library.Lib(new ValidateLibrary()));
this.evaluatorBuilder =
new EvaluatorBuilder(
env, config.isDisableLazy(), config.getTypeRegistry(), config.getExtensionRegistry());
env,
config.isDisableLazy(),
config.getTypeRegistry(),
config.getExtensionRegistry(),
config.isAllowUnknownFieldsEnabled());
this.failFast = config.isFailFast();
}

Expand All @@ -55,7 +59,11 @@ public Validator() {
Env env = Env.newEnv(Library.Lib(new ValidateLibrary()));
this.evaluatorBuilder =
new EvaluatorBuilder(
env, config.isDisableLazy(), config.getTypeRegistry(), config.getExtensionRegistry());
env,
config.isDisableLazy(),
config.getTypeRegistry(),
config.getExtensionRegistry(),
config.isAllowUnknownFieldsEnabled());
this.failFast = config.isFailFast();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,27 @@ public CelRule(AstExpression astExpression, FieldDescriptor field) {
/** Registry used to resolve dynamic extensions. */
private final ExtensionRegistry extensionRegistry;

/** Whether to allow unknown constraint fields or not. */
private final boolean allowUnknownFields;

/**
* Constructs a new build-through cache for the standard constraints, with a provided registry to
* resolve dynamic extensions.
*
* @param env The CEL environment for evaluation.
* @param typeRegistry A type registry to resolve messages.
* @param extensionRegistry An extension registry to resolve extensions.
* @param allowUnknownFields Whether to allow unknown constraint fields or not.
*/
public ConstraintCache(Env env, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
public ConstraintCache(
Env env,
TypeRegistry typeRegistry,
ExtensionRegistry extensionRegistry,
boolean allowUnknownFields) {
this.env = env;
this.typeRegistry = typeRegistry;
this.extensionRegistry = extensionRegistry;
this.allowUnknownFields = allowUnknownFields;
}

/**
Expand Down Expand Up @@ -285,21 +294,25 @@ private Message resolveConstraints(
// as a Message.
Message typeConstraints = (Message) fieldConstraints.getField(oneofFieldDescriptor);
if (!typeConstraints.getUnknownFields().isEmpty()) {
// If there are unknown fields, try to resolve them using the provided TypeRegistry.
Descriptors.Descriptor expectedConstraintDynamicDescriptor =
// If there are unknown fields, try to resolve them using the provided registries.
Descriptors.Descriptor expectedConstraintMessageDescriptor =
typeRegistry.find(expectedConstraintDescriptor.getMessageType().getFullName());
if (expectedConstraintDynamicDescriptor != null) {
try {
typeConstraints =
DynamicMessage.parseFrom(
expectedConstraintDynamicDescriptor,
typeConstraints.toByteString(),
extensionRegistry);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
if (expectedConstraintMessageDescriptor == null) {
expectedConstraintMessageDescriptor = expectedConstraintDescriptor.getMessageType();
}
try {
typeConstraints =
DynamicMessage.parseFrom(
expectedConstraintMessageDescriptor,
typeConstraints.toByteString(),
extensionRegistry);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
}
if (!allowUnknownFields && !typeConstraints.getUnknownFields().isEmpty()) {
throw new CompilationException("unrecognized field constraints");
}
return typeConstraints;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,20 @@ public class EvaluatorBuilder {
*
* @param env The CEL environment for evaluation.
* @param disableLazy Determines whether lazy loading of evaluators is disabled.
* @param typeRegistry Type registry used for resolving unknown messages.
* @param allowUnknownFields Determines whether unknown constraint fields are allowed.
* @param typeRegistry Type registry used for resolving unknown extensions.
* @param extensionRegistry Extension registry used for resolving unknown extensions.
*/
public EvaluatorBuilder(
Env env,
boolean disableLazy,
TypeRegistry typeRegistry,
ExtensionRegistry extensionRegistry) {
ExtensionRegistry extensionRegistry,
boolean allowUnknownFields) {
this.env = env;
this.disableLazy = disableLazy;
this.constraints = new ConstraintCache(env, typeRegistry, extensionRegistry);
this.constraints =
new ConstraintCache(env, typeRegistry, extensionRegistry, allowUnknownFields);
this.extensionRegistry = extensionRegistry;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@

package build.buf.protovalidate;

import static com.example.imports.validationtest.PredefinedProto.isIdent;
import static org.assertj.core.api.Assertions.assertThat;

import build.buf.validate.Violation;
import com.example.imports.validationtest.ExamplePredefinedFieldConstraints;
import com.example.noimports.validationtest.ExampleFieldConstraints;
import com.example.noimports.validationtest.ExampleMessageConstraints;
import com.example.noimports.validationtest.ExampleOneofConstraints;
import com.example.noimports.validationtest.ExampleRequiredFieldConstraints;
import com.google.protobuf.DescriptorProtos;
import com.google.protobuf.Descriptors;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.google.protobuf.TypeRegistry;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -112,6 +116,43 @@ public void testRequiredFieldConstraintDynamicMessageInvalid() throws Exception
.containsExactly(expectedViolation);
}

@Test
public void testPredefinedFieldConstraintDynamicMessage() throws Exception {
DynamicMessage.Builder messageBuilder =
createMessageWithUnknownOptions(ExamplePredefinedFieldConstraints.getDefaultInstance());
messageBuilder.setField(
messageBuilder.getDescriptorForType().findFieldByName("ident_field"), "abc123");
ExtensionRegistry registry = ExtensionRegistry.newInstance();
registry.add(isIdent);
TypeRegistry typeRegistry =
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();
}

@Test
public void testPredefinedFieldConstraintDynamicMessageInvalid() throws Exception {
DynamicMessage.Builder messageBuilder =
createMessageWithUnknownOptions(ExamplePredefinedFieldConstraints.getDefaultInstance());
messageBuilder.setField(
messageBuilder.getDescriptorForType().findFieldByName("ident_field"), "0123456789");
Violation expectedViolation =
Violation.newBuilder()
.setConstraintId("string.is_ident")
.setFieldPath("ident_field")
.setMessage("invalid identifier")
.build();
ExtensionRegistry registry = ExtensionRegistry.newInstance();
registry.add(isIdent);
TypeRegistry typeRegistry =
TypeRegistry.newBuilder().add(isIdent.getDescriptor().getContainingType()).build();
Config config =
Config.newBuilder().setExtensionRegistry(registry).setTypeRegistry(typeRegistry).build();
assertThat(new Validator(config).validate(messageBuilder.build()).getViolations())
.containsExactly(expectedViolation);
}

private static void gatherDependencies(
Descriptors.FileDescriptor fd, Set<DescriptorProtos.FileDescriptorProto> dependencies) {
dependencies.add(fd.toProto());
Expand Down
32 changes: 32 additions & 0 deletions src/test/resources/proto/validationtest/predefined.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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.

syntax = "proto2";

package validationtest;

import "buf/validate/validate.proto";

extend buf.validate.StringRules {
optional bool is_ident = 1161 [
(buf.validate.predefined).cel = {
id: "string.is_ident",
expression: "(rule && !this.matches('^[a-z0-9]{1,9}$')) ? 'invalid identifier' : ''",
}
];
}

message ExamplePredefinedFieldConstraints {
optional string ident_field = 1 [(buf.validate.field).string.(is_ident) = true];
}

0 comments on commit 6d47d34

Please sign in to comment.