Spec v1 support

Signed-off-by: Fabio José <fabiojose@gmail.com>
This commit is contained in:
Fabio José 2019-10-26 13:34:38 -03:00
parent 50508ba4b3
commit 698a3d1131
7 changed files with 937 additions and 0 deletions

View File

@ -0,0 +1,44 @@
package io.cloudevents.v1;
import java.util.Collection;
import java.util.Objects;
import io.cloudevents.Attributes;
import io.cloudevents.CloudEvent;
import io.cloudevents.extensions.ExtensionFormat;
import io.cloudevents.fun.ExtensionFormatAccessor;
/**
*
* @author fabiojose
* @version 1.0
*/
public class Accessor {
/**
* To get access the set of {@link ExtensionFormat} inside the
* event.
*
* <br>
* <br>
* This method follow the signature of
* {@link ExtensionFormatAccessor#extensionsOf(CloudEvent)}
*
* @param cloudEvent
* @throws IllegalArgumentException When argument is not an instance
* of {@link CloudEventImpl}
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static <A extends Attributes, T> Collection<ExtensionFormat>
extensionsOf(CloudEvent<A, T> cloudEvent) {
Objects.requireNonNull(cloudEvent);
if(cloudEvent instanceof CloudEventImpl) {
CloudEventImpl impl = (CloudEventImpl)cloudEvent;
return impl.getExtensionsFormats();
}
throw new IllegalArgumentException("Invalid instance type: "
+ cloudEvent.getClass());
}
}

View File

@ -0,0 +1,183 @@
/**
* Copyright 2019 The CloudEvents Authors
*
* 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
*
* http://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 io.cloudevents.v1;
import java.net.URI;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
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.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.cloudevents.Attributes;
import io.cloudevents.json.ZonedDateTimeDeserializer;
/**
*
* @author fabiojose
* @version 1.0
*/
public class AttributesImpl implements Attributes {
@NotBlank
private final String id;
@NotNull
private final URI source;
@NotBlank
@Pattern(regexp = "1\\.0")
private final String specversion;
@NotBlank
private final String type;
private final String datacontenttype;
private final URI dataschema;
@Size(min = 1)
private final String subject;
@JsonDeserialize(using = ZonedDateTimeDeserializer.class)
private final ZonedDateTime time;
public AttributesImpl(String id, URI source, String specversion,
String type, String datacontenttype,
URI dataschema, String subject, ZonedDateTime time) {
this.id = id;
this.source = source;
this.specversion = specversion;
this.type = type;
this.datacontenttype = datacontenttype;
this.dataschema = dataschema;
this.subject = subject;
this.time = time;
}
@Override
public Optional<String> getMediaType() {
return getDatacontenttype();
}
public String getId() {
return id;
}
public URI getSource() {
return source;
}
public String getSpecversion() {
return specversion;
}
public String getType() {
return type;
}
public Optional<String> getDatacontenttype() {
return Optional.ofNullable(datacontenttype);
}
public Optional<URI> getDataschema() {
return Optional.ofNullable(dataschema);
}
public Optional<String> getSubject() {
return Optional.ofNullable(subject);
}
public Optional<ZonedDateTime> getTime() {
return Optional.ofNullable(time);
}
@Override
public String toString() {
return "AttibutesImpl [id=" + id + ", source=" + source
+ ", specversion=" + specversion + ", type=" + type
+ ", datacontenttype=" + datacontenttype + ", dataschema="
+ dataschema + ", subject=" + subject
+ ", time=" + time + "]";
}
/**
* Used by the Jackson framework to unmarshall.
*/
@JsonCreator
public static AttributesImpl build(
@JsonProperty("id") String id,
@JsonProperty("source") URI source,
@JsonProperty("specversion") String specversion,
@JsonProperty("type") String type,
@JsonProperty("datacontenttype") String datacontenttype,
@JsonProperty("dataschema") URI dataschema,
@JsonProperty("subject") String subject,
@JsonProperty("time") ZonedDateTime time) {
return new AttributesImpl(id, source, specversion, type,
datacontenttype, dataschema, subject, time);
}
/**
* Creates the marshaller instance to marshall {@link AttributesImpl} as
* a {@link Map} of strings
*/
public static Map<String, String> marshal(AttributesImpl attributes) {
Objects.requireNonNull(attributes);
Map<String, String> result = new HashMap<>();
result.put(ContextAttributes.id.name(),
attributes.getId());
result.put(ContextAttributes.source.name(),
attributes.getSource().toString());
result.put(ContextAttributes.specversion.name(),
attributes.getSpecversion());
result.put(ContextAttributes.type.name(),
attributes.getType());
attributes.getDatacontenttype().ifPresent(dct -> {
result.put(ContextAttributes.datacontenttype.name(), dct);
});
attributes.getDataschema().ifPresent(dataschema -> {
result.put(ContextAttributes.dataschema.name(),
dataschema.toString());
});
attributes.getSubject().ifPresent(subject -> {
result.put(ContextAttributes.subject.name(), subject);
});
attributes.getTime().ifPresent(time -> {
result.put(ContextAttributes.time.name(),
time.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
});
return result;
}
}

View File

@ -0,0 +1,202 @@
package io.cloudevents.v1;
import static java.lang.String.format;
import java.net.URI;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
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.CloudEvent;
import io.cloudevents.extensions.ExtensionFormat;
import io.cloudevents.fun.EventBuilder;
/**
*
* @author fabiojose
* @version 1.0
*/
public class CloudEventBuilder<T> implements
EventBuilder<T, AttributesImpl> {
private CloudEventBuilder() {}
private static Validator VALIDATOR;
public static final String SPEC_VERSION = "1.0";
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 String datacontenttype;
private URI dataschema;
private String subject;
private ZonedDateTime time;
private T data;
private final Set<ExtensionFormat> extensions = new HashSet<>();
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>();
}
/**
* Builder with base event to copy attributes
* @param <T> The 'data' type
* @param base The base event to copy attributes
*/
public static <T> CloudEventBuilder<T> builder(
CloudEvent<AttributesImpl, T> base) {
Objects.requireNonNull(base);
CloudEventBuilder<T> result = new CloudEventBuilder<>();
AttributesImpl attributes = base.getAttributes();
result
.withId(attributes.getId())
.withSource(attributes.getSource())
.withType(attributes.getType());
attributes.getDataschema().ifPresent((schema) -> {
result.withDataschema(schema);
});
attributes.getDatacontenttype().ifPresent(dc -> {
result.withDatacontenttype(dc);
});
attributes.getSubject().ifPresent(subject -> {
result.withSubject(subject);
});
attributes.getTime().ifPresent(time -> {
result.withTime(time);
});
Accessor.extensionsOf(base)
.forEach(extension -> {
result.withExtension(extension);
});
base.getData().ifPresent(data -> {
result.withData(data);
});
return result;
}
@Override
public CloudEvent<AttributesImpl, T> build(T data,
AttributesImpl attributes,
Collection<ExtensionFormat> extensions) {
return null;
}
/**
*
* @return An new {@link CloudEvent} immutable instance
* @throws IllegalStateException When there are specification constraints
* violations
*/
public CloudEventImpl<T> build() {
AttributesImpl attributes = new AttributesImpl(id, source, SPEC_VERSION, type,
datacontenttype, dataschema, subject, time);
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> withDataschema(URI dataschema) {
this.dataschema = dataschema;
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> withTime(ZonedDateTime time) {
this.time = time;
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,135 @@
/**
* Copyright 2019 The CloudEvents Authors
*
* 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
*
* http://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 io.cloudevents.v1;
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.CloudEvent;
import io.cloudevents.extensions.ExtensionFormat;
import io.cloudevents.extensions.InMemoryFormat;
/**
*
* @author fabiojose
* @version 1.0
*/
@JsonInclude(value = Include.NON_ABSENT)
public class CloudEventImpl<T> implements CloudEvent<AttributesImpl, T> {
@JsonIgnore
@NotNull
private final AttributesImpl attributes;
private final T data;
@NotNull
private final Map<String, Object> extensions;
private final Set<ExtensionFormat> extensionsFormats;
CloudEventImpl(AttributesImpl attributes, T data,
Set<ExtensionFormat> extensions){
this.attributes = attributes;
this.data = data;
this.extensions = extensions.stream()
.map(ExtensionFormat::memory)
.collect(Collectors.toMap(InMemoryFormat::getKey,
InMemoryFormat::getValue));
this.extensionsFormats = extensions;
}
/**
* Used by the {@link Accessor} to access the set of {@link ExtensionFormat}
*/
Set<ExtensionFormat> getExtensionsFormats() {
return extensionsFormats;
}
@JsonUnwrapped
@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 mutation. 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("datacontenttype") String datacontenttype,
@JsonProperty("dataschema") URI dataschema,
@JsonProperty("subject") String subject,
@JsonProperty("time") ZonedDateTime time,
@JsonProperty("data") T data){
return CloudEventBuilder.<T>builder()
.withId(id)
.withSource(source)
.withType(type)
.withTime(time)
.withDataschema(dataschema)
.withDatacontenttype(datacontenttype)
.withData(data)
.withSubject(subject)
.build();
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright 2019 The CloudEvents Authors
*
* 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
*
* http://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 io.cloudevents.v1;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
*
* @author fabiojose
* @version 1.0
*/
public enum ContextAttributes {
id,
source,
specversion,
type,
datacontenttype,
dataschema,
subject,
time;
public static final List<String> VALUES =
Arrays.asList(ContextAttributes.values())
.stream()
.map(Enum::name)
.collect(Collectors.toList());
}

View File

@ -0,0 +1,136 @@
/**
* Copyright 2019 The CloudEvents Authors
*
* 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
*
* http://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 io.cloudevents.v1;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.net.URI;
import java.util.Collection;
import org.junit.Test;
import io.cloudevents.extensions.DistributedTracingExtension;
import io.cloudevents.extensions.ExtensionFormat;
import io.cloudevents.extensions.InMemoryFormat;
import io.cloudevents.v1.Accessor;
import io.cloudevents.v1.CloudEventBuilder;
import io.cloudevents.v1.CloudEventImpl;
/**
*
* @author fabiojose
*
*/
public class AccessorTest {
@Test
public void should_empty_collection_when_no_extensions() {
// setup
CloudEventImpl<Object> ce =
CloudEventBuilder.<Object>builder()
.withId("x10")
.withSource(URI.create("/source"))
.withType("event-type")
.withDataschema(URI.create("/schema"))
.withDatacontenttype("text/plain")
.withData("my-data")
.build();
// act
Collection<ExtensionFormat> actual = Accessor.extensionsOf(ce);
// assert
assertTrue(actual.isEmpty());
}
@Test
public void should_return_the_tracing_extension() {
// setup
final DistributedTracingExtension dt = new DistributedTracingExtension();
dt.setTraceparent("0");
dt.setTracestate("congo=4");
final ExtensionFormat expected = new DistributedTracingExtension.Format(dt);
CloudEventImpl<Object> ce =
CloudEventBuilder.<Object>builder()
.withId("x10")
.withSource(URI.create("/source"))
.withType("event-type")
.withDataschema(URI.create("/schema"))
.withDatacontenttype("text/plain")
.withData("my-data")
.withExtension(expected)
.build();
// act
Collection<ExtensionFormat> extensions =
Accessor.extensionsOf(ce);
// assert
assertFalse(extensions.isEmpty());
ExtensionFormat actual = extensions.iterator().next();
assertEquals("0", actual.transport().get("traceparent"));
assertEquals("congo=4", actual.transport().get("tracestate"));
assertEquals("0",
((DistributedTracingExtension)actual.memory().getValue()).getTraceparent());
assertEquals("congo=4",
((DistributedTracingExtension)actual.memory().getValue()).getTracestate());
}
@Test
public void should_return_the_custom_extension() {
// setup
String customExt = "comexampleextension1";
String customVal = "my-ext-val";
InMemoryFormat inMemory =
InMemoryFormat.of(customExt, customVal, String.class);
ExtensionFormat expected =
ExtensionFormat.of(inMemory, customExt, customVal);
CloudEventImpl<Object> ce =
CloudEventBuilder.<Object>builder()
.withId("x10")
.withSource(URI.create("/source"))
.withType("event-type")
.withDataschema(URI.create("/schema"))
.withDatacontenttype("text/plain")
.withData("my-data")
.withExtension(expected)
.build();
// act
Collection<ExtensionFormat> extensions =
Accessor.extensionsOf(ce);
// assert
assertFalse(extensions.isEmpty());
ExtensionFormat actual = extensions.iterator().next();
assertEquals(customVal, actual.transport().get(customExt));
assertEquals(String.class, actual.memory().getValueType());
assertEquals(customExt, actual.memory().getKey());
assertEquals(customVal, actual.memory().getValue());
}
}

View File

@ -0,0 +1,194 @@
/**
* Copyright 2019 The CloudEvents Authors
*
* 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
*
* http://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 io.cloudevents.v1;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
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.CloudEvent;
import io.cloudevents.extensions.DistributedTracingExtension;
import io.cloudevents.extensions.ExtensionFormat;
import io.cloudevents.extensions.InMemoryFormat;
import io.cloudevents.v1.AttributesImpl;
import io.cloudevents.v1.CloudEventBuilder;
import io.cloudevents.v1.CloudEventImpl;
/**
*
* @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 should_have_subject() {
// act
CloudEvent<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());
}
@Test
public void should_have_dte() {
// setup
final DistributedTracingExtension dt = new DistributedTracingExtension();
dt.setTraceparent("0");
dt.setTracestate("congo=4");
final ExtensionFormat tracing = new DistributedTracingExtension.Format(dt);
// act
CloudEventImpl<Object> ce =
CloudEventBuilder.builder()
.withId("id")
.withSource(URI.create("/source"))
.withType("type")
.withExtension(tracing)
.build();
Object actual = ce.getExtensions()
.get(DistributedTracingExtension.Format.IN_MEMORY_KEY);
// assert
assertNotNull(actual);
assertTrue(actual instanceof DistributedTracingExtension);
}
@Test
public void should_have_custom_extension() {
String myExtKey = "comexampleextension1";
String myExtVal = "value";
ExtensionFormat custom = ExtensionFormat
.of(InMemoryFormat.of(myExtKey, myExtKey, String.class),
myExtKey, myExtVal);
// act
CloudEventImpl<Object> ce =
CloudEventBuilder.builder()
.withId("id")
.withSource(URI.create("/source"))
.withType("type")
.withExtension(custom)
.build();
Object actual = ce.getExtensions()
.get(myExtKey);
assertNotNull(actual);
assertTrue(actual instanceof String);
}
}