-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve support for dynamic messages (#48)
Update protovalidate-java to determine if the field, message, or oneof options contains an unknown field for the protovalidate extension. If so, reparse the options type to correctly interpret the options and enable validation. This will enable protovalidate to run when the inputs are a FileDescriptorSet (with preserved options).
- Loading branch information
Showing
6 changed files
with
179 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
src/test/java/build/buf/protovalidate/ValidatorDynamicMessageTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
// Copyright 2023 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.validate.Violation; | ||
import com.example.noimports.validationtest.ExampleFieldConstraints; | ||
import com.example.noimports.validationtest.ExampleMessageConstraints; | ||
import com.example.noimports.validationtest.ExampleOneofConstraints; | ||
import com.google.protobuf.DescriptorProtos; | ||
import com.google.protobuf.Descriptors; | ||
import com.google.protobuf.DynamicMessage; | ||
import com.google.protobuf.InvalidProtocolBufferException; | ||
import com.google.protobuf.Message; | ||
import java.util.LinkedHashSet; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.function.Function; | ||
import java.util.stream.Collectors; | ||
import org.junit.Test; | ||
|
||
/** | ||
* This test mimics the behavior when performing validation with protovalidate on a file descriptor | ||
* set (as created by <code>protoc --retain_options --descriptor_set_out=...</code>). These | ||
* descriptor types have the protovalidate extensions as unknown fields and need to be parsed with | ||
* an extension registry for the constraints to be recognized and validated. | ||
*/ | ||
public class ValidatorDynamicMessageTest { | ||
|
||
@Test | ||
public void testFieldConstraintDynamicMessage() throws Exception { | ||
DynamicMessage.Builder messageBuilder = | ||
createMessageWithUnknownOptions(ExampleFieldConstraints.getDefaultInstance()); | ||
messageBuilder.setField( | ||
messageBuilder.getDescriptorForType().findFieldByName("regex_string_field"), "0123456789"); | ||
Violation expectedViolation = | ||
Violation.newBuilder() | ||
.setConstraintId("string.pattern") | ||
.setFieldPath("regex_string_field") | ||
.setMessage("value does not match regex pattern `^[a-z0-9]{1,9}$`") | ||
.build(); | ||
assertThat(new Validator().validate(messageBuilder.build()).getViolations()) | ||
.containsExactly(expectedViolation); | ||
} | ||
|
||
@Test | ||
public void testOneofConstraintDynamicMessage() throws Exception { | ||
DynamicMessage.Builder messageBuilder = | ||
createMessageWithUnknownOptions(ExampleOneofConstraints.getDefaultInstance()); | ||
Violation expectedViolation = | ||
Violation.newBuilder() | ||
.setFieldPath("contact_info") | ||
.setConstraintId("required") | ||
.setMessage("exactly one field is required in oneof") | ||
.build(); | ||
assertThat(new Validator().validate(messageBuilder.build()).getViolations()) | ||
.containsExactly(expectedViolation); | ||
} | ||
|
||
@Test | ||
public void testMessageConstraintDynamicMessage() throws Exception { | ||
DynamicMessage.Builder messageBuilder = | ||
createMessageWithUnknownOptions(ExampleMessageConstraints.getDefaultInstance()); | ||
messageBuilder.setField( | ||
messageBuilder.getDescriptorForType().findFieldByName("secondary_email"), | ||
"[email protected]"); | ||
Violation expectedViolation = | ||
Violation.newBuilder() | ||
.setConstraintId("secondary_email_depends_on_primary") | ||
.setMessage("cannot set a secondary email without setting a primary one") | ||
.build(); | ||
assertThat(new Validator().validate(messageBuilder.build()).getViolations()) | ||
.containsExactly(expectedViolation); | ||
} | ||
|
||
private static void gatherDependencies( | ||
Descriptors.FileDescriptor fd, Set<DescriptorProtos.FileDescriptorProto> dependencies) { | ||
dependencies.add(fd.toProto()); | ||
for (Descriptors.FileDescriptor dependency : fd.getDependencies()) { | ||
gatherDependencies(dependency, dependencies); | ||
} | ||
} | ||
|
||
private static DescriptorProtos.FileDescriptorSet createFileDescriptorSetForMessage( | ||
Descriptors.Descriptor message) { | ||
DescriptorProtos.FileDescriptorSet.Builder builder = | ||
DescriptorProtos.FileDescriptorSet.newBuilder(); | ||
Set<DescriptorProtos.FileDescriptorProto> dependencies = new LinkedHashSet<>(); | ||
gatherDependencies(message.getFile(), dependencies); | ||
builder.addAllFile(dependencies); | ||
return builder.build(); | ||
} | ||
|
||
private static Descriptors.FileDescriptor getFileDescriptor( | ||
String name, Map<String, DescriptorProtos.FileDescriptorProto> fds) | ||
throws Descriptors.DescriptorValidationException { | ||
DescriptorProtos.FileDescriptorProto fdProto = fds.get(name); | ||
if (fdProto == null) { | ||
throw new IllegalArgumentException("unable to file file descriptor proto: " + name); | ||
} | ||
Descriptors.FileDescriptor[] dependencies = | ||
new Descriptors.FileDescriptor[fdProto.getDependencyCount()]; | ||
for (int i = 0; i < fdProto.getDependencyCount(); i++) { | ||
dependencies[i] = getFileDescriptor(fdProto.getDependency(i), fds); | ||
} | ||
return Descriptors.FileDescriptor.buildFrom(fdProto, dependencies); | ||
} | ||
|
||
private static DynamicMessage.Builder createMessageWithUnknownOptions(Message message) | ||
throws InvalidProtocolBufferException, Descriptors.DescriptorValidationException { | ||
DescriptorProtos.FileDescriptorSet fds = | ||
createFileDescriptorSetForMessage(message.getDescriptorForType()); | ||
// Reparse file descriptor set from encoded form (loses known extensions). | ||
fds = DescriptorProtos.FileDescriptorSet.parseFrom(fds.toByteArray()); | ||
Map<String, DescriptorProtos.FileDescriptorProto> fdsMap = | ||
fds.getFileList().stream() | ||
.collect( | ||
Collectors.toMap( | ||
DescriptorProtos.FileDescriptorProto::getName, Function.identity())); | ||
Descriptors.FileDescriptor descriptor = | ||
getFileDescriptor(message.getDescriptorForType().getFile().getName(), fdsMap); | ||
return DynamicMessage.newBuilder( | ||
descriptor.findMessageTypeByName(message.getDescriptorForType().getName())); | ||
} | ||
} |