Merge pull request #42 from fabiojose/master

Fixing the issue #32
This commit is contained in:
Matthias Wessendorf 2019-08-14 15:18:53 +02:00 committed by GitHub
commit a00ddcad35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 628 additions and 8 deletions

View File

@ -3,7 +3,6 @@ language: java
sudo: false
jdk:
- oraclejdk8
- openjdk8
- openjdk9
- openjdk10

View File

@ -19,13 +19,17 @@ For Maven based projects, use the following to configure the CloudEvents Java SD
<dependency>
<groupId>io.cloudevents</groupId>
<artifactId>cloudevents-api</artifactId>
<version>0.2.1</version>
<version>0.2.2</version>
</dependency>
```
Application developers can now create strongly-typed CloudEvents, such as:
```java
import io.cloudevents.v02.CloudEventBuilder;
import io.cloudevents.v02.CloudEvent;
import io.cloudevents.json.Json;
// given
final String eventId = UUID.randomUUID().toString();
final URI src = URI.create("/trigger");
@ -34,11 +38,14 @@ final MyCustomEvent payload = ...
// passing in the given attributes
final CloudEvent<MyCustomEvent> cloudEvent = new CloudEventBuilder<MyCustomEvent>()
.type(eventType)
.id(eventId)
.source(src)
.data(payload)
.withType(eventType)
.withId(eventId)
.withSource(src)
.withData(payload)
.build();
// marshalling as json
final String json = Json.encode(cloudEvent);
```
## Possible Integrations

View File

@ -42,6 +42,18 @@
<artifactId>jackson-datatype-jdk8</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>${javax.el.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
@ -61,6 +73,8 @@
<properties>
<jackson.version>2.9.6</jackson.version>
<hibernate-validator.version>6.0.17.Final</hibernate-validator.version>
<javax.el.version>3.0.1-b11</javax.el.version>
</properties>
</project>

View File

@ -140,6 +140,7 @@ public class DefaultCloudEventImpl<T> implements CloudEvent<T>, Serializable {
this.schemaURL = schemaURL;
}
@JsonAlias("contenttype")
void setContentType(String contentType) {
this.contentType = contentType;
}

View File

@ -61,7 +61,17 @@ public final class Json {
throw new IllegalStateException("Failed to encode as JSON: " + e.getMessage());
}
}
public static <T> T fromInputStream(final InputStream inputStream,
Class<T> clazz) {
try {
return MAPPER.readValue(inputStream, clazz);
} catch (Exception e) {
throw new IllegalStateException("Failed to encode as JSON: "
+ e.getMessage());
}
}
/**
* Decode a given JSON string to a CloudEvent .
*

View File

@ -0,0 +1,112 @@
package io.cloudevents.v02;
import java.net.URI;
import java.time.ZonedDateTime;
import java.util.Optional;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.cloudevents.json.ZonedDateTimeDeserializer;
/**
*
* @author fabiojose
*
* Implemented using immutable data structure.
*
*/
@JsonInclude(value = Include.NON_ABSENT)
public class CloudEvent<T> {
@NotBlank
private final String type;
@NotBlank
@Pattern(regexp = "0\\.2")
private final String specversion;
@NotNull
private final URI source;
@NotBlank
private final String id;
private final ZonedDateTime time;
private final URI schemaurl;
private final String contenttype;
private final T data;
CloudEvent(String id, URI source, String specversion, String type,
ZonedDateTime time, URI schemaurl, String contenttype,
T data) {
this.id = id;
this.source = source;
this.specversion = specversion;
this.type = type;
this.time = time;
this.schemaurl = schemaurl;
this.contenttype = contenttype;
this.data = data;
}
public String getType() {
return type;
}
public String getId() {
return id;
}
public String getSpecversion() {
return specversion;
}
public URI getSource() {
return source;
}
@JsonDeserialize(using = ZonedDateTimeDeserializer.class)
public Optional<ZonedDateTime> getTime() {
return Optional.ofNullable(time);
}
public Optional<URI> getSchemaurl() {
return Optional.ofNullable(schemaurl);
}
public Optional<String> getContenttype() {
return Optional.ofNullable(contenttype);
}
public Optional<T> getData() {
return Optional.ofNullable(data);
}
@JsonCreator
public static <T> CloudEvent<T> build(
@JsonProperty("id") String id,
@JsonProperty("source") URI source,
@JsonProperty("specversion") String specversion,
@JsonProperty("type") String type,
@JsonProperty("time") ZonedDateTime time,
@JsonProperty("schemaurl") URI schemaurl,
@JsonProperty("contenttype") String contenttype,
@JsonProperty("data") T data) {
return new CloudEventBuilder<T>()
.withId(id)
.withSource(source)
.withType(type)
.withTime(time)
.withSchemaurl(schemaurl)
.withContenttype(contenttype)
.withData(data)
.build();
}
}

View File

@ -0,0 +1,103 @@
package io.cloudevents.v02;
import static java.lang.String.format;
import java.net.URI;
import java.time.ZonedDateTime;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
/**
* CloudEvent instances builder
*
* @author fabiojose
*
*/
public class CloudEventBuilder<T> {
private static final String SPEC_VERSION = "0.2";
private static final String MESSAGE_SEPARATOR = ", ";
private static final String MESSAGE = "'%s' %s";
private static final String ERR_MESSAGE = "invalid payload: %s";
private String type;
private String id;
private URI source;
private ZonedDateTime time;
private URI schemaurl;
private String contenttype;
private T data;
private Validator getValidator() {
return Validation.buildDefaultValidatorFactory().getValidator();
}
/**
*
* @return An new {@link CloudEvent} immutable instance
* @throws IllegalStateException When there are specification constraints
* violations
*/
public CloudEvent<T> build() {
CloudEvent<T> event = new CloudEvent<>(id, source, SPEC_VERSION, type,
time, schemaurl, contenttype, data);
Set<ConstraintViolation<CloudEvent<T>>> violations =
getValidator().validate(event);
final String errs =
violations.stream()
.map(v -> format(MESSAGE, v.getPropertyPath(), v.getMessage()))
.collect(Collectors.joining(MESSAGE_SEPARATOR));
Optional.ofNullable(
"".equals(errs) ? null : errs
).ifPresent((e) -> {
throw new IllegalStateException(format(ERR_MESSAGE, e));
});
return event;
}
public CloudEventBuilder<T> withType(String type) {
this.type = type;
return this;
}
public CloudEventBuilder<T> withId(String id) {
this.id = id;
return this;
}
public CloudEventBuilder<T> withSource(URI source) {
this.source = source;
return this;
}
public CloudEventBuilder<T> withTime(ZonedDateTime time) {
this.time = time;
return this;
}
public CloudEventBuilder<T> withSchemaurl(URI schemaurl) {
this.schemaurl = schemaurl;
return this;
}
public CloudEventBuilder<T> withContenttype(String contenttype) {
this.contenttype = contenttype;
return this;
}
public CloudEventBuilder<T> withData(T data) {
this.data = data;
return this;
}
}

View File

@ -0,0 +1,221 @@
package io.cloudevents.v02;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.net.URI;
import java.time.ZonedDateTime;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
/**
*
* @author fabiojose
*
*/
public class CloudEventBuilderTest {
@Rule
public ExpectedException expectedEx = ExpectedException.none();
@Test
public void error_when_null_id() {
// setup
expectedEx.expect(IllegalStateException.class);
expectedEx.expectMessage("invalid payload: 'id' must not be blank");
// act
new CloudEventBuilder<Object>()
.withSource(URI.create("/test"))
.withType("type")
.build();
}
@Test
public void error_when_empty_id() {
// setup
expectedEx.expect(IllegalStateException.class);
expectedEx.expectMessage("invalid payload: 'id' must not be blank");
// act
new CloudEventBuilder<Object>()
.withId("")
.withSource(URI.create("/test"))
.withType("type")
.build();
}
@Test
public void error_when_null_type() {
// setup
expectedEx.expect(IllegalStateException.class);
expectedEx.expectMessage("invalid payload: 'type' must not be blank");
// act
new CloudEventBuilder<Object>()
.withId("id")
.withSource(URI.create("/test"))
.build();
}
@Test
public void error_when_empty_type() {
// setup
expectedEx.expect(IllegalStateException.class);
expectedEx.expectMessage("invalid payload: 'type' must not be blank");
// act
new CloudEventBuilder<Object>()
.withId("id")
.withSource(URI.create("/test"))
.withType("")
.build();
}
@Test
public void error_when_null_source() {
// setup
expectedEx.expect(IllegalStateException.class);
expectedEx.expectMessage("invalid payload: 'source' must not be null");
// act
new CloudEventBuilder<Object>()
.withId("id")
.withType("type")
.build();
}
@Test
public void should_have_id() {
// act
CloudEvent<Object> ce =
new CloudEventBuilder<>()
.withId("id")
.withSource(URI.create("/source"))
.withType("type")
.build();
// assert
assertEquals("id", ce.getId());
}
@Test
public void should_have_source() {
// act
CloudEvent<Object> ce =
new CloudEventBuilder<>()
.withId("id")
.withSource(URI.create("/source"))
.withType("type")
.build();
// assert
assertEquals(URI.create("/source"), ce.getSource());
}
@Test
public void should_have_type() {
// act
CloudEvent<Object> ce =
new CloudEventBuilder<>()
.withId("id")
.withSource(URI.create("/source"))
.withType("type")
.build();
// assert
assertEquals("type", ce.getType());
}
@Test
public void should_have_specversion() {
// act
CloudEvent<Object> ce =
new CloudEventBuilder<>()
.withId("id")
.withSource(URI.create("/source"))
.withType("type")
.build();
// assert
assertEquals("0.2", ce.getSpecversion());
}
@Test
public void should_have_time() {
// setup
ZonedDateTime expected = ZonedDateTime.now();
// act
CloudEvent<Object> ce =
new CloudEventBuilder<>()
.withId("id")
.withSource(URI.create("/source"))
.withType("type")
.withTime(expected)
.build();
// assert
assertTrue(ce.getTime().isPresent());
assertEquals(expected, ce.getTime().get());
}
@Test
public void should_have_schemaurl() {
// setup
URI expected = URI.create("/schema");
// act
CloudEvent<Object> ce =
new CloudEventBuilder<>()
.withId("id")
.withSource(URI.create("/source"))
.withType("type")
.withSchemaurl(expected)
.build();
// assert
assertTrue(ce.getSchemaurl().isPresent());
assertEquals(expected, ce.getSchemaurl().get());
}
@Test
public void should_have_contenttype() {
// setup
String expected = "application/json";
// act
CloudEvent<Object> ce =
new CloudEventBuilder<>()
.withId("id")
.withSource(URI.create("/source"))
.withType("type")
.withContenttype(expected)
.build();
// assert
assertTrue(ce.getContenttype().isPresent());
assertEquals(expected, ce.getContenttype().get());
}
@Test
public void should_have_data() {
// setup
String expected = "my data";
// act
CloudEvent<Object> ce =
new CloudEventBuilder<>()
.withId("id")
.withSource(URI.create("/source"))
.withType("type")
.withData(expected)
.build();
// assert
assertTrue(ce.getData().isPresent());
assertEquals(expected, ce.getData().get());
}
}

View File

@ -0,0 +1,142 @@
package io.cloudevents.v02;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.InputStream;
import java.net.URI;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import io.cloudevents.json.Json;
/**
*
* @author fabiojose
*
*/
public class CloudEventJacksonTest {
private static InputStream resourceOf(String name) {
return Thread.currentThread().getContextClassLoader().getResourceAsStream(name);
}
@Rule
public ExpectedException expectedEx = ExpectedException.none();
@Test
public void should_encode_right_with_minimal_attrs() {
// setup
CloudEvent<Object> ce =
new CloudEventBuilder<>()
.withId("x10")
.withSource(URI.create("/source"))
.withType("event-type")
.build();
// act
String json = Json.encode(ce);
// assert
assertTrue(json.contains("x10"));
assertTrue(json.contains("/source"));
assertTrue(json.contains("event-type"));
assertTrue(json.contains("0.2"));
assertFalse(json.contains("time"));
assertFalse(json.contains("schemaurl"));
assertFalse(json.contains("contenttype"));
assertFalse(json.contains("data"));
}
@Test
public void should_have_optional_attrs() {
// setup
CloudEvent<Object> ce =
new CloudEventBuilder<>()
.withId("x10")
.withSource(URI.create("/source"))
.withType("event-type")
.withSchemaurl(URI.create("/schema"))
.withContenttype("text/plain")
.withData("my-data")
.build();
// act
String json = Json.encode(ce);
// assert
assertTrue(json.contains("/schema"));
assertTrue(json.contains("text/plain"));
assertTrue(json.contains("my-data"));
}
@Test
public void should_have_type() {
// act
CloudEvent<?> ce = Json.fromInputStream(resourceOf("02_aws.json"), CloudEvent.class);
// assert
assertEquals("aws.s3.object.created", ce.getType());
}
@Test
public void should_have_id() {
// act
CloudEvent<?> ce = Json.fromInputStream(resourceOf("02_aws.json"), CloudEvent.class);
// assert
assertEquals("C234-1234-1234", ce.getId());
}
//should have time
@Test
public void should_have_time() {
// act
CloudEvent<?> ce = Json.fromInputStream(resourceOf("02_aws.json"), CloudEvent.class);
// assert
assertTrue(ce.getTime().isPresent());
}
@Test
public void should_have_source() {
// act
CloudEvent<?> ce = Json.fromInputStream(resourceOf("02_aws.json"), CloudEvent.class);
// assert
assertEquals(URI.create("https://serverless.com"), ce.getSource());
}
@Test
public void should_have_contenttype() {
// act
CloudEvent<?> ce = Json.fromInputStream(resourceOf("02_aws.json"), CloudEvent.class);
// assert
assertTrue(ce.getContenttype().isPresent());
assertEquals("application/json", ce.getContenttype().get());
}
@Test
public void should_have_specversion() {
// act
CloudEvent<?> ce = Json.fromInputStream(resourceOf("02_aws.json"), CloudEvent.class);
// assert
assertEquals("0.2", ce.getSpecversion());
}
@Test
public void should_throw_when_absent() {
// setup
expectedEx.expect(IllegalStateException.class);
expectedEx.expectMessage("invalid payload: 'id' must not be blank");
// act
Json.fromInputStream(resourceOf("02_absent.json"), CloudEvent.class);
}
}

View File

@ -0,0 +1,7 @@
{
"type": "aws.s3.object.created",
"time": "2018-04-26T14:48:09.769Z",
"source": "https://serverless.com",
"contenttype": "application/json",
"specversion": "0.2"
}

View File

@ -3,7 +3,7 @@
"id": "C234-1234-1234",
"time": "2018-04-26T14:48:09.769Z",
"source": "https://serverless.com",
"contentType": "application/json",
"contenttype": "application/json",
"specversion": "0.2",
"data":
{ "s3SchemaVersion": "1.0",

View File

@ -53,6 +53,10 @@
<id>jponge</id>
<name>Julien Ponge</name>
</developer>
<developer>
<id>fabiojose</id>
<name>Fabio José de Moraes</name>
</developer>
</developers>
<modules>