Specification Compliant handling of numeric context attributes (#358)
* - Added tests case to verify expected handling of numeric context attributes - Updated serializer. Signed-off-by: Day, Jeremy(jday) <jday@paypal.com> * - Added @deprecated marker for CloudEventContextWriter.set(name, Number) - Added use of new method for JSON serializer. Cleanup of deprecated implementations can occur independantly. Signed-off-by: Day, Jeremy(jday) <jday@paypal.com> * Addressed Review Comments - Now throws exception when non specification compliant numeric attribute values are received during deserialization. - Added test cases to verify deserialization exceptions. Signed-off-by: Day, Jeremy(jday) <jday@paypal.com> * Address Review Comments Signed-off-by: Day, Jeremy(jday) <jday@paypal.com> * Address Review Comment Signed-off-by: Day, Jeremy(jday) <jday@paypal.com>
This commit is contained in:
parent
13f8b56618
commit
5e3bfc890f
|
@ -83,11 +83,29 @@ public interface CloudEventContextWriter {
|
|||
* @return self
|
||||
* @throws CloudEventRWException if anything goes wrong while writing this extension.
|
||||
* @throws IllegalArgumentException if you're trying to set the specversion attribute.
|
||||
*
|
||||
* @deprecated CloudEvent specification only permits {@link Integer} type as a
|
||||
* numeric value.
|
||||
*/
|
||||
default CloudEventContextWriter withContextAttribute(String name, Number value) throws CloudEventRWException {
|
||||
return withContextAttribute(name, value.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set attribute with type {@link Integer}.
|
||||
* This setter should not be invoked for specversion, because the writer should
|
||||
* already know the specversion or because it doesn't need it to correctly write the value.
|
||||
*
|
||||
* @param name name of the attribute
|
||||
* @param value value of the attribute
|
||||
* @return self
|
||||
* @throws CloudEventRWException if anything goes wrong while writing this extension.
|
||||
* @throws IllegalArgumentException if you're trying to set the specversion attribute.
|
||||
*/
|
||||
default CloudEventContextWriter withContextAttribute(String name, Integer value) throws CloudEventRWException {
|
||||
return withContextAttribute(name, value.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set attribute with type {@link Boolean} attribute.
|
||||
* This setter should not be invoked for specversion, because the writer should
|
||||
|
|
|
@ -137,6 +137,17 @@ public class CloudEventRWException extends RuntimeException {
|
|||
);
|
||||
}
|
||||
|
||||
public static CloudEventRWException newInvalidAttributeType(String attributeName,Object value){
|
||||
return new CloudEventRWException(
|
||||
CloudEventRWExceptionKind.INVALID_ATTRIBUTE_TYPE,
|
||||
"Invalid attribute/extension type for \""
|
||||
+ attributeName
|
||||
+ "\": Type" + value.getClass().getCanonicalName()
|
||||
+ ". Value: " + value
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param attributeName the invalid attribute name
|
||||
* @param value the value of the attribute
|
||||
|
|
|
@ -246,6 +246,26 @@ public final class CloudEventBuilder extends BaseCloudEventBuilder<CloudEventBui
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloudEventContextWriter withContextAttribute(String name, Integer value) throws CloudEventRWException
|
||||
{
|
||||
requireValidAttributeWrite(name);
|
||||
switch (name) {
|
||||
case TIME:
|
||||
case SCHEMAURL:
|
||||
case ID:
|
||||
case TYPE:
|
||||
case DATACONTENTTYPE:
|
||||
case DATACONTENTENCODING:
|
||||
case SUBJECT:
|
||||
case SOURCE:
|
||||
throw CloudEventRWException.newInvalidAttributeType(name, Integer.class);
|
||||
default:
|
||||
withExtension(name, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloudEventContextWriter withContextAttribute(String name, Boolean value) throws CloudEventRWException {
|
||||
requireValidAttributeWrite(name);
|
||||
|
|
|
@ -238,6 +238,25 @@ public final class CloudEventBuilder extends BaseCloudEventBuilder<CloudEventBui
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloudEventContextWriter withContextAttribute(String name, Integer value) throws CloudEventRWException
|
||||
{
|
||||
requireValidAttributeWrite(name);
|
||||
switch (name) {
|
||||
case TIME:
|
||||
case DATASCHEMA:
|
||||
case ID:
|
||||
case TYPE:
|
||||
case DATACONTENTTYPE:
|
||||
case SUBJECT:
|
||||
case SOURCE:
|
||||
throw CloudEventRWException.newInvalidAttributeType(name, Integer.class);
|
||||
default:
|
||||
withExtension(name, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloudEventContextWriter withContextAttribute(String name, Boolean value) throws CloudEventRWException {
|
||||
requireValidAttributeWrite(name);
|
||||
|
|
|
@ -21,6 +21,7 @@ import io.cloudevents.CloudEvent;
|
|||
import io.cloudevents.core.builder.CloudEventBuilder;
|
||||
import io.cloudevents.types.Time;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Objects;
|
||||
|
@ -116,6 +117,16 @@ public class Data {
|
|||
.withExtension("binary", BINARY_VALUE)
|
||||
.build();
|
||||
|
||||
public static final CloudEvent V1_WITH_NUMERIC_EXT = CloudEventBuilder.v1()
|
||||
.withId(ID)
|
||||
.withType(TYPE)
|
||||
.withSource(SOURCE)
|
||||
.withExtension("integer", 42)
|
||||
.withExtension("decimal", new BigDecimal("42.42"))
|
||||
.withExtension("float", 4.2f)
|
||||
.withExtension("long", new Long(4200))
|
||||
.build();
|
||||
|
||||
public static final CloudEvent V03_MIN = CloudEventBuilder.v03(V1_MIN).build();
|
||||
public static final CloudEvent V03_WITH_JSON_DATA = CloudEventBuilder.v03(V1_WITH_JSON_DATA).build();
|
||||
public static final CloudEvent V03_WITH_JSON_DATA_WITH_EXT = CloudEventBuilder.v03(V1_WITH_JSON_DATA_WITH_EXT).build();
|
||||
|
|
|
@ -134,7 +134,17 @@ class CloudEventDeserializer extends StdDeserializer<CloudEvent> {
|
|||
writer.withContextAttribute(extensionName, extensionValue.booleanValue());
|
||||
break;
|
||||
case NUMBER:
|
||||
writer.withContextAttribute(extensionName, extensionValue.numberValue());
|
||||
|
||||
final Number numericValue = extensionValue.numberValue();
|
||||
|
||||
// Only 'Int' values are supported by the specification
|
||||
|
||||
if (numericValue instanceof Integer){
|
||||
writer.withContextAttribute(extensionName, (Integer) numericValue);
|
||||
} else{
|
||||
throw CloudEventRWException.newInvalidAttributeType(extensionName,numericValue);
|
||||
}
|
||||
|
||||
break;
|
||||
case STRING:
|
||||
writer.withContextAttribute(extensionName, extensionValue.textValue());
|
||||
|
|
|
@ -65,10 +65,23 @@ class CloudEventSerializer extends StdSerializer<CloudEvent> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CloudEventContextWriter withContextAttribute(String name, Number value) throws CloudEventRWException {
|
||||
public CloudEventContextWriter withContextAttribute(String name, Number value) throws CloudEventRWException
|
||||
{
|
||||
// Only Integer types are supported by the specification
|
||||
if (value instanceof Integer) {
|
||||
this.withContextAttribute(name, (Integer) value);
|
||||
} else {
|
||||
// Default to string representation for other numeric values
|
||||
this.withContextAttribute(name, value.toString());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloudEventContextWriter withContextAttribute(String name, Integer value) throws CloudEventRWException
|
||||
{
|
||||
try {
|
||||
gen.writeFieldName(name);
|
||||
provider.findValueSerializer(value.getClass()).serialize(value, gen, provider);
|
||||
gen.writeNumberField(name, value.intValue());
|
||||
return this;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
|
@ -24,14 +24,17 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
|||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.SpecVersion;
|
||||
import io.cloudevents.core.builder.CloudEventBuilder;
|
||||
import io.cloudevents.core.format.EventDeserializationException;
|
||||
import io.cloudevents.core.provider.EventFormatProvider;
|
||||
import io.cloudevents.rw.CloudEventRWException;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
|
@ -40,8 +43,7 @@ import java.util.Objects;
|
|||
import java.util.stream.Stream;
|
||||
|
||||
import static io.cloudevents.core.test.Data.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class JsonFormatTest {
|
||||
|
||||
|
@ -120,6 +122,21 @@ class JsonFormatTest {
|
|||
.hasMessageContaining(CloudEventRWException.newInvalidSpecVersion("9000.1").getMessage());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("badJsonContent")
|
||||
/**
|
||||
* JSON content that should fail deserialization
|
||||
* as it represents content that is not CE
|
||||
* specification compliant.
|
||||
*/
|
||||
void verifyDeserializeError(String inputFile){
|
||||
|
||||
byte[] input = loadFile(inputFile);
|
||||
|
||||
assertThatExceptionOfType(EventDeserializationException.class).isThrownBy(() -> getFormat().deserialize(input));
|
||||
|
||||
}
|
||||
|
||||
public static Stream<Arguments> serializeTestArgumentsDefault() {
|
||||
return Stream.of(
|
||||
Arguments.of(V03_MIN, "v03/min.json"),
|
||||
|
@ -133,7 +150,8 @@ class JsonFormatTest {
|
|||
Arguments.of(V1_WITH_JSON_DATA_WITH_EXT, "v1/json_data_with_ext.json"),
|
||||
Arguments.of(V1_WITH_XML_DATA, "v1/base64_xml_data.json"),
|
||||
Arguments.of(V1_WITH_TEXT_DATA, "v1/base64_text_data.json"),
|
||||
Arguments.of(V1_WITH_BINARY_EXT, "v1/binary_attr.json")
|
||||
Arguments.of(V1_WITH_BINARY_EXT, "v1/binary_attr.json"),
|
||||
Arguments.of(V1_WITH_NUMERIC_EXT,"v1/numeric_ext.json")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -200,6 +218,15 @@ class JsonFormatTest {
|
|||
);
|
||||
}
|
||||
|
||||
public static Stream<String> badJsonContent() {
|
||||
return Stream.of(
|
||||
"v03/fail_numeric_decimal.json",
|
||||
"v03/fail_numeric_long.json",
|
||||
"v1/fail_numeric_decimal.json",
|
||||
"v1/fail_numeric_long.json"
|
||||
);
|
||||
}
|
||||
|
||||
private JsonFormat getFormat() {
|
||||
return (JsonFormat) EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"specversion": "1.0",
|
||||
"id": "1",
|
||||
"type": "mock.test",
|
||||
"source": "http://localhost/source",
|
||||
"decimal": 42.42
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"specversion": "1.0",
|
||||
"id": "1",
|
||||
"type": "mock.test",
|
||||
"source": "http://localhost/source",
|
||||
"long": 4247483647
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"specversion": "1.0",
|
||||
"id": "1",
|
||||
"type": "mock.test",
|
||||
"source": "http://localhost/source",
|
||||
"decimal": 42.42
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"specversion": "1.0",
|
||||
"id": "1",
|
||||
"type": "mock.test",
|
||||
"source": "http://localhost/source",
|
||||
"long": 4247483647
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"specversion": "1.0",
|
||||
"id": "1",
|
||||
"type": "mock.test",
|
||||
"source": "http://localhost/source",
|
||||
"integer": 42,
|
||||
"decimal": "42.42",
|
||||
"float": "4.2",
|
||||
"long": "4200"
|
||||
}
|
Loading…
Reference in New Issue