Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
5 changes: 2 additions & 3 deletions src/main/java/build/buf/protovalidate/AnyEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import build.buf.validate.FieldPath;
import build.buf.validate.FieldRules;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
Expand Down Expand Up @@ -78,12 +77,12 @@ final class AnyEvaluator implements Evaluator {
@Override
public List<RuleViolation.Builder> evaluate(Value val, boolean failFast)
throws ExecutionException {
Message anyValue = val.messageValue();
MessageReflector anyValue = val.messageValue();
if (anyValue == null) {
return RuleViolation.NO_VIOLATIONS;
}
List<RuleViolation.Builder> violationList = new ArrayList<>();
String typeURL = (String) anyValue.getField(typeURLDescriptor);
String typeURL = anyValue.getField(typeURLDescriptor).jvmValue(String.class);
if (!in.isEmpty() && !in.contains(typeURL)) {
RuleViolation.Builder violation =
RuleViolation.newBuilder()
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/build/buf/protovalidate/CelPrograms.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public boolean tautology() {
@Override
public List<RuleViolation.Builder> evaluate(Value val, boolean failFast)
throws ExecutionException {
CelVariableResolver bindings = Variable.newThisVariable(val.value(Object.class));
CelVariableResolver bindings = Variable.newThisVariable(val.celValue());
List<RuleViolation.Builder> violations = new ArrayList<>();
for (CompiledProgram program : programs) {
RuleViolation.Builder violation = program.eval(val, bindings);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/build/buf/protovalidate/EnumEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public boolean tautology() {
@Override
public List<RuleViolation.Builder> evaluate(Value val, boolean failFast)
throws ExecutionException {
Object enumValue = val.value(Object.class);
Long enumValue = val.jvmValue(Long.class);
if (enumValue == null) {
return RuleViolation.NO_VIOLATIONS;
}
Expand Down
15 changes: 9 additions & 6 deletions src/main/java/build/buf/protovalidate/FieldEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,17 @@ public List<RuleViolation.Builder> evaluate(Value val, boolean failFast)
if (this.shouldIgnoreAlways()) {
return RuleViolation.NO_VIOLATIONS;
}
Message message = val.messageValue();
MessageReflector message = val.messageValue();
if (message == null) {
return RuleViolation.NO_VIOLATIONS;
}
boolean hasField;
if (descriptor.isRepeated()) {
hasField = message.getRepeatedFieldCount(descriptor) != 0;
if (descriptor.isMapField()) {
hasField = !message.getField(descriptor).mapValue().isEmpty();
} else {
hasField = !message.getField(descriptor).repeatedValue().isEmpty();
}
} else {
hasField = message.hasField(descriptor);
}
Expand All @@ -134,11 +138,10 @@ public List<RuleViolation.Builder> evaluate(Value val, boolean failFast)
if (this.shouldIgnoreEmpty() && !hasField) {
return RuleViolation.NO_VIOLATIONS;
}
Object fieldValue = message.getField(descriptor);
if (this.shouldIgnoreDefault()
&& Objects.equals(zero, ProtoAdapter.toCel(descriptor, fieldValue))) {
Value fieldValue = message.getField(descriptor);
if (this.shouldIgnoreDefault() && Objects.equals(zero, fieldValue.jvmValue(Object.class))) {
return RuleViolation.NO_VIOLATIONS;
}
return valueEvaluator.evaluate(new ObjectValue(descriptor, fieldValue), failFast);
return valueEvaluator.evaluate(fieldValue, failFast);
}
}
11 changes: 8 additions & 3 deletions src/main/java/build/buf/protovalidate/ListElementValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,20 @@ final class ListElementValue implements Value {
}

@Override
public @Nullable Message messageValue() {
public @Nullable MessageReflector messageValue() {
if (fieldDescriptor.getJavaType() == Descriptors.FieldDescriptor.JavaType.MESSAGE) {
return (Message) value;
return new ProtobufMessageReflector((Message) value);
}
return null;
}

@Override
public <T> T value(Class<T> clazz) {
public Object celValue() {
return ProtoAdapter.scalarToCel(fieldDescriptor.getType(), value);
}

@Override
public <T> T jvmValue(Class<T> clazz) {
Descriptors.FieldDescriptor.Type type = fieldDescriptor.getType();
if (type == Descriptors.FieldDescriptor.Type.MESSAGE) {
return clazz.cast(value);
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/build/buf/protovalidate/MapEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -155,19 +155,19 @@ private List<RuleViolation.Builder> evalPairs(Value key, Value value, boolean fa
case TYPE_SINT64:
case TYPE_SFIXED32:
case TYPE_SFIXED64:
fieldPathElementBuilder.setIntKey(key.value(Number.class).longValue());
fieldPathElementBuilder.setIntKey(key.jvmValue(Number.class).longValue());
break;
case TYPE_UINT32:
case TYPE_UINT64:
case TYPE_FIXED32:
case TYPE_FIXED64:
fieldPathElementBuilder.setUintKey(key.value(Number.class).longValue());
fieldPathElementBuilder.setUintKey(key.jvmValue(Number.class).longValue());
break;
case TYPE_BOOL:
fieldPathElementBuilder.setBoolKey(key.value(Boolean.class));
fieldPathElementBuilder.setBoolKey(key.jvmValue(Boolean.class));
break;
case TYPE_STRING:
fieldPathElementBuilder.setStringKey(key.value(String.class));
fieldPathElementBuilder.setStringKey(key.jvmValue(String.class));
break;
default:
throw new ExecutionException("Unexpected map key type");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public boolean tautology() {
@Override
public List<RuleViolation.Builder> evaluate(Value val, boolean failFast)
throws ExecutionException {
Message msg = val.messageValue();
MessageReflector msg = val.messageValue();
if (msg == null) {
return RuleViolation.NO_VIOLATIONS;
}
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/build/buf/protovalidate/MessageReflector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2023-2025 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 com.google.protobuf.Descriptors.FieldDescriptor;

/**
* {@link MessageReflector} is a wrapper around a protobuf message that provides reflective access
* to the underlying message.
*
* <p>{@link MessageReflector} is a runtime-independent interface. Any protobuf runtime that
* implements this interface can wrap its messages and, along with their {@link
* com.google.protobuf.Descriptors.Descriptor}s, protovalidate-java will be able to validate them.
*/
public interface MessageReflector {
/**
* Whether the wrapped message has the field described by the provided field descriptor.
*
* @param field The field descriptor to check for.
* @return Whether the field is present.
*/
boolean hasField(FieldDescriptor field);

/**
* Get the value described by the provided field descriptor.
*
* @param field The field descriptor for which to retrieve a value.
* @return The value corresponding to the field descriptor.
*/
Value getField(FieldDescriptor field);
}
17 changes: 11 additions & 6 deletions src/main/java/build/buf/protovalidate/MessageValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
final class MessageValue implements Value {

/** Object type since the object type is inferred from the field descriptor. */
private final Object value;
private final ProtobufMessageReflector value;

/**
* Constructs a {@link MessageValue} with the provided message value.
*
* @param value The message value.
*/
MessageValue(Message value) {
this.value = value;
this.value = new ProtobufMessageReflector(value);
}

@Override
Expand All @@ -42,13 +42,18 @@ final class MessageValue implements Value {
}

@Override
public Message messageValue() {
return (Message) value;
public MessageReflector messageValue() {
return value;
}

@Override
public <T> T value(Class<T> clazz) {
return clazz.cast(value);
public Object celValue() {
return value.getMessage();
}
Comment on lines +50 to +52
Copy link
Member

Choose a reason for hiding this comment

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

The CEL library needs to support the protobuf runtime, otherwise this will never be recognized as the protobuf message.

Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure I understand what you mean here - when the protovalidate runtime (this project) requests the value of this MessageValue object using this method, it's intending to pass the value into the CEL runtime.

Looking at the usage today in CelPrograms:

CelVariableResolver bindings = Variable.newThisVariable(val.value(Object.class));

where the return value of value(Object.class) is just this.value, which is a protobuf Message.

After this PR, this becomes:

CelVariableResolver bindings = Variable.newThisVariable(val.celValue());

which is a more specific request than "cast this to an Object" - it's "please give me this value as it can be interpreted by CEL." The answer happens to be the same, as the new implementation returns value.getMessage(), which is the same protobuf Message value as before.

In the specific case of protokt, I have not implemented custom support for CEL to read protokt's protokt.v1.Message format, so when this is requested from a protokt message we'll wrap the message in a DynamicMessage that CEL can read. It's an area we can optimize further later by providing reflection support for CEL.

Copy link
Member

Choose a reason for hiding this comment

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

In the specific case of protokt, I have not implemented custom support for CEL to read protokt's protokt.v1.Message format, so when this is requested from a protokt message we'll wrap the message in a DynamicMessage that CEL can read. It's an area we can optimize further later by providing reflection support for CEL.

This is what I was referring to, unless the CEL library itself supports alternate runtimes, I am not sure how we can fully support this.

Copy link
Author

Choose a reason for hiding this comment

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

Right now, when asked to evaluate a Message for CEL, the protokt runtime converts it to a protobuf-java DynamicMessage. This is the same thing it does today for the root message object to enable protovalidate today, but is suboptimal since we can access basic fields (i.e., those used in validations evaluated from this library instead of within CEL) without converting the entire message to a DynamicMessage.

The plan was: If we benchmark and find that full message conversion is common and a bottleneck, we'll build native CEL support for protokt's Message interface as an extension in CEL.


@Override
public <T> T jvmValue(Class<T> clazz) {
throw new UnsupportedOperationException();
}

@Override
Expand Down
14 changes: 11 additions & 3 deletions src/main/java/build/buf/protovalidate/ObjectValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,23 @@ public Descriptors.FieldDescriptor fieldDescriptor() {

@Nullable
@Override
public Message messageValue() {
public MessageReflector messageValue() {
if (fieldDescriptor.getJavaType() == Descriptors.FieldDescriptor.JavaType.MESSAGE) {
return (Message) value;
return new ProtobufMessageReflector((Message) value);
}
return null;
}

@Override
public <T> T value(Class<T> clazz) {
public Object celValue() {
return ProtoAdapter.toCel(fieldDescriptor, value);
}

@Override
public <T> T jvmValue(Class<T> clazz) {
if (value instanceof Descriptors.EnumValueDescriptor) {
return clazz.cast((long) ((Descriptors.EnumValueDescriptor) value).getNumber());
}
return clazz.cast(ProtoAdapter.toCel(fieldDescriptor, value));
}

Expand Down
9 changes: 6 additions & 3 deletions src/main/java/build/buf/protovalidate/OneofEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import build.buf.protovalidate.exceptions.ExecutionException;
import build.buf.validate.FieldPathElement;
import com.google.protobuf.Descriptors.OneofDescriptor;
import com.google.protobuf.Message;
import java.util.Collections;
import java.util.List;

Expand Down Expand Up @@ -48,8 +47,12 @@ public boolean tautology() {
@Override
public List<RuleViolation.Builder> evaluate(Value val, boolean failFast)
throws ExecutionException {
Message message = val.messageValue();
if (message == null || !required || (message.getOneofFieldDescriptor(descriptor) != null)) {
MessageReflector message = val.messageValue();
if (message == null) {
return RuleViolation.NO_VIOLATIONS;
}
boolean hasField = descriptor.getFields().stream().anyMatch(message::hasField);
if (!required || hasField) {
return RuleViolation.NO_VIOLATIONS;
}
return Collections.singletonList(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2023-2025 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 com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;

class ProtobufMessageReflector implements MessageReflector {
private final Message message;

ProtobufMessageReflector(Message message) {
this.message = message;
}

public Message getMessage() {
return message;
}

@Override
public boolean hasField(FieldDescriptor field) {
return message.hasField(field);
}

@Override
public Value getField(FieldDescriptor field) {
return new ObjectValue(field, message.getField(field));
}
}
2 changes: 1 addition & 1 deletion src/main/java/build/buf/protovalidate/RuleViolation.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ static class FieldValue implements Violation.FieldValue {
* @param value A {@link Value} to create this {@link FieldValue} from.
*/
FieldValue(Value value) {
this.value = value.value(Object.class);
this.value = value.jvmValue(Object.class);
this.descriptor = Objects.requireNonNull(value.fieldDescriptor());
}

Expand Down
37 changes: 22 additions & 15 deletions src/main/java/build/buf/protovalidate/Value.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package build.buf.protovalidate;

import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
Expand All @@ -24,7 +23,7 @@
* {@link Value} is a wrapper around a protobuf value that provides helper methods for accessing the
* value.
*/
interface Value {
public interface Value {
/**
* Get the field descriptor that corresponds to the underlying Value, if it is a message field.
*
Expand All @@ -34,21 +33,12 @@ interface Value {
Descriptors.@Nullable FieldDescriptor fieldDescriptor();

/**
* Get the underlying value as a {@link Message} type.
* Get the underlying value as a {@link MessageReflector} type.
*
* @return The underlying {@link Message} value. null if the underlying value is not a {@link
* Message} type.
* @return The underlying {@link MessageReflector} value. null if the underlying value is not a {@link
* MessageReflector} type.
*/
@Nullable Message messageValue();

/**
* Get the underlying value and cast it to the class type.
*
* @param clazz The inferred class.
* @return The value casted to the inferred class type.
* @param <T> The class type.
*/
<T> T value(Class<T> clazz);
@Nullable MessageReflector messageValue();

/**
* Get the underlying value as a list.
Expand All @@ -65,4 +55,21 @@ interface Value {
* list.
*/
Map<Value, Value> mapValue();

/**
* Get the underlying value as it should be provided to CEL.
*
* @return The underlying value as a CEL-compatible type.
*/
Object celValue();

/**
* Get the underlying value and cast it to the class type, which will be a type checkable
* internally by protovalidate-java.
*
* @param clazz The inferred class.
* @return The value cast to the inferred class type.
* @param <T> The class type.
*/
<T> T jvmValue(Class<T> clazz);
}
2 changes: 1 addition & 1 deletion src/main/java/build/buf/protovalidate/ValueEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public boolean tautology() {
@Override
public List<RuleViolation.Builder> evaluate(Value val, boolean failFast)
throws ExecutionException {
if (this.shouldIgnore(val.value(Object.class))) {
if (this.shouldIgnore(val.jvmValue(Object.class))) {
return RuleViolation.NO_VIOLATIONS;
}
List<RuleViolation.Builder> allViolations = new ArrayList<>();
Expand Down