Skip to content

Commit

Permalink
Make ProtoCloudEventData consistent (cloudevents#535)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
JemDay committed Mar 10, 2023
1 parent 569e025 commit 4c81f3e
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 63 deletions.
56 changes: 52 additions & 4 deletions docs/protobuf.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();

...
}

```

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package io.cloudevents.protobuf;

import com.google.protobuf.Any;
import com.google.protobuf.Message;
import io.cloudevents.CloudEventData;

Expand All @@ -26,13 +27,7 @@
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
Expand All @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,31 @@
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
Expand All @@ -53,17 +60,21 @@ public boolean equals(Object obj) {
// 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)));

Expand All @@ -72,15 +83,7 @@ public boolean equals(Object obj) {
} else {
return false;
}
}

private Any getAsAny(Message m) {

if (m instanceof Any) {
return (Any) m;
}

return Any.pack(m);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,7 +30,6 @@
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;
Expand Down Expand Up @@ -244,16 +246,20 @@ public CloudEvent end(CloudEventData data) throws CloudEventRWException {

// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ public void testBasic() {
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
Expand Down Expand Up @@ -91,7 +91,7 @@ public void testBytes() {
final byte[] actData = pdw.toBytes();

// Verify
Arrays.equals(expData, actData);
assertThat(Arrays.equals(actData, expData)).isTrue();

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@

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;
Expand All @@ -49,7 +49,7 @@ public void verifyDataWrapper() {
assertThat(ced).isInstanceOf(ProtoCloudEventData.class);

ProtoCloudEventData pced = (ProtoCloudEventData) ced;
assertThat(pced.getMessage()).isNotNull();
assertThat(pced.getAny()).isNotNull();
}

@Test
Expand Down Expand Up @@ -82,12 +82,11 @@ public void verifyMessage() throws InvalidProtocolBufferException {
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);
}

Expand Down

0 comments on commit 4c81f3e

Please sign in to comment.