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:
Jem Day 2021-03-24 08:58:33 -07:00 committed by GitHub
parent 13f8b56618
commit 5e3bfc890f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 174 additions and 7 deletions

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -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());

View File

@ -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);

View File

@ -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);
}

View File

@ -0,0 +1,7 @@
{
"specversion": "1.0",
"id": "1",
"type": "mock.test",
"source": "http://localhost/source",
"decimal": 42.42
}

View File

@ -0,0 +1,7 @@
{
"specversion": "1.0",
"id": "1",
"type": "mock.test",
"source": "http://localhost/source",
"long": 4247483647
}

View File

@ -0,0 +1,7 @@
{
"specversion": "1.0",
"id": "1",
"type": "mock.test",
"source": "http://localhost/source",
"decimal": 42.42
}

View File

@ -0,0 +1,7 @@
{
"specversion": "1.0",
"id": "1",
"type": "mock.test",
"source": "http://localhost/source",
"long": 4247483647
}

View File

@ -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"
}