Support for spec 0.3

Signed-off-by: Fabio José <fabiojose@gmail.com>
This commit is contained in:
Fabio José 2019-08-19 22:07:08 -03:00
parent 4d5ed04fd9
commit 06fb04e42f
5 changed files with 775 additions and 0 deletions

View File

@ -0,0 +1,122 @@
package io.cloudevents.v03;
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 javax.validation.constraints.Size;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.cloudevents.Attributes;
import io.cloudevents.json.ZonedDateTimeDeserializer;
/**
* The event attributes implementation for v0.2
*
* @author fabiojose
*
*/
@JsonInclude(value = Include.NON_ABSENT)
public class AttributesImpl implements Attributes {
@NotBlank
private final String id;
@NotNull
private final URI source;
@NotBlank
@Pattern(regexp = "0\\.3")
private final String specversion;
@NotBlank
private final String type;
@JsonDeserialize(using = ZonedDateTimeDeserializer.class)
private final ZonedDateTime time;
private final URI schemaurl;
@Pattern(regexp = "base64")
private final String datacontentencoding;
private final String datacontenttype;
@Size(min = 1)
private final String subject;
AttributesImpl(String id, URI source, String specversion, String type,
ZonedDateTime time, URI schemaurl, String datacontentencoding,
String datacontenttype, String subject) {
this.id = id;
this.source = source;
this.specversion = specversion;
this.type = type;
this.time = time;
this.schemaurl = schemaurl;
this.datacontentencoding = datacontentencoding;
this.datacontenttype = datacontenttype;
this.subject = subject;
}
public String getId() {
return id;
}
public URI getSource() {
return source;
}
public String getSpecversion() {
return specversion;
}
public String getType() {
return type;
}
public Optional<ZonedDateTime> getTime() {
return Optional.ofNullable(time);
}
public Optional<URI> getSchemaurl() {
return Optional.ofNullable(schemaurl);
}
public Optional<String> getDatacontentencoding() {
return Optional.ofNullable(datacontentencoding);
}
public Optional<String> getDatacontenttype() {
return Optional.ofNullable(datacontenttype);
}
public Optional<String> getSubject() {
return Optional.ofNullable(subject);
}
@Override
public String toString() {
return "AttributesImpl [id=" + id + ", source=" + source + ", specversion="
+ specversion + ", type=" + type + ", time=" + time + ", schemaurl=" + schemaurl + ", datacontentencoding=" + datacontentencoding
+ ", datacontenttype=" + datacontenttype + ", subject=" + subject + "]";
}
@JsonCreator
public static AttributesImpl 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("datacontentenconding") String datacontentencoding,
@JsonProperty("datacontenttype") String datacontenttype,
@JsonProperty("subject") String subject) {
return new AttributesImpl(id, source, specversion, type, time,
schemaurl, datacontentencoding, datacontenttype, subject);
}
}

View File

@ -0,0 +1,153 @@
package io.cloudevents.v03;
import static java.lang.String.format;
import java.net.URI;
import java.time.ZonedDateTime;
import java.util.HashSet;
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;
import io.cloudevents.Event;
import io.cloudevents.ExtensionFormat;
/**
* The event builder.
*
* @author fabiojose
*
*/
public final class CloudEventBuilder<T> {
private static Validator VALIDATOR;
public static final String SPEC_VERSION = "0.3";
private static final String MESSAGE_SEPARATOR = ", ";
private static final String MESSAGE = "'%s' %s";
private static final String ERR_MESSAGE = "invalid payload: %s";
private String id;
private URI source;
private String type;
private ZonedDateTime time;
private URI schemaurl;
private String datacontentencoding;
private String datacontenttype;
private String subject;
private T data;
private final Set<ExtensionFormat> extensions = new HashSet<>();
private CloudEventBuilder() {}
private static Validator getValidator() {
if(null== VALIDATOR) {
VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
}
return VALIDATOR;
}
/**
* Gets a brand new builder instance
* @param <T> The 'data' type
*/
public static <T> CloudEventBuilder<T> builder() {
return new CloudEventBuilder<T>();
}
/**
*
* @return An new {@link Event} immutable instance
* @throws IllegalStateException When there are specification constraints
* violations
*/
public CloudEventImpl<T> build() {
AttributesImpl attributes = new AttributesImpl(id, source, SPEC_VERSION,
type, time, schemaurl, datacontentencoding, datacontenttype,
subject);
CloudEventImpl<T> cloudEvent =
new CloudEventImpl<T>(attributes, data, extensions);
Set<ConstraintViolation<Object>> violations =
getValidator().validate(cloudEvent);
violations.addAll(getValidator().validate(cloudEvent.getAttributes()));
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 cloudEvent;
}
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> withType(String type) {
this.type = type;
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> withDatacontentencoding(
String datacontentencoding) {
this.datacontentencoding = datacontentencoding;
return this;
}
public CloudEventBuilder<T> withDatacontenttype(
String datacontenttype) {
this.datacontenttype = datacontenttype;
return this;
}
public CloudEventBuilder<T> withSubject(
String subject) {
this.subject = subject;
return this;
}
public CloudEventBuilder<T> withData(T data) {
this.data = data;
return this;
}
public CloudEventBuilder<T> withExtension(ExtensionFormat extension) {
this.extensions.add(extension);
return this;
}
}

View File

@ -0,0 +1,113 @@
package io.cloudevents.v03;
import java.net.URI;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import io.cloudevents.Event;
import io.cloudevents.ExtensionFormat;
/**
* The event implementation
*
* @author fabiojose
*
*/
@JsonInclude(value = Include.NON_ABSENT)
public class CloudEventImpl<T> implements Event<AttributesImpl, T> {
@JsonIgnore
@JsonUnwrapped
@NotNull
private final AttributesImpl attributes;
private final T data;
@NotNull
private final Map<String, Object> extensions;
CloudEventImpl(AttributesImpl attributes, T data,
Set<ExtensionFormat> extensions) {
this.attributes = attributes;
this.data = data;
this.extensions = extensions
.stream()
.collect(Collectors
.toMap(ExtensionFormat::getKey,
ExtensionFormat::getExtension));
}
@Override
public AttributesImpl getAttributes() {
return attributes;
}
@Override
public Optional<T> getData() {
return Optional.ofNullable(data);
}
@JsonAnyGetter
@Override
public Map<String, Object> getExtensions() {
return Collections.unmodifiableMap(extensions);
}
/**
* The unique method that allows mutable. Used by
* Jackson Framework to inject the extensions.
*
* @param name Extension name
* @param value Extension value
*/
@JsonAnySetter
void addExtension(String name, Object value) {
extensions.put(name, value);
}
/**
* Used by the Jackson Framework to unmarshall.
*/
@JsonCreator
public static <T> CloudEventImpl<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("datacontentencoding") String datacontentencoding,
@JsonProperty("datacontenttype") String datacontenttype,
@JsonProperty("subject") String subject,
@JsonProperty("data") T data) {
return CloudEventBuilder.<T>builder()
.withId(id)
.withSource(source)
.withType(type)
.withTime(time)
.withSchemaurl(schemaurl)
.withDatacontentencoding(datacontentencoding)
.withDatacontenttype(datacontenttype)
.withData(data)
.withSubject(subject)
.build();
}
}

View File

@ -0,0 +1,138 @@
package io.cloudevents.v03;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.net.URI;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import io.cloudevents.Event;
/**
*
* @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
CloudEventBuilder.builder()
.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
CloudEventBuilder.builder()
.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
CloudEventBuilder.builder()
.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
CloudEventBuilder.builder()
.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
CloudEventBuilder.builder()
.withId("id")
.withType("type")
.build();
}
@Test
public void error_when_empty_subject() {
// setup
expectedEx.expect(IllegalStateException.class);
expectedEx.expectMessage("invalid payload: 'subject' size must be between 1 and 2147483647");
// act
CloudEventBuilder.<Object>builder()
.withId("id")
.withType("type")
.withSource(URI.create("/source"))
.withSubject("")
.build();
}
@Test
public void error_when_invalid_encoding() {
// setup
expectedEx.expect(IllegalStateException.class);
expectedEx.expectMessage("invalid payload: 'datacontentencoding' must match \"base64\"");
// act
CloudEventBuilder.<Object>builder()
.withId("id")
.withType("type")
.withSource(URI.create("/source"))
.withSubject("subject")
.withDatacontentencoding("binary")
.build();
}
@Test
public void should_have_subject() {
// act
Event<AttributesImpl, Object> ce =
CloudEventBuilder.<Object>builder()
.withId("id")
.withSource(URI.create("/source"))
.withType("type")
.withSubject("subject")
.build();
// assert
assertTrue(ce.getAttributes().getSubject().isPresent());
assertEquals("subject", ce.getAttributes().getSubject().get());
}
}

View File

@ -0,0 +1,249 @@
package io.cloudevents.v03;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
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 com.fasterxml.jackson.core.type.TypeReference;
import io.cloudevents.Event;
import io.cloudevents.ExtensionFormat;
import io.cloudevents.extensions.DistributedTracingExtension;
import io.cloudevents.json.Json;
import io.cloudevents.json.types.Much;
/**
*
* @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
Event<AttributesImpl, Object> ce =
CloudEventBuilder.builder()
.withId("x10")
.withSource(URI.create("/source"))
.withType("event-type")
.build();
// act
String json = Json.encode(ce);
System.out.println(json);
// assert
assertTrue(json.contains("x10"));
assertTrue(json.contains("/source"));
assertTrue(json.contains("event-type"));
assertTrue(json.contains("0.3"));
assertFalse(json.contains("time"));
assertFalse(json.contains("schemaurl"));
assertFalse(json.contains("contenttype"));
assertFalse(json.contains("data"));
}
@Test
public void should_have_optional_attrs() {
// setup
Event<AttributesImpl, Object> ce =
CloudEventBuilder.builder()
.withId("x10")
.withSource(URI.create("/source"))
.withType("event-type")
.withSchemaurl(URI.create("/schema"))
.withDatacontenttype("text/plain")
.withDatacontentencoding("base64")
.withSubject("subject0")
.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"));
assertTrue(json.contains("\"base64\""));
assertTrue(json.contains("subject0"));
assertTrue(json.contains("\"schemaurl\""));
assertTrue(json.contains("datacontenttype"));
assertTrue(json.contains("datacontentencoding"));
assertTrue(json.contains("\"subject\""));
}
@Test
public void should_serialize_trace_extension() {
// setup
String expected = "\"distributedTracing\":{\"traceparent\":\"0\",\"tracestate\":\"congo=4\"}";
final DistributedTracingExtension dt = new DistributedTracingExtension();
dt.setTraceparent("0");
dt.setTracestate("congo=4");
final ExtensionFormat tracing = new DistributedTracingExtension.InMemory(dt);
Event<AttributesImpl, Object> ce =
CloudEventBuilder.builder()
.withId("x10")
.withSource(URI.create("/source"))
.withType("event-type")
.withSchemaurl(URI.create("/schema"))
.withDatacontenttype("text/plain")
.withData("my-data")
.withExtension(tracing)
.build();
// act
String actual = Json.encode(ce);
System.out.println(actual);
// assert
assertTrue(actual.contains(expected));
}
@Test
public void should_have_type() {
// act
Event<AttributesImpl, Object> ce =
Json.fromInputStream(resourceOf("03_new.json"), CloudEventImpl.class);
// assert
assertEquals("aws.s3.object.created", ce.getAttributes().getType());
}
@Test
public void should_have_id() {
// act
Event<AttributesImpl, Object> ce =
Json.fromInputStream(resourceOf("03_new.json"), CloudEventImpl.class);
// assert
assertEquals("C234-1234-1234", ce.getAttributes().getId());
}
//should have time
@Test
public void should_have_time() {
// act
Event<AttributesImpl, Object> ce =
Json.fromInputStream(resourceOf("03_new.json"), CloudEventImpl.class);
// assert
assertTrue(ce.getAttributes().getTime().isPresent());
}
@Test
public void should_have_source() {
// act
Event<AttributesImpl, Object> ce =
Json.fromInputStream(resourceOf("03_new.json"), CloudEventImpl.class);
// assert
assertEquals(URI.create("https://serverless.com"), ce.getAttributes().getSource());
}
@Test
public void should_have_datacontenttype() {
// act
Event<AttributesImpl, Object> ce =
Json.fromInputStream(resourceOf("03_new.json"), CloudEventImpl.class);
// assert
assertTrue(ce.getAttributes().getDatacontenttype().isPresent());
assertEquals("application/json", ce.getAttributes().getDatacontenttype().get());
}
@Test
public void should_have_datacontentencoding() {
// act
Event<AttributesImpl, Object> ce =
Json.fromInputStream(resourceOf("03_base64.json"), CloudEventImpl.class);
// assert
assertTrue(ce.getAttributes().getDatacontentencoding().isPresent());
assertEquals("base64", ce.getAttributes().getDatacontentencoding().get());
}
@Test
public void should_have_specversion() {
// act
Event<AttributesImpl, Object> ce =
Json.fromInputStream(resourceOf("03_new.json"), CloudEventImpl.class);
// assert
assertEquals("0.3", ce.getAttributes().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("03_absent.json"), CloudEventImpl.class);
}
@Test
public void should_have_tracing_extension() {
// act
Event<AttributesImpl, Object> ce =
Json.fromInputStream(resourceOf("03_extension.json"), CloudEventImpl.class);
// assert
assertNotNull(ce.getExtensions()
.get(DistributedTracingExtension.InMemory.IN_MEMORY_KEY));
}
@Test
public void should_have_custom_extension() {
// setup
String extensionKey = "my-extension";
String expected = "extension-value";
// act
Event<AttributesImpl, Object> ce =
Json.fromInputStream(resourceOf("03_extension.json"), CloudEventImpl.class);
// assert
assertEquals(expected, ce.getExtensions()
.get(extensionKey));
}
@Test
public void should_have_custom_data() {
// setup
Much expected = new Much();
expected.setWow("kinda");
String json = "{\"type\":\"aws.s3.object.created\",\"id\":\"C234-1234-1234\",\"time\":\"2019-08-19T19:35:00.000Z\",\"source\":\"https://serverless.com\",\"datacontenttype\":\"application/json\",\"specversion\":\"0.3\",\"data\":{\"wow\":\"kinda\"}}";
// act
Event<AttributesImpl, Much> ce =
Json.decodeValue(json, new TypeReference<CloudEventImpl<Much>>() {});
// assert
assertTrue(ce.getData().isPresent());
assertEquals(expected.getWow(), ce.getData().get().getWow());
}
}