From 1effcc5f64f3ae6c4854aa6131bb606fb15f78a5 Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Fri, 8 May 2026 23:41:23 -0400 Subject: [PATCH 1/7] Introduce MessageReflector for alternative protobuf runtimes Adds a public MessageReflector interface so alternative protobuf runtimes (e.g. protokt) can participate in protovalidate-java validation without first converting their messages to com.google.protobuf.Message. New public surface: - MessageReflector: hasField / getField / celValue - Value (promoted to public): recursive navigation, celValue(), jvmValue() for protovalidate-java internal dispatch - Validator.validate(MessageReflector, Descriptor): entry point for alternative-runtime callers; delegates to the same evaluator tree used for Message callers Internal refactor: - FieldEvaluator/OneofEvaluator/MessageOneofEvaluator/AnyEvaluator accept MessageReflector from Value.messageValue() - ProtobufMessageReflector adapts com.google.protobuf.Message to the new interface so the existing Message path is unchanged - MessageValue retains its Message-only constructor; new MessageReflectorValue wraps a caller-supplied MessageReflector --- .../build/buf/protovalidate/AnyEvaluator.java | 5 +- .../build/buf/protovalidate/CelPrograms.java | 2 +- .../buf/protovalidate/EnumEvaluator.java | 2 +- .../buf/protovalidate/FieldEvaluator.java | 15 ++--- .../buf/protovalidate/ListElementValue.java | 11 +++- .../build/buf/protovalidate/MapEvaluator.java | 8 +-- .../protovalidate/MessageOneofEvaluator.java | 2 +- .../buf/protovalidate/MessageReflector.java | 50 +++++++++++++++ .../protovalidate/MessageReflectorValue.java | 61 +++++++++++++++++++ .../build/buf/protovalidate/MessageValue.java | 17 ++++-- .../build/buf/protovalidate/ObjectValue.java | 14 ++++- .../buf/protovalidate/OneofEvaluator.java | 9 ++- .../ProtobufMessageReflector.java | 45 ++++++++++++++ .../buf/protovalidate/RuleViolation.java | 2 +- .../build/buf/protovalidate/Validator.java | 14 +++++ .../buf/protovalidate/ValidatorImpl.java | 17 +++++- .../java/build/buf/protovalidate/Value.java | 37 ++++++----- .../buf/protovalidate/ValueEvaluator.java | 2 +- 18 files changed, 262 insertions(+), 51 deletions(-) create mode 100644 src/main/java/build/buf/protovalidate/MessageReflector.java create mode 100644 src/main/java/build/buf/protovalidate/MessageReflectorValue.java create mode 100644 src/main/java/build/buf/protovalidate/ProtobufMessageReflector.java diff --git a/src/main/java/build/buf/protovalidate/AnyEvaluator.java b/src/main/java/build/buf/protovalidate/AnyEvaluator.java index d7d18356d..450a555fa 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 ed712f796..f88e56035 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 d2fa00df4..52f6e1bb1 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 877aec5ea..26533f547 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,7 +94,7 @@ 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; } @@ -113,17 +112,19 @@ 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); } /** * Returns whether the given field is set on the message. Handles repeated and map fields, which - * are not supported by {@link Message#hasField}. + * require inspecting the list/map contents rather than a presence bit. */ - static boolean isFieldSet(Message message, FieldDescriptor field) { + static boolean isFieldSet(MessageReflector message, FieldDescriptor field) { if (field.isRepeated()) { - return message.getRepeatedFieldCount(field) != 0; + if (field.isMapField()) { + return !message.getField(field).mapValue().isEmpty(); + } + return !message.getField(field).repeatedValue().isEmpty(); } return message.hasField(field); } diff --git a/src/main/java/build/buf/protovalidate/ListElementValue.java b/src/main/java/build/buf/protovalidate/ListElementValue.java index 8e397d328..848cfbf92 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 bc418e977..0dfcecbfb 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 aecce4e92..3b68fb5f5 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 000000000..c6cd3be26 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/MessageReflector.java @@ -0,0 +1,50 @@ +// Copyright 2023-2026 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); + + /** + * Returns the representation of this message to hand to CEL for {@code this}-variable rule + * evaluation. Implementations return whichever form CEL can navigate for their runtime — + * e.g. a {@link com.google.protobuf.Message} or a {@code dev.cel.common.values.CelValue}. + */ + Object celValue(); +} diff --git a/src/main/java/build/buf/protovalidate/MessageReflectorValue.java b/src/main/java/build/buf/protovalidate/MessageReflectorValue.java new file mode 100644 index 000000000..fbe2c923d --- /dev/null +++ b/src/main/java/build/buf/protovalidate/MessageReflectorValue.java @@ -0,0 +1,61 @@ +// Copyright 2023-2026 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; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.jspecify.annotations.Nullable; + +/** Top-level {@link Value} backed by a user-supplied {@link MessageReflector}. */ +final class MessageReflectorValue implements Value { + + private final MessageReflector reflector; + + MessageReflectorValue(MessageReflector reflector) { + this.reflector = reflector; + } + + @Override + public Descriptors.@Nullable FieldDescriptor fieldDescriptor() { + return null; + } + + @Override + public MessageReflector messageValue() { + return reflector; + } + + @Override + public Object celValue() { + return reflector.celValue(); + } + + @Override + public T jvmValue(Class clazz) { + throw new UnsupportedOperationException(); + } + + @Override + public List repeatedValue() { + return Collections.emptyList(); + } + + @Override + public Map mapValue() { + return Collections.emptyMap(); + } +} diff --git a/src/main/java/build/buf/protovalidate/MessageValue.java b/src/main/java/build/buf/protovalidate/MessageValue.java index fa96e3b8c..a9e6261bc 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 Object celValue() { + return value.getMessage(); } @Override - public T value(Class clazz) { - return clazz.cast(value); + 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 9a53574ac..2b01a6938 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 5ddccd6d6..c45611e88 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 000000000..5c32cd753 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/ProtobufMessageReflector.java @@ -0,0 +1,45 @@ +// Copyright 2023-2026 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)); + } + + @Override + public Object celValue() { + return message; + } +} diff --git a/src/main/java/build/buf/protovalidate/RuleViolation.java b/src/main/java/build/buf/protovalidate/RuleViolation.java index 454d2f782..39d18930d 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/Validator.java b/src/main/java/build/buf/protovalidate/Validator.java index 449abef73..f193514bd 100644 --- a/src/main/java/build/buf/protovalidate/Validator.java +++ b/src/main/java/build/buf/protovalidate/Validator.java @@ -17,6 +17,7 @@ import build.buf.protovalidate.exceptions.CompilationException; import build.buf.protovalidate.exceptions.ExecutionException; import build.buf.protovalidate.exceptions.ValidationException; +import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Message; /** A validator that can be used to validate messages */ @@ -35,4 +36,17 @@ public interface Validator { * @throws ValidationException if there are any compilation or validation execution errors. */ ValidationResult validate(Message msg) throws ValidationException; + + /** + * Validates a message provided as a {@link MessageReflector}. Used by alternative protobuf + * runtimes that reflect over their own message types via this interface rather than going + * through a {@link Message}. + * + * @param message the {@link MessageReflector} view of the message to validate. + * @param descriptor the protobuf {@link Descriptor} for the message type. + * @return the {@link ValidationResult} from the evaluation. + * @throws ValidationException if there are any compilation or validation execution errors. + */ + ValidationResult validate(MessageReflector message, Descriptor descriptor) + throws ValidationException; } diff --git a/src/main/java/build/buf/protovalidate/ValidatorImpl.java b/src/main/java/build/buf/protovalidate/ValidatorImpl.java index 39613c9f8..8046a37a5 100644 --- a/src/main/java/build/buf/protovalidate/ValidatorImpl.java +++ b/src/main/java/build/buf/protovalidate/ValidatorImpl.java @@ -48,9 +48,22 @@ public ValidationResult validate(Message msg) throws ValidationException { if (msg == null) { return ValidationResult.EMPTY; } - Descriptor descriptor = msg.getDescriptorForType(); + return validateInternal(new MessageValue(msg), msg.getDescriptorForType()); + } + + @Override + public ValidationResult validate(MessageReflector message, Descriptor descriptor) + throws ValidationException { + if (message == null) { + return ValidationResult.EMPTY; + } + return validateInternal(new MessageReflectorValue(message), descriptor); + } + + private ValidationResult validateInternal(Value value, Descriptor descriptor) + throws ValidationException { Evaluator evaluator = evaluatorBuilder.load(descriptor); - List result = evaluator.evaluate(new MessageValue(msg), this.failFast); + List result = evaluator.evaluate(value, this.failFast); if (result.isEmpty()) { return ValidationResult.EMPTY; } diff --git a/src/main/java/build/buf/protovalidate/Value.java b/src/main/java/build/buf/protovalidate/Value.java index a2bb96b29..a5975810d 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(); /** * Returns the underlying protobuf Java value without any CEL-specific adaptation. @@ -81,4 +71,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 433da1c40..2408b0049 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<>(); From 511175616c93733aacefe55405262063bd289bee Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Sat, 9 May 2026 00:30:27 -0400 Subject: [PATCH 2/7] Collapse validate(Message) into validate(MessageReflector, Descriptor) - validate(Message) now wraps in a ProtobufMessageReflector and delegates to the reflector-based overload, removing the duplicated body. - Drop stale comment on MessageValue. --- .../java/build/buf/protovalidate/MessageValue.java | 1 - .../java/build/buf/protovalidate/ValidatorImpl.java | 10 +++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/MessageValue.java b/src/main/java/build/buf/protovalidate/MessageValue.java index a9e6261bc..9d5cf2c55 100644 --- a/src/main/java/build/buf/protovalidate/MessageValue.java +++ b/src/main/java/build/buf/protovalidate/MessageValue.java @@ -24,7 +24,6 @@ /** The {@link Value} type that contains a {@link com.google.protobuf.Message}. */ final class MessageValue implements Value { - /** Object type since the object type is inferred from the field descriptor. */ private final ProtobufMessageReflector value; /** diff --git a/src/main/java/build/buf/protovalidate/ValidatorImpl.java b/src/main/java/build/buf/protovalidate/ValidatorImpl.java index 8046a37a5..36f8cf178 100644 --- a/src/main/java/build/buf/protovalidate/ValidatorImpl.java +++ b/src/main/java/build/buf/protovalidate/ValidatorImpl.java @@ -48,7 +48,7 @@ public ValidationResult validate(Message msg) throws ValidationException { if (msg == null) { return ValidationResult.EMPTY; } - return validateInternal(new MessageValue(msg), msg.getDescriptorForType()); + return validate(new ProtobufMessageReflector(msg), msg.getDescriptorForType()); } @Override @@ -57,13 +57,9 @@ public ValidationResult validate(MessageReflector message, Descriptor descriptor if (message == null) { return ValidationResult.EMPTY; } - return validateInternal(new MessageReflectorValue(message), descriptor); - } - - private ValidationResult validateInternal(Value value, Descriptor descriptor) - throws ValidationException { Evaluator evaluator = evaluatorBuilder.load(descriptor); - List result = evaluator.evaluate(value, this.failFast); + List result = + evaluator.evaluate(new MessageReflectorValue(message), this.failFast); if (result.isEmpty()) { return ValidationResult.EMPTY; } From 55fb99b01a16a8b4f4747ca09e96dd0bfbed0295 Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Sat, 9 May 2026 01:36:30 -0400 Subject: [PATCH 3/7] Enable CelValue (StructValue) path --- build.gradle.kts | 1 + gradle/libs.versions.toml | 2 +- src/main/java/build/buf/protovalidate/ValidateLibrary.java | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4255d3a7b..3b27ff10c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -331,6 +331,7 @@ configure { allprojects { version = releaseVersion repositories { + mavenLocal() mavenCentral() } apply(plugin = "com.diffplug.spotless") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a38775b8..2e75a38ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] assertj = "3.27.7" buf = "1.69.0" -cel = "0.13.0" +cel = "0.13.0-SNAPSHOT" error-prone = "2.49.0" junit = "5.14.4" maven-publish = "0.36.0" diff --git a/src/main/java/build/buf/protovalidate/ValidateLibrary.java b/src/main/java/build/buf/protovalidate/ValidateLibrary.java index 6b032450c..80406ef3c 100644 --- a/src/main/java/build/buf/protovalidate/ValidateLibrary.java +++ b/src/main/java/build/buf/protovalidate/ValidateLibrary.java @@ -38,7 +38,8 @@ */ final class ValidateLibrary implements CelCompilerLibrary, CelRuntimeLibrary { - private static final CelOptions CEL_OPTIONS = CelOptions.DEFAULT; + private static final CelOptions CEL_OPTIONS = + CelOptions.current().enableCelValue(true).build(); private final ConcurrentMap patternCache = new ConcurrentHashMap<>(); From c947beb2156bcc70d0f10c3de1be4f0eb2d8a33c Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Wed, 27 May 2026 16:53:51 -0400 Subject: [PATCH 4/7] Move getDescriptorForType onto MessageReflector The descriptor is intrinsic to the message; threading it as a separate argument to validate() forced callers to keep them synchronized. MessageReflector now exposes getDescriptorForType() and Validator takes only the reflector. ProtobufMessageReflector returns the underlying Message's descriptor. --- .../build/buf/protovalidate/MessageReflector.java | 15 +++++++++++---- .../protovalidate/ProtobufMessageReflector.java | 6 ++++++ .../java/build/buf/protovalidate/Validator.java | 5 +---- .../build/buf/protovalidate/ValidatorImpl.java | 7 +++---- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/MessageReflector.java b/src/main/java/build/buf/protovalidate/MessageReflector.java index c6cd3be26..50f79d4b0 100644 --- a/src/main/java/build/buf/protovalidate/MessageReflector.java +++ b/src/main/java/build/buf/protovalidate/MessageReflector.java @@ -14,6 +14,7 @@ package build.buf.protovalidate; +import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; /** @@ -21,10 +22,16 @@ * 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. + * implements this interface can wrap its messages so that protovalidate-java can validate them. */ public interface MessageReflector { + /** + * The {@link Descriptor} of the wrapped message's type. + * + * @return The descriptor. + */ + Descriptor getDescriptorForType(); + /** * Whether the wrapped message has the field described by the provided field descriptor. * @@ -43,8 +50,8 @@ public interface MessageReflector { /** * Returns the representation of this message to hand to CEL for {@code this}-variable rule - * evaluation. Implementations return whichever form CEL can navigate for their runtime — - * e.g. a {@link com.google.protobuf.Message} or a {@code dev.cel.common.values.CelValue}. + * evaluation. Implementations return whichever form CEL can navigate for their runtime, such as + * a {@link com.google.protobuf.Message} or a {@code dev.cel.common.values.CelValue}. */ Object celValue(); } diff --git a/src/main/java/build/buf/protovalidate/ProtobufMessageReflector.java b/src/main/java/build/buf/protovalidate/ProtobufMessageReflector.java index 5c32cd753..75f7c46d6 100644 --- a/src/main/java/build/buf/protovalidate/ProtobufMessageReflector.java +++ b/src/main/java/build/buf/protovalidate/ProtobufMessageReflector.java @@ -14,6 +14,7 @@ package build.buf.protovalidate; +import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Message; @@ -28,6 +29,11 @@ public Message getMessage() { return message; } + @Override + public Descriptor getDescriptorForType() { + return message.getDescriptorForType(); + } + @Override public boolean hasField(FieldDescriptor field) { return message.hasField(field); diff --git a/src/main/java/build/buf/protovalidate/Validator.java b/src/main/java/build/buf/protovalidate/Validator.java index f193514bd..f0571971f 100644 --- a/src/main/java/build/buf/protovalidate/Validator.java +++ b/src/main/java/build/buf/protovalidate/Validator.java @@ -17,7 +17,6 @@ import build.buf.protovalidate.exceptions.CompilationException; import build.buf.protovalidate.exceptions.ExecutionException; import build.buf.protovalidate.exceptions.ValidationException; -import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Message; /** A validator that can be used to validate messages */ @@ -43,10 +42,8 @@ public interface Validator { * through a {@link Message}. * * @param message the {@link MessageReflector} view of the message to validate. - * @param descriptor the protobuf {@link Descriptor} for the message type. * @return the {@link ValidationResult} from the evaluation. * @throws ValidationException if there are any compilation or validation execution errors. */ - ValidationResult validate(MessageReflector message, Descriptor descriptor) - throws ValidationException; + ValidationResult validate(MessageReflector message) throws ValidationException; } diff --git a/src/main/java/build/buf/protovalidate/ValidatorImpl.java b/src/main/java/build/buf/protovalidate/ValidatorImpl.java index 36f8cf178..7911545d0 100644 --- a/src/main/java/build/buf/protovalidate/ValidatorImpl.java +++ b/src/main/java/build/buf/protovalidate/ValidatorImpl.java @@ -48,16 +48,15 @@ public ValidationResult validate(Message msg) throws ValidationException { if (msg == null) { return ValidationResult.EMPTY; } - return validate(new ProtobufMessageReflector(msg), msg.getDescriptorForType()); + return validate(new ProtobufMessageReflector(msg)); } @Override - public ValidationResult validate(MessageReflector message, Descriptor descriptor) - throws ValidationException { + public ValidationResult validate(MessageReflector message) throws ValidationException { if (message == null) { return ValidationResult.EMPTY; } - Evaluator evaluator = evaluatorBuilder.load(descriptor); + Evaluator evaluator = evaluatorBuilder.load(message.getDescriptorForType()); List result = evaluator.evaluate(new MessageReflectorValue(message), this.failFast); if (result.isEmpty()) { From 766042b834f36ba051bc89af3b8ab4e015a40dd7 Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Sat, 30 May 2026 13:22:26 -0400 Subject: [PATCH 5/7] Reconcile MessageReflector with upstream rawValue() and Native rule paths Upstream main has moved since the MessageReflector cherry-picks were authored: - Value gained an Object rawValue() abstract method so native rule evaluators can read the underlying protobuf-java value without CEL adaptation. Add an override on MessageReflectorValue that returns the wrapped MessageReflector itself; this is consistent with the existing messageValue() override and gives native paths the same access point used elsewhere in the evaluator pipeline. - WrappedValueEvaluator was added on upstream main to unwrap google.protobuf.*Value wrappers for native scalar evaluators. Its evaluate() called val.messageValue() and used the returned com.google.protobuf.Message directly, but messageValue() now returns MessageReflector. Switch to MessageReflector.getField(field), which returns a Value that the wrapped Evaluator can consume directly. --- .../build/buf/protovalidate/MessageReflectorValue.java | 5 +++++ .../build/buf/protovalidate/WrappedValueEvaluator.java | 9 ++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/MessageReflectorValue.java b/src/main/java/build/buf/protovalidate/MessageReflectorValue.java index fbe2c923d..555943edf 100644 --- a/src/main/java/build/buf/protovalidate/MessageReflectorValue.java +++ b/src/main/java/build/buf/protovalidate/MessageReflectorValue.java @@ -39,6 +39,11 @@ public MessageReflector messageValue() { return reflector; } + @Override + public Object rawValue() { + return reflector; + } + @Override public Object celValue() { return reflector.celValue(); diff --git a/src/main/java/build/buf/protovalidate/WrappedValueEvaluator.java b/src/main/java/build/buf/protovalidate/WrappedValueEvaluator.java index 6cba9a242..39395df55 100644 --- a/src/main/java/build/buf/protovalidate/WrappedValueEvaluator.java +++ b/src/main/java/build/buf/protovalidate/WrappedValueEvaluator.java @@ -16,7 +16,6 @@ import build.buf.protovalidate.exceptions.ExecutionException; import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Message; import java.util.List; /** @@ -26,7 +25,7 @@ * *

CEL's runtime auto-unwraps wrappers to their inner primitive type, so the CEL path doesn't * need this adapter. Native evaluators expect the underlying scalar (e.g. {@code Long} for {@code - * Int64Value.value}); without unwrapping they'd see the wrapper {@link Message} and misbehave. + * Int64Value.value}); without unwrapping they'd see the wrapper message and misbehave. * Mirrors {@code wrappedValueEval} in protovalidate-go's {@code builder.go}. * *

The wrapped evaluator's {@link RuleBase} is constructed against the OUTER wrapper field's @@ -58,12 +57,12 @@ public boolean tautology() { @Override public List evaluate(Value val, boolean failFast) throws ExecutionException { - Message message = val.messageValue(); + MessageReflector message = val.messageValue(); if (message == null) { // proto3 message-typed field absent — no value to validate. return RuleViolation.NO_VIOLATIONS; } - Object innerValue = message.getField(innerField); - return inner.evaluate(new ObjectValue(innerField, innerValue), failFast); + Value innerValue = message.getField(innerField); + return inner.evaluate(innerValue, failFast); } } From 0f698eeae1f49a7b6fe4dafc116a2233f84ef2cf Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Sat, 30 May 2026 13:26:22 -0400 Subject: [PATCH 6/7] Fix javadoc: add @return on MessageReflector#celValue and correct #value(Class) link --- src/main/java/build/buf/protovalidate/MessageReflector.java | 2 ++ src/main/java/build/buf/protovalidate/Value.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/build/buf/protovalidate/MessageReflector.java b/src/main/java/build/buf/protovalidate/MessageReflector.java index 50f79d4b0..99da86c08 100644 --- a/src/main/java/build/buf/protovalidate/MessageReflector.java +++ b/src/main/java/build/buf/protovalidate/MessageReflector.java @@ -52,6 +52,8 @@ public interface MessageReflector { * Returns the representation of this message to hand to CEL for {@code this}-variable rule * evaluation. Implementations return whichever form CEL can navigate for their runtime, such as * a {@link com.google.protobuf.Message} or a {@code dev.cel.common.values.CelValue}. + * + * @return The CEL-navigable representation of this message. */ Object celValue(); } diff --git a/src/main/java/build/buf/protovalidate/Value.java b/src/main/java/build/buf/protovalidate/Value.java index a5975810d..385778928 100644 --- a/src/main/java/build/buf/protovalidate/Value.java +++ b/src/main/java/build/buf/protovalidate/Value.java @@ -43,7 +43,7 @@ public interface Value { /** * Returns the underlying protobuf Java value without any CEL-specific adaptation. * - *

{@link #value(Class)} routes scalars through {@code ProtoAdapter.toCel}, which converts + *

{@link #jvmValue(Class)} routes scalars through {@code ProtoAdapter.toCel}, which converts * {@code int32→Long}, {@code uint32→UnsignedLong}, {@code float→Double}, {@code bytes→ * CelByteString}, etc. — appropriate for the CEL evaluation path but lossy for native rule * evaluators that compare against raw protobuf field values. Native evaluators in {@code From 83e7227daf8903f2f6b8bb50f0896264176d33f4 Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Sat, 30 May 2026 14:24:14 -0400 Subject: [PATCH 7/7] Pin cel to 0.13.0-standard-SNAPSHOT --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e75a38ef..90123c5e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] assertj = "3.27.7" buf = "1.69.0" -cel = "0.13.0-SNAPSHOT" +cel = "0.13.0-standard-SNAPSHOT" error-prone = "2.49.0" junit = "5.14.4" maven-publish = "0.36.0"