From 6d64ff4ce6e032fb2b36380be369511f019b586f Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Fri, 8 May 2026 20:02:47 -0400 Subject: [PATCH 1/2] Add CelValueProvider support to Config Allow alternative protobuf runtimes to register a custom CelValueProvider that teaches CEL how to navigate their message types natively via StructValue, without requiring conversion to protobuf-java DynamicMessages. The provider is wired through to the CEL builder's setValueProvider(), which configures the CelValueRuntimeTypeProvider to delegate field access to StructValue.select()/find() for custom message types. --- .../java/build/buf/protovalidate/Config.java | 32 +++++++++++++++++-- .../buf/protovalidate/ValidatorImpl.java | 25 ++++++++------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/Config.java b/src/main/java/build/buf/protovalidate/Config.java index 289bc7171..2ef5dc4c9 100644 --- a/src/main/java/build/buf/protovalidate/Config.java +++ b/src/main/java/build/buf/protovalidate/Config.java @@ -16,6 +16,8 @@ import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.TypeRegistry; +import dev.cel.common.values.CelValueProvider; +import org.jspecify.annotations.Nullable; /** Config is the configuration for a Validator. */ public final class Config { @@ -27,16 +29,19 @@ public final class Config { private final TypeRegistry typeRegistry; private final ExtensionRegistry extensionRegistry; private final boolean allowUnknownFields; + @Nullable private final CelValueProvider celValueProvider; private Config( boolean failFast, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry, - boolean allowUnknownFields) { + boolean allowUnknownFields, + @Nullable CelValueProvider celValueProvider) { this.failFast = failFast; this.typeRegistry = typeRegistry; this.extensionRegistry = extensionRegistry; this.allowUnknownFields = allowUnknownFields; + this.celValueProvider = celValueProvider; } /** @@ -84,12 +89,22 @@ public boolean isAllowingUnknownFields() { return allowUnknownFields; } + /** + * Gets the custom CEL value provider, if one was configured. + * + * @return the custom value provider, or null if none was set + */ + public @Nullable CelValueProvider getCelValueProvider() { + return celValueProvider; + } + /** Builder for configuration. Provides a forward compatible API for users. */ public static final class Builder { private boolean failFast; private TypeRegistry typeRegistry = DEFAULT_TYPE_REGISTRY; private ExtensionRegistry extensionRegistry = DEFAULT_EXTENSION_REGISTRY; private boolean allowUnknownFields; + @Nullable private CelValueProvider celValueProvider; private Builder() {} @@ -162,8 +177,21 @@ public Builder setAllowUnknownFields(boolean allowUnknownFields) { * * @return the configuration. */ + /** + * Set a custom CEL value provider for the validator. This allows alternative protobuf runtimes + * to provide their own message representations to CEL without converting to protobuf-java + * DynamicMessages. + * + * @param celValueProvider the value provider + * @return this builder + */ + public Builder setCelValueProvider(CelValueProvider celValueProvider) { + this.celValueProvider = celValueProvider; + return this; + } + public Config build() { - return new Config(failFast, typeRegistry, extensionRegistry, allowUnknownFields); + return new Config(failFast, typeRegistry, extensionRegistry, allowUnknownFields, celValueProvider); } } } diff --git a/src/main/java/build/buf/protovalidate/ValidatorImpl.java b/src/main/java/build/buf/protovalidate/ValidatorImpl.java index d1748775e..167e53981 100644 --- a/src/main/java/build/buf/protovalidate/ValidatorImpl.java +++ b/src/main/java/build/buf/protovalidate/ValidatorImpl.java @@ -19,6 +19,7 @@ import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Message; import dev.cel.bundle.Cel; +import dev.cel.bundle.CelBuilder; import dev.cel.bundle.CelFactory; import java.util.ArrayList; import java.util.List; @@ -34,26 +35,28 @@ final class ValidatorImpl implements Validator { private final boolean failFast; ValidatorImpl(Config config) { - ValidateLibrary validateLibrary = new ValidateLibrary(); - Cel cel = - CelFactory.standardCelBuilder() - .addCompilerLibraries(validateLibrary) - .addRuntimeLibraries(validateLibrary) - .build(); + Cel cel = buildCel(config); this.evaluatorBuilder = new EvaluatorBuilder(cel, config); this.failFast = config.isFailFast(); } ValidatorImpl(Config config, List descriptors, boolean disableLazy) throws CompilationException { + Cel cel = buildCel(config); + this.evaluatorBuilder = new EvaluatorBuilder(cel, config, descriptors, disableLazy); + this.failFast = config.isFailFast(); + } + + private static Cel buildCel(Config config) { ValidateLibrary validateLibrary = new ValidateLibrary(); - Cel cel = + CelBuilder builder = CelFactory.standardCelBuilder() .addCompilerLibraries(validateLibrary) - .addRuntimeLibraries(validateLibrary) - .build(); - this.evaluatorBuilder = new EvaluatorBuilder(cel, config, descriptors, disableLazy); - this.failFast = config.isFailFast(); + .addRuntimeLibraries(validateLibrary); + if (config.getCelValueProvider() != null) { + builder = builder.setValueProvider(config.getCelValueProvider()); + } + return builder.build(); } @Override From 9175dc600af3ef059f0144b9b7b4e84846f6b9b1 Mon Sep 17 00:00:00 2001 From: Andrew Parmet Date: Fri, 8 May 2026 20:05:33 -0400 Subject: [PATCH 2/2] Add validate(CelValue, Descriptor) overload for alternative runtimes Allow alternative protobuf runtimes to pass a CelValue (typically a StructValue) for CEL evaluation while providing a protobuf-java Message for structural validation. CEL navigates the StructValue via select()/ find() for lazy field access, while FieldEvaluator uses the underlying Message for presence checks and required field validation. --- .../protovalidate/CelBackedMessageValue.java | 67 +++++++++++++++++++ .../build/buf/protovalidate/Validator.java | 14 ++++ .../buf/protovalidate/ValidatorImpl.java | 22 ++++++ 3 files changed, 103 insertions(+) create mode 100644 src/main/java/build/buf/protovalidate/CelBackedMessageValue.java diff --git a/src/main/java/build/buf/protovalidate/CelBackedMessageValue.java b/src/main/java/build/buf/protovalidate/CelBackedMessageValue.java new file mode 100644 index 000000000..b2369c41c --- /dev/null +++ b/src/main/java/build/buf/protovalidate/CelBackedMessageValue.java @@ -0,0 +1,67 @@ +// 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; +import com.google.protobuf.Message; +import dev.cel.common.values.CelValue; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.jspecify.annotations.Nullable; + +/** + * A {@link Value} backed by a {@link CelValue} for CEL evaluation and a protobuf {@link Message} + * for structural validation (field presence, required checks, etc.). + * + *

When CEL evaluates expressions against this value, it receives the CelValue (typically a + * StructValue) which can navigate fields lazily. For non-CEL evaluators that need protobuf Message + * access (e.g., FieldEvaluator for presence checks), the underlying Message is provided. + */ +final class CelBackedMessageValue implements Value { + private final CelValue celValue; + private final Message message; + + CelBackedMessageValue(CelValue celValue, Message message) { + this.celValue = celValue; + this.message = message; + } + + @Override + public Descriptors.@Nullable FieldDescriptor fieldDescriptor() { + return null; + } + + @Override + public Message messageValue() { + return message; + } + + @Override + public T value(Class clazz) { + // CEL receives the CelValue; it knows how to navigate StructValues via selectField + return clazz.cast(celValue); + } + + @Override + public List repeatedValue() { + return Collections.emptyList(); + } + + @Override + public Map mapValue() { + return Collections.emptyMap(); + } +} diff --git a/src/main/java/build/buf/protovalidate/Validator.java b/src/main/java/build/buf/protovalidate/Validator.java index f8e67195e..a3447f50c 100644 --- a/src/main/java/build/buf/protovalidate/Validator.java +++ b/src/main/java/build/buf/protovalidate/Validator.java @@ -17,7 +17,9 @@ 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; +import dev.cel.common.values.CelValue; /** A validator that can be used to validate messages */ public interface Validator { @@ -35,4 +37,16 @@ public interface Validator { * @throws ValidationException if there are any compilation or validation execution errors. */ ValidationResult validate(Message msg) throws ValidationException; + + /** + * Validates a message represented as a CelValue. This allows alternative protobuf runtimes to + * provide their messages as StructValues that CEL can navigate directly without conversion to + * protobuf-java Message types. + * + * @param celValue a CelValue (typically a StructValue) representing the message + * @param descriptor the protobuf 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(CelValue celValue, 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 167e53981..d3f3da372 100644 --- a/src/main/java/build/buf/protovalidate/ValidatorImpl.java +++ b/src/main/java/build/buf/protovalidate/ValidatorImpl.java @@ -21,6 +21,7 @@ import dev.cel.bundle.Cel; import dev.cel.bundle.CelBuilder; import dev.cel.bundle.CelFactory; +import dev.cel.common.values.CelValue; import java.util.ArrayList; import java.util.List; @@ -67,6 +68,27 @@ public ValidationResult validate(Message msg) throws ValidationException { Descriptor descriptor = msg.getDescriptorForType(); Evaluator evaluator = evaluatorBuilder.load(descriptor); List result = evaluator.evaluate(new MessageValue(msg), this.failFast); + return toResult(result); + } + + @Override + public ValidationResult validate(CelValue celValue, Descriptor descriptor) + throws ValidationException { + if (celValue == null) { + return ValidationResult.EMPTY; + } + Object underlying = celValue.value(); + if (!(underlying instanceof Message)) { + throw new ValidationException( + "CelValue.value() must return a protobuf Message for structural validation"); + } + Evaluator evaluator = evaluatorBuilder.load(descriptor); + List result = + evaluator.evaluate(new CelBackedMessageValue(celValue, (Message) underlying), this.failFast); + return toResult(result); + } + + private static ValidationResult toResult(List result) { if (result.isEmpty()) { return ValidationResult.EMPTY; }