diff --git a/src/main/java/build/buf/protovalidate/AnyEvaluator.java b/src/main/java/build/buf/protovalidate/AnyEvaluator.java index d19f30e1..15de686a 100644 --- a/src/main/java/build/buf/protovalidate/AnyEvaluator.java +++ b/src/main/java/build/buf/protovalidate/AnyEvaluator.java @@ -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; @@ -78,12 +77,12 @@ final class AnyEvaluator implements Evaluator { @Override public List evaluate(Value val, boolean failFast) throws ExecutionException { - Message anyValue = val.messageValue(); + MessageReflector anyValue = val.messageValue(); if (anyValue == null) { return RuleViolation.NO_VIOLATIONS; } List 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() diff --git a/src/main/java/build/buf/protovalidate/CelPrograms.java b/src/main/java/build/buf/protovalidate/CelPrograms.java index 520f01ca..f4c14136 100644 --- a/src/main/java/build/buf/protovalidate/CelPrograms.java +++ b/src/main/java/build/buf/protovalidate/CelPrograms.java @@ -45,7 +45,7 @@ public boolean tautology() { @Override public List evaluate(Value val, boolean failFast) throws ExecutionException { - CelVariableResolver bindings = Variable.newThisVariable(val.value(Object.class)); + CelVariableResolver bindings = Variable.newThisVariable(val.celValue()); List violations = new ArrayList<>(); for (CompiledProgram program : programs) { RuleViolation.Builder violation = program.eval(val, bindings); diff --git a/src/main/java/build/buf/protovalidate/EnumEvaluator.java b/src/main/java/build/buf/protovalidate/EnumEvaluator.java index e3036f90..bc3d185d 100644 --- a/src/main/java/build/buf/protovalidate/EnumEvaluator.java +++ b/src/main/java/build/buf/protovalidate/EnumEvaluator.java @@ -77,7 +77,7 @@ public boolean tautology() { @Override public List 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; } diff --git a/src/main/java/build/buf/protovalidate/FieldEvaluator.java b/src/main/java/build/buf/protovalidate/FieldEvaluator.java index 8b0d1ce2..efd42800 100644 --- a/src/main/java/build/buf/protovalidate/FieldEvaluator.java +++ b/src/main/java/build/buf/protovalidate/FieldEvaluator.java @@ -19,7 +19,6 @@ import build.buf.validate.FieldRules; import build.buf.validate.Ignore; import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Message; import java.util.Collections; import java.util.List; @@ -95,13 +94,17 @@ public List 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); } @@ -118,7 +121,6 @@ public List evaluate(Value val, boolean failFast) if (this.shouldIgnoreEmpty() && !hasField) { return RuleViolation.NO_VIOLATIONS; } - return valueEvaluator.evaluate( - new ObjectValue(descriptor, message.getField(descriptor)), failFast); + return valueEvaluator.evaluate(message.getField(descriptor), failFast); } } diff --git a/src/main/java/build/buf/protovalidate/ListElementValue.java b/src/main/java/build/buf/protovalidate/ListElementValue.java index dbb96d67..28f2f066 100644 --- a/src/main/java/build/buf/protovalidate/ListElementValue.java +++ b/src/main/java/build/buf/protovalidate/ListElementValue.java @@ -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 value(Class clazz) { + public Object celValue() { + return ProtoAdapter.scalarToCel(fieldDescriptor.getType(), value); + } + + @Override + public T jvmValue(Class clazz) { Descriptors.FieldDescriptor.Type type = fieldDescriptor.getType(); if (type == Descriptors.FieldDescriptor.Type.MESSAGE) { return clazz.cast(value); diff --git a/src/main/java/build/buf/protovalidate/MapEvaluator.java b/src/main/java/build/buf/protovalidate/MapEvaluator.java index 5d66b86c..2d930b40 100644 --- a/src/main/java/build/buf/protovalidate/MapEvaluator.java +++ b/src/main/java/build/buf/protovalidate/MapEvaluator.java @@ -155,19 +155,19 @@ private List 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"); diff --git a/src/main/java/build/buf/protovalidate/MessageOneofEvaluator.java b/src/main/java/build/buf/protovalidate/MessageOneofEvaluator.java index 410c02fd..c5edf0d5 100644 --- a/src/main/java/build/buf/protovalidate/MessageOneofEvaluator.java +++ b/src/main/java/build/buf/protovalidate/MessageOneofEvaluator.java @@ -45,7 +45,7 @@ public boolean tautology() { @Override public List evaluate(Value val, boolean failFast) throws ExecutionException { - Message msg = val.messageValue(); + MessageReflector msg = val.messageValue(); if (msg == null) { return RuleViolation.NO_VIOLATIONS; } diff --git a/src/main/java/build/buf/protovalidate/MessageReflector.java b/src/main/java/build/buf/protovalidate/MessageReflector.java new file mode 100644 index 00000000..bb0b445a --- /dev/null +++ b/src/main/java/build/buf/protovalidate/MessageReflector.java @@ -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. + * + *

{@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); +} diff --git a/src/main/java/build/buf/protovalidate/MessageValue.java b/src/main/java/build/buf/protovalidate/MessageValue.java index c2145313..246820f2 100644 --- a/src/main/java/build/buf/protovalidate/MessageValue.java +++ b/src/main/java/build/buf/protovalidate/MessageValue.java @@ -25,7 +25,7 @@ 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. @@ -33,7 +33,7 @@ final class MessageValue implements Value { * @param value The message value. */ MessageValue(Message value) { - this.value = value; + this.value = new ProtobufMessageReflector(value); } @Override @@ -42,13 +42,18 @@ final class MessageValue implements Value { } @Override - public Message messageValue() { - return (Message) value; + public MessageReflector messageValue() { + return value; } @Override - public T value(Class clazz) { - return clazz.cast(value); + public Object celValue() { + return value.getMessage(); + } + + @Override + public T jvmValue(Class clazz) { + throw new UnsupportedOperationException(); } @Override diff --git a/src/main/java/build/buf/protovalidate/ObjectValue.java b/src/main/java/build/buf/protovalidate/ObjectValue.java index 1ca0afc9..3d095456 100644 --- a/src/main/java/build/buf/protovalidate/ObjectValue.java +++ b/src/main/java/build/buf/protovalidate/ObjectValue.java @@ -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 value(Class clazz) { + public Object celValue() { + return ProtoAdapter.toCel(fieldDescriptor, value); + } + + @Override + public T jvmValue(Class clazz) { + if (value instanceof Descriptors.EnumValueDescriptor) { + return clazz.cast((long) ((Descriptors.EnumValueDescriptor) value).getNumber()); + } return clazz.cast(ProtoAdapter.toCel(fieldDescriptor, value)); } diff --git a/src/main/java/build/buf/protovalidate/OneofEvaluator.java b/src/main/java/build/buf/protovalidate/OneofEvaluator.java index 4124039b..9c0f9604 100644 --- a/src/main/java/build/buf/protovalidate/OneofEvaluator.java +++ b/src/main/java/build/buf/protovalidate/OneofEvaluator.java @@ -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; @@ -48,8 +47,12 @@ public boolean tautology() { @Override public List 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( diff --git a/src/main/java/build/buf/protovalidate/ProtobufMessageReflector.java b/src/main/java/build/buf/protovalidate/ProtobufMessageReflector.java new file mode 100644 index 00000000..4d44b92d --- /dev/null +++ b/src/main/java/build/buf/protovalidate/ProtobufMessageReflector.java @@ -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)); + } +} diff --git a/src/main/java/build/buf/protovalidate/RuleViolation.java b/src/main/java/build/buf/protovalidate/RuleViolation.java index 6486d159..a5ee5469 100644 --- a/src/main/java/build/buf/protovalidate/RuleViolation.java +++ b/src/main/java/build/buf/protovalidate/RuleViolation.java @@ -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()); } diff --git a/src/main/java/build/buf/protovalidate/Value.java b/src/main/java/build/buf/protovalidate/Value.java index 5d6217b5..e76e3928 100644 --- a/src/main/java/build/buf/protovalidate/Value.java +++ b/src/main/java/build/buf/protovalidate/Value.java @@ -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; @@ -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. * @@ -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 The class type. - */ - T value(Class clazz); + @Nullable MessageReflector messageValue(); /** * Get the underlying value as a list. @@ -65,4 +55,21 @@ interface Value { * list. */ Map 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 The class type. + */ + T jvmValue(Class clazz); } diff --git a/src/main/java/build/buf/protovalidate/ValueEvaluator.java b/src/main/java/build/buf/protovalidate/ValueEvaluator.java index 198d65b6..19d9ab31 100644 --- a/src/main/java/build/buf/protovalidate/ValueEvaluator.java +++ b/src/main/java/build/buf/protovalidate/ValueEvaluator.java @@ -71,7 +71,7 @@ public boolean tautology() { @Override public List 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 allViolations = new ArrayList<>();