Skip to content
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
2 changes: 2 additions & 0 deletions common/src/main/java/dev/cel/common/values/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ java_library(
"ProtoLiteCelValueConverter.java",
"ProtoMessageLiteValue.java",
"RawProtoMessageLiteValue.java",
"WireMessageLite.java",
],
tags = [
],
Expand Down Expand Up @@ -350,6 +351,7 @@ cel_android_library(
"ProtoLiteCelValueConverter.java",
"ProtoMessageLiteValue.java",
"RawProtoMessageLiteValue.java",
"WireMessageLite.java",
],
tags = [
],
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.Immutable;
import com.google.protobuf.ByteString;
import com.google.protobuf.MessageLite;
import dev.cel.common.types.CelType;
import dev.cel.common.types.StructTypeReference;
import dev.cel.common.values.ProtoLiteCelValueConverter.MessageFields;
import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import org.jspecify.annotations.Nullable;

Expand All @@ -38,6 +40,11 @@
* <p>If the codebase has access to full protobuf messages with descriptors, use {@code
* ProtoMessageValue} instead.
*
* <p>An instance is backed by either a materialized {@link #rawValue()} or unparsed {@link
* #wireBytes()} (exactly one is non-null). Wire-backed instances decode selected fields directly
* from {@link #wireBytes()} and lazily parse the full {@link MessageLite} only if {@link #value()}
* is invoked.
*
* <p>Implements {@link OptimizedSelectable} so that select chains can address fields by number:
*
* <ul>
Expand All @@ -52,27 +59,48 @@
*/
@AutoValue
@Immutable
public abstract class ProtoMessageLiteValue extends StructValue<String, MessageLite>
abstract class ProtoMessageLiteValue extends StructValue<String, MessageLite>
implements OptimizedSelectable {

@Override
public abstract MessageLite value();
// Populated when wrapping an already-materialized root MessageLite (e.g., from activation).
abstract @Nullable MessageLite rawValue();

// Populated when slicing a nested submessage from parent wire bytes to avoid deserializing and
// re-serializing intermediate hops; lazily parsed into a MessageLite only if value() is called.
abstract @Nullable ByteString wireBytes();

@Override
public abstract CelType celType();

abstract ProtoLiteCelValueConverter protoLiteCelValueConverter();

@Memoized
@Override
public MessageLite value() {
MessageLite msg = rawValue();
if (msg != null) {
return msg;
}
return protoLiteCelValueConverter()
.parseMessageLite(checkNotNull(wireBytes()), celType().name());
}

ByteString toByteString() {
ByteString bytes = wireBytes();
return bytes != null ? bytes : checkNotNull(rawValue()).toByteString();
}

@Memoized
MessageFields messageFields() {
try {
return protoLiteCelValueConverter().readMessageFields(value(), celType().name());
return protoLiteCelValueConverter().readAllFields(toByteString(), celType().name());
} catch (IOException e) {
throw new IllegalStateException("Unable to read message fields for " + celType().name(), e);
throw new IllegalArgumentException(
"Failed to decode proto message of type: " + celType().name(), e);
}
}

ImmutableMap<String, Object> fieldValues() {
private ImmutableMap<String, Object> fieldValues() {
return messageFields().values();
}

Expand All @@ -82,9 +110,30 @@ ImmutableListMultimap<Integer, Object> unknownFields() {

@Override
public boolean isZeroValue() {
ByteString bytes = wireBytes();
if (bytes != null && bytes.isEmpty()) {
return true;
}
return value().getDefaultInstanceForType().equals(value());
}

@Override
public final boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof ProtoMessageLiteValue)) {
return false;
}
ProtoMessageLiteValue that = (ProtoMessageLiteValue) other;
return this.celType().equals(that.celType()) && this.value().equals(that.value());
}

@Override
public final int hashCode() {
return Objects.hash(value(), celType());
}

@Override
public Object select(String field) {
return find(field)
Expand All @@ -93,9 +142,8 @@ public Object select(String field) {

@Override
public Optional<Object> find(String field) {
Object fieldValue = fieldValues().get(field);
return Optional.ofNullable(fieldValue)
.map(value -> protoLiteCelValueConverter().toRuntimeValue(fieldValue));
return Optional.ofNullable(fieldValues().get(field))
.map(protoLiteCelValueConverter()::toRuntimeValue);
}

@Override
Expand Down Expand Up @@ -130,7 +178,7 @@ public Optional<Object> findByFieldNumber(SelectField field) {
FieldLiteDescriptor fd = findFieldDescriptor(field);
if (fd != null) {
return Optional.ofNullable(fieldValues().get(fd.getFieldName()))
.map(value -> protoLiteCelValueConverter().toRuntimeValue(value));
.map(protoLiteCelValueConverter()::toRuntimeValue);
}
return RawProtoMessageLiteValue.navigateWire(
field, unknownFields().get(field.fieldNumber()), protoLiteCelValueConverter());
Expand All @@ -142,13 +190,30 @@ public Optional<Object> findByFieldNumber(SelectField field) {
.orElse(null);
}

public static ProtoMessageLiteValue create(
static ProtoMessageLiteValue create(
MessageLite value, String typeName, ProtoLiteCelValueConverter protoLiteCelValueConverter) {
checkNotNull(value);
checkNotNull(typeName);
checkNotNull(protoLiteCelValueConverter);
return new AutoValue_ProtoMessageLiteValue(
value, StructTypeReference.create(typeName), protoLiteCelValueConverter);
value,
/* wireBytes= */ null,
StructTypeReference.create(typeName),
protoLiteCelValueConverter);
}

static ProtoMessageLiteValue create(
ByteString wireBytes,
String typeName,
ProtoLiteCelValueConverter protoLiteCelValueConverter) {
checkNotNull(wireBytes);
checkNotNull(typeName);
checkNotNull(protoLiteCelValueConverter);
return new AutoValue_ProtoMessageLiteValue(
/* rawValue= */ null,
wireBytes,
StructTypeReference.create(typeName),
protoLiteCelValueConverter);
}

ProtoMessageLiteValue() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import com.google.protobuf.ByteString;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.WireFormat;
import dev.cel.common.annotations.Internal;
import dev.cel.common.exceptions.CelAttributeNotFoundException;
import dev.cel.common.types.CelType;
import dev.cel.common.types.StructTypeReference;
Expand All @@ -39,6 +38,7 @@
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
Expand All @@ -49,38 +49,63 @@
* client-server version skew issues where newer fields or submessages lack generated classes and
* descriptors in the evaluation environment.
*
* <p>Rather than requiring compiled {@link MessageLite} classes or runtime schema descriptors, this
* <p>Rather than requiring compiled {@code MessageLite} classes or runtime schema descriptors, this
* value encapsulates the raw wire-format {@link ByteString} payload and performs classless,
* reflection-free field traversal directly over wire tags via {@link CodedInputStream}.
*/
@AutoValue
@AutoValue.CopyAnnotations
@Immutable
@SuppressWarnings("Immutable") // Immutable wire fields
@Internal
public abstract class RawProtoMessageLiteValue extends StructValue<String, RawProtoMessageLiteValue>
implements OptimizedSelectable {
abstract class RawProtoMessageLiteValue extends StructValue<String, WireMessageLite>
implements OptimizedSelectable, WireMessageLite {

private static final String UNKNOWN_MESSAGE_TYPE_NAME = "cel.@unknownMessage";
private static final int MAP_KEY_FIELD_NUMBER = 1;
private static final int MAP_VALUE_FIELD_NUMBER = 2;

abstract ByteString rawWireBytes();
@Override
public abstract ByteString toByteString();

@Override
public abstract CelType celType();

abstract ProtoLiteCelValueConverter protoLiteCelValueConverter();

@Override
public RawProtoMessageLiteValue value() {
public String protoTypeName() {
return celType().name();
}

@Override
public WireMessageLite value() {
return this;
}

@Override
public final boolean equals(Object other) {
// TODO: Support message equality
throw new UnsupportedOperationException("Message equality is not supported");
}

@Override
public final int hashCode() {
throw new UnsupportedOperationException("Message equality is not supported");
}

@Override
public final String toString() {
return String.format(
Locale.US,
"WireMessageLite{protoTypeName=%s, size=%d}",
protoTypeName(),
toByteString().size());
}

@Memoized
ImmutableListMultimap<Integer, Object> unknownFields() {
try {
CodedInputStream inputStream = rawWireBytes().newCodedInput();
CodedInputStream inputStream = toByteString().newCodedInput();
Multimap<Integer, Object> fields = Multimaps.newMultimap(new TreeMap<>(), ArrayList::new);
for (int tag = inputStream.readTag(); tag != 0; tag = inputStream.readTag()) {
int tagWireType = WireFormat.getTagWireType(tag);
Expand All @@ -96,7 +121,7 @@ ImmutableListMultimap<Integer, Object> unknownFields() {

@Override
public boolean isZeroValue() {
return rawWireBytes().isEmpty();
return toByteString().isEmpty();
}

/**
Expand Down Expand Up @@ -169,27 +194,15 @@ private static Object decodeWireField(
}

boolean isRepeated = field.defaultValue() instanceof List;
String protoTypeName = resolveProtoTypeName(field);

return decodeWireEntries(unknowns, typeCode, protoTypeName, isRepeated, converter);
}

/**
* Resolves the protobuf message type name for a field from the optimizer metadata in {@link
* SelectField}, or {@link #UNKNOWN_MESSAGE_TYPE_NAME} if unspecified.
*/
private static String resolveProtoTypeName(SelectField field) {
if (!field.protoTypeName().isEmpty()) {
return field.protoTypeName();
}
return UNKNOWN_MESSAGE_TYPE_NAME;
return decodeWireEntries(unknowns, typeCode, field.protoTypeName(), isRepeated, converter);
}

private static Object resolveDefault(SelectField field, ProtoLiteCelValueConverter converter) {
if (field.defaultValue() != null) {
return field.defaultValue();
}
return create(ByteString.EMPTY, resolveProtoTypeName(field), converter);
return decodeMessageValue(ByteString.EMPTY, field.protoTypeName(), converter);
}

/**
Expand Down Expand Up @@ -419,9 +432,7 @@ static Object decodeWireValue(
throw new UnsupportedOperationException("Groups are not supported");
case MESSAGE:
ByteString msgBytes = requireType(raw, ByteString.class, fieldType);
return converter
.tryDecodeWellKnownProto(msgBytes, protoTypeName)
.orElseGet(() -> create(msgBytes, protoTypeName, converter));
return decodeMessageValue(msgBytes, protoTypeName, converter);
case BYTES:
return CelByteString.of(requireType(raw, ByteString.class, fieldType).toByteArray());
case UINT32:
Expand All @@ -437,6 +448,13 @@ static Object decodeWireValue(
throw new IllegalArgumentException("Unsupported proto field type: " + fieldType);
}

private static Object decodeMessageValue(
ByteString msgBytes, String protoTypeName, ProtoLiteCelValueConverter converter) {
return converter
.tryDecodeProtoMessage(msgBytes, protoTypeName)
.orElseGet(() -> create(msgBytes, protoTypeName, converter));
}

private static <T> T requireType(
Object raw, Class<T> expectedType, WireFormat.FieldType fieldType) {
if (!expectedType.isInstance(raw)) {
Expand Down Expand Up @@ -509,12 +527,12 @@ private static ImmutableList<Object> decodePacked(
}
}

public static RawProtoMessageLiteValue create(
static RawProtoMessageLiteValue create(
ByteString rawWireBytes, ProtoLiteCelValueConverter protoLiteCelValueConverter) {
return create(rawWireBytes, "", protoLiteCelValueConverter);
}

public static RawProtoMessageLiteValue create(
static RawProtoMessageLiteValue create(
ByteString rawWireBytes,
String protoTypeName,
ProtoLiteCelValueConverter protoLiteCelValueConverter) {
Expand Down
47 changes: 47 additions & 0 deletions common/src/main/java/dev/cel/common/values/WireMessageLite.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2026 Google LLC
//
// 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
//
// https://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 dev.cel.common.values;

import com.google.errorprone.annotations.Immutable;
import com.google.protobuf.ByteString;
import dev.cel.common.annotations.Beta;

/**
* Represents a protobuf message evaluation result in {@code CelLiteRuntime} when no {@code
* CelLiteDescriptor} is registered for the message type.
*
* <p>When a message-typed expression is evaluated in {@code CelLiteRuntime}:
*
* <ul>
* <li>If a {@code CelLiteDescriptor} is registered for the message type, evaluation produces a
* {@code MessageLite} instance.
* <li>Otherwise, evaluation produces a {@code WireMessageLite} carrying the message's protobuf
* type name and wire-encoded payload.
* </ul>
*/
@Immutable
@Beta
public interface WireMessageLite {

/**
* Returns the fully-qualified protobuf message type name (e.g. {@code
* "cel.expr.conformance.proto3.TestAllTypes.NestedMessage"}), or {@code "cel.@unknownMessage"} if
* the message type name is not known at runtime.
*/
String protoTypeName();

/** Serializes the message to a {@link ByteString} in protobuf wire format. */
ByteString toByteString();
}
Loading
Loading