Skip to content

Introduce MessageReflector to enable alternative protobuf runtimes #202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
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-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 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package build.buf.protovalidate.internal.evaluator;
package build.buf.protovalidate;

import com.google.protobuf.Message;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
Expand All @@ -25,22 +24,13 @@
*/
public interface Value {
/**
* 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);
MessageReflector messageValue();

/**
* Get the underlying value as a list.
Expand All @@ -57,4 +47,22 @@ public 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.
*/
@Nullable
<T> T jvmValue(Class<T> clazz);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@

package build.buf.protovalidate.internal.evaluator;

import build.buf.protovalidate.MessageReflector;
import build.buf.protovalidate.ValidationResult;
import build.buf.protovalidate.Value;
import build.buf.protovalidate.exceptions.ExecutionException;
import build.buf.validate.Violation;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
Expand All @@ -45,12 +46,12 @@ class AnyEvaluator implements Evaluator {

@Override
public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException {
Message anyValue = val.messageValue();
MessageReflector anyValue = val.messageValue();
if (anyValue == null) {
return ValidationResult.EMPTY;
}
List<Violation> violationList = new ArrayList<>();
String typeURL = (String) anyValue.getField(typeURLDescriptor);
String typeURL = anyValue.getField(typeURLDescriptor).jvmValue(String.class);
if (!in.isEmpty() && !in.contains(typeURL)) {
Violation violation =
Violation.newBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package build.buf.protovalidate.internal.evaluator;

import build.buf.protovalidate.ValidationResult;
import build.buf.protovalidate.Value;
import build.buf.protovalidate.exceptions.ExecutionException;
import build.buf.validate.Violation;
import com.google.protobuf.Descriptors;
Expand Down Expand Up @@ -62,11 +63,11 @@ public boolean tautology() {
*/
@Override
public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException {
Descriptors.EnumValueDescriptor enumValue = val.value(Descriptors.EnumValueDescriptor.class);
Integer enumValue = val.jvmValue(Integer.class);
if (enumValue == null) {
return ValidationResult.EMPTY;
}
if (!values.contains(enumValue.getNumber())) {
if (!values.contains(enumValue)) {
return new ValidationResult(
Collections.singletonList(
Violation.newBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package build.buf.protovalidate.internal.evaluator;

import build.buf.protovalidate.ValidationResult;
import build.buf.protovalidate.Value;
import build.buf.protovalidate.exceptions.ExecutionException;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@

package build.buf.protovalidate.internal.evaluator;

import build.buf.protovalidate.MessageReflector;
import build.buf.protovalidate.ValidationResult;
import build.buf.protovalidate.Value;
import build.buf.protovalidate.exceptions.ExecutionException;
import build.buf.validate.Violation;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -68,13 +69,17 @@ public boolean tautology() {

@Override
public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException {
Message message = val.messageValue();
MessageReflector message = val.messageValue();
if (message == null) {
return ValidationResult.EMPTY;
}
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 @@ -90,12 +95,11 @@ public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionEx
if (ignoreEmpty && !hasField) {
return ValidationResult.EMPTY;
}
Object fieldValue = message.getField(descriptor);
if (ignoreDefault && Objects.equals(zero, fieldValue)) {
Value fieldValue = message.getField(descriptor);
if (ignoreDefault && Objects.equals(zero, fieldValue.jvmValue(Object.class))) {
return ValidationResult.EMPTY;
}
ValidationResult evalResult =
valueEvaluator.evaluate(new ObjectValue(descriptor, fieldValue), failFast);
ValidationResult evalResult = valueEvaluator.evaluate(fieldValue, failFast);
List<Violation> violations =
ErrorPathUtils.prefixErrorPaths(evalResult.getViolations(), "%s", descriptor.getName());
return new ValidationResult(violations);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package build.buf.protovalidate.internal.evaluator;

import build.buf.protovalidate.MessageReflector;
import build.buf.protovalidate.Value;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
Expand All @@ -25,11 +27,8 @@
import javax.annotation.Nullable;
import org.projectnessie.cel.common.ULong;

/**
* The {@link build.buf.protovalidate.internal.evaluator.Value} type that contains a field
* descriptor and its value.
*/
public final class ObjectValue implements Value {
/** The {@link Value} type that contains a field descriptor and its value. */
public final class FieldValue implements Value {

/**
* {@link com.google.protobuf.Descriptors.FieldDescriptor} is the field descriptor for the value.
Expand All @@ -40,27 +39,38 @@ public final class ObjectValue implements Value {
private final Object value;

/**
* Constructs a new {@link build.buf.protovalidate.internal.evaluator.ObjectValue}.
* Constructs a new {@link FieldValue}.
*
* @param fieldDescriptor The field descriptor for the value.
* @param value The value associated with the field descriptor.
*/
ObjectValue(Descriptors.FieldDescriptor fieldDescriptor, Object value) {
FieldValue(Descriptors.FieldDescriptor fieldDescriptor, Object value) {
this.fieldDescriptor = fieldDescriptor;
this.value = value;
}

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

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

@Override
public Object celValue() {
if (value instanceof Descriptors.EnumValueDescriptor) {
return ((Descriptors.EnumValueDescriptor) value).getNumber();
}
Descriptors.FieldDescriptor.Type type = fieldDescriptor.getType();
if (!fieldDescriptor.isRepeated()
&& (type == Descriptors.FieldDescriptor.Type.UINT32
Expand All @@ -74,9 +84,9 @@ public <T> T value(Class<T> clazz) {
* When using uint32/uint64 in your protobuf objects or CEL expressions in Java,
* wrap them with the org.projectnessie.cel.common.ULong type.
*/
return clazz.cast(ULong.valueOf(((Number) value).longValue()));
return ULong.valueOf(((Number) value).longValue());
}
return clazz.cast(value);
return value;
}

@Override
Expand All @@ -85,7 +95,7 @@ public List<Value> repeatedValue() {
if (fieldDescriptor.isRepeated()) {
List<?> list = (List<?>) value;
for (Object o : list) {
out.add(new build.buf.protovalidate.internal.evaluator.ObjectValue(fieldDescriptor, o));
out.add(new FieldValue(fieldDescriptor, o));
}
}
return out;
Expand All @@ -103,12 +113,10 @@ public Map<Value, Value> mapValue() {
Map<Value, Value> out = new HashMap<>(input.size());
for (AbstractMessage entry : input) {
Object keyValue = entry.getField(keyDesc);
Value keyJavaValue =
new build.buf.protovalidate.internal.evaluator.ObjectValue(keyDesc, keyValue);
Value keyJavaValue = new FieldValue(keyDesc, keyValue);

Object valValue = entry.getField(valDesc);
Value valJavaValue =
new build.buf.protovalidate.internal.evaluator.ObjectValue(valDesc, valValue);
Value valJavaValue = new FieldValue(valDesc, valValue);

out.put(keyJavaValue, valJavaValue);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package build.buf.protovalidate.internal.evaluator;

import build.buf.protovalidate.ValidationResult;
import build.buf.protovalidate.Value;
import build.buf.protovalidate.exceptions.ExecutionException;
import build.buf.validate.Violation;
import java.util.ArrayList;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package build.buf.protovalidate.internal.evaluator;

import build.buf.protovalidate.ValidationResult;
import build.buf.protovalidate.Value;
import build.buf.protovalidate.exceptions.ExecutionException;
import build.buf.validate.FieldConstraints;
import build.buf.validate.Violation;
Expand Down Expand Up @@ -100,7 +101,7 @@ private List<Violation> evalPairs(Value key, Value value, boolean failFast)
violations.addAll(keyViolations);
violations.addAll(valueViolations);

Object keyName = key.value(Object.class);
Object keyName = key.jvmValue(Object.class);
if (keyName == null) {
return Collections.emptyList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package build.buf.protovalidate.internal.evaluator;

import build.buf.protovalidate.ValidationResult;
import build.buf.protovalidate.Value;
import build.buf.protovalidate.exceptions.ExecutionException;
import build.buf.validate.Violation;
import java.util.ArrayList;
Expand Down
Loading