Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/main/java/build/buf/protovalidate/EvaluatorBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.projectnessie.cel.Env;
import org.projectnessie.cel.EnvOption;
Expand Down Expand Up @@ -184,6 +186,31 @@ private void buildMessage(Descriptor desc, MessageEvaluator msgEval)
}
}

private void collectDependencies(Set<Descriptor> dependencyTypes, Descriptor desc) {
dependencyTypes.add(desc);
for (FieldDescriptor field : desc.getFields()) {
if (field.getJavaType() != FieldDescriptor.JavaType.MESSAGE) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, now that you mention it though, I think I probably Do have to cover the map entry case. I'm not sure why I didn't do that last night, maybe I was yet again making the mistake of thinking it's fine since it's a message on the wire. (I keep doing that even though I know it doesn't look that way in the reflection API.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't care about the synthetic message for map entries, but we do care about the message M in map<int32, M>.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, exactly. However, guess what? I just debugged this thoroughly and... this code actually does traverse the synthetic map entry and therefore the value message in maps. I don't really know why, but I'm going to just add the tests showing that it works (validated by stepping through to make sure it's really not working by accident) and just call it for now since this is a rather urgent fix. At least in Java protobuf, the map fields actually do have type MESSAGE.

continue;
}
Descriptor submessageDesc = field.getMessageType();
if (dependencyTypes.contains(submessageDesc)) {
continue;
}
collectDependencies(dependencyTypes, submessageDesc);
}
}

private Message[] getTypesForMessage(Message message) {
Set<Descriptor> dependencyTypes = new HashSet<>();
collectDependencies(dependencyTypes, message.getDescriptorForType());
Message[] dependencyTypeMessages = new Message[dependencyTypes.size()];
int i = 0;
for (Descriptor dependencyType : dependencyTypes) {
dependencyTypeMessages[i++] = DynamicMessage.newBuilder(dependencyType).buildPartial();
}
return dependencyTypeMessages;
}

private void processMessageExpressions(
Descriptor desc, MessageRules msgRules, MessageEvaluator msgEval, DynamicMessage message)
throws CompilationException {
Expand All @@ -193,7 +220,7 @@ private void processMessageExpressions(
}
Env finalEnv =
env.extend(
EnvOption.types(message),
EnvOption.types((Object[]) getTypesForMessage(message)),
EnvOption.declarations(
Decls.newVar(Variable.THIS_NAME, Decls.newObjectType(desc.getFullName()))));
List<CompiledProgram> compiledPrograms = compileRules(celList, finalEnv, false);
Expand Down Expand Up @@ -350,7 +377,7 @@ private void processFieldExpressions(
try {
DynamicMessage defaultInstance =
DynamicMessage.parseFrom(fieldDescriptor.getMessageType(), new byte[0]);
opts.add(EnvOption.types(defaultInstance));
opts.add(EnvOption.types((Object[]) getTypesForMessage(defaultInstance)));
} catch (InvalidProtocolBufferException e) {
throw new CompilationException("field descriptor type is invalid " + e.getMessage(), e);
}
Expand Down
166 changes: 166 additions & 0 deletions src/test/java/build/buf/protovalidate/ValidatorImportTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// 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 com.example.imports.validationtest.ExampleImportMessage;
import com.example.imports.validationtest.ExampleImportMessageFieldRule;
import com.example.imports.validationtest.ExampleImportMessageInMap;
import com.example.imports.validationtest.ExampleImportMessageInMapFieldRule;
import com.example.imports.validationtest.ExampleImportedMessage;
import org.junit.jupiter.api.Test;

public class ValidatorImportTest {
@Test
public void testImportedMessageFromAnotherFile() throws Exception {
com.example.imports.validationtest.ExampleImportMessage valid =
ExampleImportMessage.newBuilder()
.setImportedSubmessage(
ExampleImportedMessage.newBuilder().setHexString("0123456789abcdef").build())
.build();
assertThat(
ValidatorFactory.newBuilder()
.build()
.validate(valid)
.toProto()
.getViolationsList()
.size())
.isEqualTo(0);

com.example.imports.validationtest.ExampleImportMessage invalid =
ExampleImportMessage.newBuilder()
.setImportedSubmessage(ExampleImportedMessage.newBuilder().setHexString("zyx").build())
.build();
assertThat(
ValidatorFactory.newBuilder()
.build()
.validate(invalid)
.toProto()
.getViolationsList()
.size())
.isEqualTo(1);
}

@Test
public void testImportedMessageFromAnotherFileInField() throws Exception {
com.example.imports.validationtest.ExampleImportMessageFieldRule valid =
ExampleImportMessageFieldRule.newBuilder()
.setMessageWithImport(
ExampleImportMessage.newBuilder()
.setImportedSubmessage(
ExampleImportedMessage.newBuilder()
.setHexString("0123456789abcdef")
.build())
.build())
.build();
assertThat(
ValidatorFactory.newBuilder()
.build()
.validate(valid)
.toProto()
.getViolationsList()
.size())
.isEqualTo(0);

com.example.imports.validationtest.ExampleImportMessageFieldRule invalid =
ExampleImportMessageFieldRule.newBuilder()
.setMessageWithImport(
ExampleImportMessage.newBuilder()
.setImportedSubmessage(
ExampleImportedMessage.newBuilder().setHexString("zyx").build())
.build())
.build();
assertThat(
ValidatorFactory.newBuilder()
.build()
.validate(invalid)
.toProto()
.getViolationsList()
.size())
.isEqualTo(1);
}

@Test
public void testImportedMessageFromAnotherFileInMap() throws Exception {
com.example.imports.validationtest.ExampleImportMessageInMap valid =
ExampleImportMessageInMap.newBuilder()
.putImportedSubmessage(
0, ExampleImportedMessage.newBuilder().setHexString("0123456789abcdef").build())
.build();
assertThat(
ValidatorFactory.newBuilder()
.build()
.validate(valid)
.toProto()
.getViolationsList()
.size())
.isEqualTo(0);

com.example.imports.validationtest.ExampleImportMessageInMap invalid =
ExampleImportMessageInMap.newBuilder()
.putImportedSubmessage(
0, ExampleImportedMessage.newBuilder().setHexString("zyx").build())
.build();
assertThat(
ValidatorFactory.newBuilder()
.build()
.validate(invalid)
.toProto()
.getViolationsList()
.size())
.isEqualTo(1);
}

@Test
public void testImportedMessageFromAnotherFileInMapInField() throws Exception {
com.example.imports.validationtest.ExampleImportMessageInMapFieldRule valid =
ExampleImportMessageInMapFieldRule.newBuilder()
.setMessageWithImport(
ExampleImportMessageInMap.newBuilder()
.putImportedSubmessage(
0,
ExampleImportedMessage.newBuilder()
.setHexString("0123456789abcdef")
.build())
.build())
.build();
assertThat(
ValidatorFactory.newBuilder()
.build()
.validate(valid)
.toProto()
.getViolationsList()
.size())
.isEqualTo(0);

com.example.imports.validationtest.ExampleImportMessageInMapFieldRule invalid =
ExampleImportMessageInMapFieldRule.newBuilder()
.setMessageWithImport(
ExampleImportMessageInMap.newBuilder()
.putImportedSubmessage(
0, ExampleImportedMessage.newBuilder().setHexString("zyx").build())
.build())
.build();
assertThat(
ValidatorFactory.newBuilder()
.build()
.validate(invalid)
.toProto()
.getViolationsList()
.size())
.isEqualTo(1);
}
}
23 changes: 23 additions & 0 deletions src/test/resources/proto/validationtest/import_test.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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 = "proto3";

package validationtest;

import "buf/validate/validate.proto";

message ExampleImportedMessage {
string hex_string = 1 [(buf.validate.field).string.pattern = "^[0-9a-fA-F]+$"];
}
10 changes: 4 additions & 6 deletions src/test/resources/proto/validationtest/predefined.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@ 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' : ''",
}
];
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 ExamplePredefinedFieldRules {
Expand Down
57 changes: 56 additions & 1 deletion src/test/resources/proto/validationtest/validationtest.proto
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ syntax = "proto3";
package validationtest;

import "buf/validate/validate.proto";
import "validationtest/import_test.proto";

message ExampleFieldRules {
string regex_string_field = 1 [(buf.validate.field).string.pattern = "^[a-z0-9]{1,9}$"];
Expand All @@ -42,7 +43,7 @@ message ExampleOneofRules {

message ExampleMessageRules {
option (buf.validate.message).cel = {
id: "secondary_email_depends_on_primary",
id: "secondary_email_depends_on_primary"
expression:
"has(this.secondary_email) && !has(this.primary_email)"
"? 'cannot set a secondary email without setting a primary one'"
Expand All @@ -66,3 +67,57 @@ message FieldExpressionMapInt32 {
expression: "this.all(k, this[k] == 1)"
}];
}

message ExampleImportMessage {
option (buf.validate.message) = {
cel: {
id: "imported_submessage_must_not_be_null"
expression: "this.imported_submessage != null"
}
cel: {
id: "hex_string_must_not_be_empty"
expression: "this.imported_submessage.hex_string != ''"
}
};
ExampleImportedMessage imported_submessage = 1;
}

message ExampleImportMessageFieldRule {
ExampleImportMessage message_with_import = 1 [
(buf.validate.field).cel = {
id: "field_must_not_be_null"
expression: "this.imported_submessage != null"
},
(buf.validate.field).cel = {
id: "field_string_must_not_be_empty"
expression: "this.imported_submessage.hex_string != ''"
}
];
}

message ExampleImportMessageInMap {
option (buf.validate.message) = {
cel: {
id: "imported_submessage_must_not_be_null"
expression: "this.imported_submessage[0] != null"
}
cel: {
id: "hex_string_must_not_be_empty"
expression: "this.imported_submessage[0].hex_string != ''"
}
};
map<int64, ExampleImportedMessage> imported_submessage = 1;
}

message ExampleImportMessageInMapFieldRule {
ExampleImportMessageInMap message_with_import = 1 [
(buf.validate.field).cel = {
id: "field_must_not_be_null"
expression: "this.imported_submessage[0] != null"
},
(buf.validate.field).cel = {
id: "field_string_must_not_be_empty"
expression: "this.imported_submessage[0].hex_string != ''"
}
];
}