Skip to content
Closed

. #470

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
67 changes: 67 additions & 0 deletions src/main/java/build/buf/protovalidate/CelBackedMessageValue.java
Original file line number Diff line number Diff line change
@@ -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.).
*
* <p>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> T value(Class<T> clazz) {
// CEL receives the CelValue; it knows how to navigate StructValues via selectField
return clazz.cast(celValue);
}

@Override
public List<Value> repeatedValue() {
return Collections.emptyList();
}

@Override
public Map<Value, Value> mapValue() {
return Collections.emptyMap();
}
}
32 changes: 30 additions & 2 deletions src/main/java/build/buf/protovalidate/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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() {}

Expand Down Expand Up @@ -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);
}
}
}
14 changes: 14 additions & 0 deletions src/main/java/build/buf/protovalidate/Validator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
47 changes: 36 additions & 11 deletions src/main/java/build/buf/protovalidate/ValidatorImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
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 dev.cel.common.values.CelValue;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -34,26 +36,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<Descriptor> 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
Expand All @@ -64,6 +68,27 @@ public ValidationResult validate(Message msg) throws ValidationException {
Descriptor descriptor = msg.getDescriptorForType();
Evaluator evaluator = evaluatorBuilder.load(descriptor);
List<RuleViolation.Builder> 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<RuleViolation.Builder> result =
evaluator.evaluate(new CelBackedMessageValue(celValue, (Message) underlying), this.failFast);
return toResult(result);
}

private static ValidationResult toResult(List<RuleViolation.Builder> result) {
if (result.isEmpty()) {
return ValidationResult.EMPTY;
}
Expand Down
Loading