Make ProtoCloudEventData consistent (#535)

Modified ProtoCloudEventData to always return a 
Protobuf Any object - this ensures it is coherent with
the Protobuf CloudEvent format specification.

It remains possible to wrap any Protobuf 'Message'
object directly (which includes an 'Any') as a
convienience to reduce application code.

Signed-off-by: Jem Day <Jem.Day@cliffhanger.com>
This commit is contained in:
Jem Day 2023-03-10 00:35:24 -08:00 committed by GitHub
parent 569e025cf0
commit 4c81f3eacc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 117 additions and 63 deletions

View File

@ -11,20 +11,22 @@ This module provides the Protocol Buffer (protobuf) `EventFormat` implementation
Protobuf runtime and classes generated from the CloudEvents
[proto spec](https://github.com/cloudevents/spec/blob/v1.0.1/spec.proto).
# Setup
For Maven based projects, use the following dependency:
```xml
<dependency>
<groupId>io.cloudevents</groupId>
<artifactId>cloudevents-protobuf</artifactId>
<version>2.3.0</version>
<version>x.y.z</version>
</dependency>
```
No further configuration is required is use the module.
## Using the Protobuf Event Format
You don't need to perform any operation to configure the module, more than
adding the dependency to your project:
### Event serialization
```java
import io.cloudevents.CloudEvent;
@ -44,5 +46,51 @@ byte[]serialized = EventFormatProvider
.serialize(event);
```
The `EventFormatProvider` will resolve automatically the `ProtobufFormat` using the
The `EventFormatProvider` will automatically resolve the `ProtobufFormat` using the
`ServiceLoader` APIs.
## Passing Protobuf messages as CloudEvent data.
The `ProtoCloudEventData` capability provides a convenience mechanism to handle Protobuf message object data.
### Building
```java
// Build my business event message.
com.google.protobuf.Message myMessage = ..... ;
// Wrap the protobuf message as CloudEventData.
CloudEventData ceData = ProtoCloudEventData.wrap(myMessage);
// Build the CloudEvent
CloudEvent event = CloudEventBuilder.v1()
.withId("hello")
.withType("example.protodata")
.withSource(URI.create("http://localhost"))
.withData(ceData)
.build();
```
### Reading
If the `ProtobufFormat` is used to deserialize a CloudEvent that contains a protobuf message object as data you can use
the `ProtoCloudEventData` to access it as an 'Any' directly.
```java
// Deserialize the event.
CloudEvent myEvent = eventFormat.deserialize(raw);
// Get the Data
CloudEventData eventData = myEvent.getData();
if (ceData instanceOf ProtoCloudEventData) {
// Obtain the protobuf 'any'
Any anAny = ((ProtoCloudEventData) eventData).getAny();
...
}
```

View File

@ -16,6 +16,7 @@
*/
package io.cloudevents.protobuf;
import com.google.protobuf.Any;
import com.google.protobuf.Message;
import io.cloudevents.CloudEventData;
@ -26,13 +27,7 @@ import io.cloudevents.CloudEventData;
public interface ProtoCloudEventData extends CloudEventData {
/**
* Gets the protobuf {@link Message} representation of this data.
* @return The data as a {@link Message}
*/
Message getMessage();
/**
* Convenience helper to wrap a Protobuf Message as
* Convenience helper to wrap a Protobuf {@link Message} as
* CloudEventData.
*
* @param protoMessage The message to wrap
@ -41,4 +36,11 @@ public interface ProtoCloudEventData extends CloudEventData {
static CloudEventData wrap(Message protoMessage) {
return new ProtoDataWrapper(protoMessage);
}
/**
* Gets the protobuf {@link Any} representation of this data.
*
* @return The data as an {@link Any}
*/
Any getAny();
}

View File

@ -19,24 +19,31 @@ package io.cloudevents.protobuf;
import com.google.protobuf.Any;
import com.google.protobuf.Message;
import java.util.Arrays;
import java.util.Objects;
class ProtoDataWrapper implements ProtoCloudEventData {
private final Message protoMessage;
private final Any protoAny;
ProtoDataWrapper(Message protoMessage) {
this.protoMessage = protoMessage;
Objects.requireNonNull(protoMessage);
if (protoMessage instanceof Any) {
protoAny = (Any) protoMessage;
} else {
protoAny = Any.pack(protoMessage);
}
}
@Override
public Message getMessage() {
return protoMessage;
public Any getAny() {
return protoAny;
}
@Override
public byte[] toBytes() {
return protoMessage.toByteArray();
return protoAny.toByteArray();
}
@Override
@ -53,17 +60,21 @@ class ProtoDataWrapper implements ProtoCloudEventData {
// Now compare the actual data
ProtoDataWrapper rhs = (ProtoDataWrapper) obj;
if (this.getMessage() == rhs.getMessage()){
return true;
}
final Any lhsAny = getAny();
final Any rhsAny = rhs.getAny();
// This is split out for readability.
// Compare the content in terms onf an 'Any'.
// - Verify the types match
// - Verify the values match.
// 1. Sanity compare the 'Any' references.
// 2. Compare the content in terms onf an 'Any'.
// - Verify the types match
// - Verify the values match.
final Any lhsAny = getAsAny(this.getMessage());
final Any rhsAny = getAsAny(rhs.getMessage());
// NULL checks not required as object cannot be built
// with a null.
if (lhsAny == rhsAny) {
return true;
}
final boolean typesMatch = (ProtoSupport.extractMessageType(lhsAny).equals(ProtoSupport.extractMessageType(rhsAny)));
@ -72,15 +83,7 @@ class ProtoDataWrapper implements ProtoCloudEventData {
} else {
return false;
}
}
private Any getAsAny(Message m) {
if (m instanceof Any) {
return (Any) m;
}
return Any.pack(m);
}
}

View File

@ -16,18 +16,14 @@
*/
package io.cloudevents.protobuf;
import com.google.protobuf.Message;
import io.cloudevents.CloudEventData;
import io.cloudevents.SpecVersion;
import io.cloudevents.core.data.BytesCloudEventData;
import io.cloudevents.core.v1.CloudEventV1;
import io.cloudevents.rw.CloudEventDataMapper;
import io.cloudevents.rw.CloudEventRWException;
import io.cloudevents.rw.CloudEventReader;
import io.cloudevents.rw.CloudEventWriter;
import io.cloudevents.rw.CloudEventWriterFactory;
import io.cloudevents.rw.*;
import io.cloudevents.v1.proto.CloudEvent;
import io.cloudevents.v1.proto.CloudEvent.CloudEventAttributeValue;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Instant;

View File

@ -16,8 +16,11 @@
*/
package io.cloudevents.protobuf;
import com.google.protobuf.*;
import com.google.protobuf.Any;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Timestamp;
import io.cloudevents.CloudEventData;
import io.cloudevents.SpecVersion;
import io.cloudevents.core.CloudEventUtils;
@ -27,7 +30,6 @@ import io.cloudevents.rw.CloudEventContextWriter;
import io.cloudevents.rw.CloudEventRWException;
import io.cloudevents.rw.CloudEventWriter;
import io.cloudevents.v1.proto.CloudEvent;
import io.cloudevents.v1.proto.CloudEvent.*;
import java.net.URI;
import java.time.Instant;
@ -244,16 +246,20 @@ class ProtoSerializer {
// If it's a proto message we can handle that directly.
if (data instanceof ProtoCloudEventData) {
final ProtoCloudEventData protoData = (ProtoCloudEventData) data;
final Message m = protoData.getMessage();
if (m != null) {
// If it's already an 'Any' don't re-pack it.
if (m instanceof Any) {
protoBuilder.setProtoData((Any) m);
}else {
protoBuilder.setProtoData(Any.pack(m));
}
final Any anAny = protoData.getAny();
// Even though our local implementation cannot be instantiated
// with NULL data nothing stops somebody from having their own
// variant that isn't as 'safe'.
if (anAny != null) {
protoBuilder.setProtoData(anAny);
} else {
throw CloudEventRWException.newOther("ProtoCloudEventData: getAny() was NULL");
}
} else {
if (Objects.equals(dataContentType, PROTO_DATA_CONTENT_TYPE)) {
// This will throw if the data provided is not an Any. The protobuf CloudEvent spec requires proto data to be stored as

View File

@ -45,12 +45,12 @@ class ProtoDataWrapperTest {
ProtoDataWrapper pdw = new ProtoDataWrapper(quote1);
assertThat(pdw).isNotNull();
assertThat(pdw.getMessage()).isNotNull();
assertThat(pdw.getAny()).isNotNull();
assertThat(pdw.toBytes()).withFailMessage("toBytes was NULL").isNotNull();
assertThat(pdw.toBytes()).withFailMessage("toBytes[] returned empty array").hasSizeGreaterThan(0);
// This is current behavior and will probably change in the next version.
assertThat(pdw.getMessage()).isInstanceOf(io.cloudevents.test.v1.proto.Test.Quote.class);
// Ensure it's a Quote.
assertThat(pdw.getAny().is(io.cloudevents.test.v1.proto.Test.Quote.class)).isTrue();
}
@Test
@ -91,7 +91,7 @@ class ProtoDataWrapperTest {
final byte[] actData = pdw.toBytes();
// Verify
Arrays.equals(expData, actData);
assertThat(Arrays.equals(actData, expData)).isTrue();
}

View File

@ -18,16 +18,16 @@ package io.cloudevents.protobuf;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import io.cloudevents.CloudEvent;
import io.cloudevents.CloudEventData;
import io.cloudevents.core.builder.CloudEventBuilder;
import io.cloudevents.core.format.EventFormat;
import io.cloudevents.test.v1.proto.Test.Decimal;
import io.cloudevents.test.v1.proto.Test.Quote;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.net.URI;
import org.junit.jupiter.api.Test;
import io.cloudevents.test.v1.proto.Test.Quote;
import io.cloudevents.test.v1.proto.Test.Decimal;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
import static org.assertj.core.api.Assertions.assertThat;
@ -49,7 +49,7 @@ public class ProtoMessageDataTest {
assertThat(ced).isInstanceOf(ProtoCloudEventData.class);
ProtoCloudEventData pced = (ProtoCloudEventData) ced;
assertThat(pced.getMessage()).isNotNull();
assertThat(pced.getAny()).isNotNull();
}
@Test
@ -82,12 +82,11 @@ public class ProtoMessageDataTest {
assertThat(eventData).isNotNull();
assertThat(eventData).isInstanceOf(ProtoCloudEventData.class);
Message newMessage = ((ProtoCloudEventData) eventData).getMessage();
assertThat(newMessage).isNotNull();
assertThat(newMessage).isInstanceOf(Any.class);
Any newAny = ((ProtoCloudEventData) eventData).getAny();
assertThat(newAny).isNotNull();
// Hydrate the data - maybe there's a cleaner way to do this.
Quote newQuote = ((Any) newMessage).unpack(Quote.class);
Quote newQuote = newAny.unpack(Quote.class);
assertThat(newQuote).ignoringRepeatedFieldOrder().isEqualTo(pyplQuote);
}