diff --git a/api/src/main/java/io/cloudevents/v1/Accessor.java b/api/src/main/java/io/cloudevents/v1/Accessor.java new file mode 100644 index 00000000..b08d34c7 --- /dev/null +++ b/api/src/main/java/io/cloudevents/v1/Accessor.java @@ -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. + * + *
+ *
+ * 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 Collection + extensionsOf(CloudEvent cloudEvent) { + Objects.requireNonNull(cloudEvent); + + if(cloudEvent instanceof CloudEventImpl) { + CloudEventImpl impl = (CloudEventImpl)cloudEvent; + return impl.getExtensionsFormats(); + } + + throw new IllegalArgumentException("Invalid instance type: " + + cloudEvent.getClass()); + } +} diff --git a/api/src/main/java/io/cloudevents/v1/AttributesImpl.java b/api/src/main/java/io/cloudevents/v1/AttributesImpl.java new file mode 100644 index 00000000..412b4048 --- /dev/null +++ b/api/src/main/java/io/cloudevents/v1/AttributesImpl.java @@ -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 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 getDatacontenttype() { + return Optional.ofNullable(datacontenttype); + } + + public Optional getDataschema() { + return Optional.ofNullable(dataschema); + } + + public Optional getSubject() { + return Optional.ofNullable(subject); + } + + public Optional 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 marshal(AttributesImpl attributes) { + Objects.requireNonNull(attributes); + Map 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; + } +} diff --git a/api/src/main/java/io/cloudevents/v1/CloudEventBuilder.java b/api/src/main/java/io/cloudevents/v1/CloudEventBuilder.java new file mode 100644 index 00000000..39a8526a --- /dev/null +++ b/api/src/main/java/io/cloudevents/v1/CloudEventBuilder.java @@ -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 implements + EventBuilder { + + 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 extensions = new HashSet<>(); + + private static Validator getValidator() { + if(null== VALIDATOR) { + VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); + } + return VALIDATOR; + } + + /** + * Gets a brand new builder instance + * @param The 'data' type + */ + public static CloudEventBuilder builder() { + return new CloudEventBuilder(); + } + + /** + * Builder with base event to copy attributes + * @param The 'data' type + * @param base The base event to copy attributes + */ + public static CloudEventBuilder builder( + CloudEvent base) { + Objects.requireNonNull(base); + + CloudEventBuilder 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 build(T data, + AttributesImpl attributes, + Collection extensions) { + + return null; + } + + /** + * + * @return An new {@link CloudEvent} immutable instance + * @throws IllegalStateException When there are specification constraints + * violations + */ + public CloudEventImpl build() { + + AttributesImpl attributes = new AttributesImpl(id, source, SPEC_VERSION, type, + datacontenttype, dataschema, subject, time); + + CloudEventImpl cloudEvent = + new CloudEventImpl(attributes, data, extensions); + + Set> 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 withId(String id) { + this.id = id; + return this; + } + + public CloudEventBuilder withSource(URI source) { + this.source = source; + return this; + } + + public CloudEventBuilder withType(String type) { + this.type = type; + return this; + } + + public CloudEventBuilder withDataschema(URI dataschema) { + this.dataschema = dataschema; + return this; + } + + public CloudEventBuilder withDatacontenttype( + String datacontenttype) { + this.datacontenttype = datacontenttype; + return this; + } + + public CloudEventBuilder withSubject( + String subject) { + this.subject = subject; + return this; + } + + public CloudEventBuilder withTime(ZonedDateTime time) { + this.time = time; + return this; + } + + public CloudEventBuilder withData(T data) { + this.data = data; + return this; + } + + public CloudEventBuilder withExtension(ExtensionFormat extension) { + this.extensions.add(extension); + return this; + } +} diff --git a/api/src/main/java/io/cloudevents/v1/CloudEventImpl.java b/api/src/main/java/io/cloudevents/v1/CloudEventImpl.java new file mode 100644 index 00000000..8e5d2d3a --- /dev/null +++ b/api/src/main/java/io/cloudevents/v1/CloudEventImpl.java @@ -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 implements CloudEvent { + + @JsonIgnore + @NotNull + private final AttributesImpl attributes; + + private final T data; + + @NotNull + private final Map extensions; + + private final Set extensionsFormats; + + CloudEventImpl(AttributesImpl attributes, T data, + Set 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 getExtensionsFormats() { + return extensionsFormats; + } + + @JsonUnwrapped + @Override + public AttributesImpl getAttributes() { + return attributes; + } + + @Override + public Optional getData() { + return Optional.ofNullable(data); + } + + @JsonAnyGetter + @Override + public Map 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 CloudEventImpl 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.builder() + .withId(id) + .withSource(source) + .withType(type) + .withTime(time) + .withDataschema(dataschema) + .withDatacontenttype(datacontenttype) + .withData(data) + .withSubject(subject) + .build(); + } +} diff --git a/api/src/main/java/io/cloudevents/v1/ContextAttributes.java b/api/src/main/java/io/cloudevents/v1/ContextAttributes.java new file mode 100644 index 00000000..1f2a3d64 --- /dev/null +++ b/api/src/main/java/io/cloudevents/v1/ContextAttributes.java @@ -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 VALUES = + Arrays.asList(ContextAttributes.values()) + .stream() + .map(Enum::name) + .collect(Collectors.toList()); +} diff --git a/api/src/test/java/io/cloudevents/v1/AccessorTest.java b/api/src/test/java/io/cloudevents/v1/AccessorTest.java new file mode 100644 index 00000000..7a07f623 --- /dev/null +++ b/api/src/test/java/io/cloudevents/v1/AccessorTest.java @@ -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 ce = + CloudEventBuilder.builder() + .withId("x10") + .withSource(URI.create("/source")) + .withType("event-type") + .withDataschema(URI.create("/schema")) + .withDatacontenttype("text/plain") + .withData("my-data") + .build(); + + // act + Collection 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 ce = + CloudEventBuilder.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 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 ce = + CloudEventBuilder.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 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()); + } +} diff --git a/api/src/test/java/io/cloudevents/v1/CloudEventBuilderTest.java b/api/src/test/java/io/cloudevents/v1/CloudEventBuilderTest.java new file mode 100644 index 00000000..f7b832ef --- /dev/null +++ b/api/src/test/java/io/cloudevents/v1/CloudEventBuilderTest.java @@ -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.builder() + .withId("id") + .withType("type") + .withSource(URI.create("/source")) + .withSubject("") + .build(); + } + + @Test + public void should_have_subject() { + // act + CloudEvent ce = + CloudEventBuilder.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 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 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); + } +}