From c88ba5484d49c39598a72ab40a5b84fceb83651e Mon Sep 17 00:00:00 2001 From: Vladimir Orany Date: Thu, 3 Oct 2024 10:21:33 +0200 Subject: [PATCH] DynamoDb Converters v2 Improvements (#263) * DynamoDb Converters v2 Improvements - added `@ConvertedJson` to mirror `DynamoDBTypeConvertedJson` functionality - always present `EmptySafeStringSetConverter` to allow having sets initialized * DynamoDb Converters v2 Improvements - added `@ConvertedJson` to mirror `DynamoDBTypeConvertedJson` functionality - always present `EmptySafeStringSetConverter` to allow having sets initialized --- docs/guide/src/docs/asciidoc/dynamodb.adoc | 2 +- .../micronaut-amazon-awssdk-dynamodb.gradle | 4 + .../dynamodb/annotation/ConvertedJson.java | 32 +++++++ .../ConvertedJsonAttributeConverter.java | 80 +++++++++++++++++ .../convert/EmptySafeStringSetConverter.java | 57 ++++++++++++ .../LegacyAttributeConverterProvider.java | 3 +- .../schema/BeanIntrospectionTableSchema.java | 5 +- .../DefaultDynamoDBServiceSpec.groovy | 2 +- .../awssdk/dynamodb/DynamoDBEntity.java | 28 +++++- .../awssdk/dynamodb/DynamoDBLocalTest.java | 6 ++ .../amazon/awssdk/dynamodb/Options.java | 66 ++++++++++++++ ...mple.java => CompressedEntityExample.java} | 6 +- .../CompressedStringConverterSpec.groovy | 23 ++--- ...ConvertedJsonAttributeConverterSpec.groovy | 88 +++++++++++++++++++ .../convert/ConvertedJsonEntityExample.java | 55 ++++++++++++ .../dynamodb/convert/RawDataUtil.groovy | 33 +++++++ .../BeanIntrospectionTableSchemaSpec.groovy | 2 +- 17 files changed, 464 insertions(+), 28 deletions(-) create mode 100644 subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/annotation/ConvertedJson.java create mode 100644 subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonAttributeConverter.java create mode 100644 subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/EmptySafeStringSetConverter.java create mode 100644 subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/Options.java rename subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/{EntityExample.java => CompressedEntityExample.java} (90%) create mode 100644 subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonAttributeConverterSpec.groovy create mode 100644 subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonEntityExample.java create mode 100644 subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/RawDataUtil.groovy diff --git a/docs/guide/src/docs/asciidoc/dynamodb.adoc b/docs/guide/src/docs/asciidoc/dynamodb.adoc index 95dff7e2e..4ccd0ea23 100644 --- a/docs/guide/src/docs/asciidoc/dynamodb.adoc +++ b/docs/guide/src/docs/asciidoc/dynamodb.adoc @@ -61,7 +61,7 @@ The entity class is a class which instances represent the items in DynamoDB. For AWS SDK v2 you don't need to use the https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/package-summary.html[native annotations] but you fill their counterparts in `com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation` package. The only requirements is that -the class needs to be annotated either with `@Introspected` or `@DynamoDbBean`. +the class needs to be annotated either with `@Introspected` or `@DynamoDbBean`. There is a replacement for `@DynamoDBTypeConvertedJson` annotation as well - you can use `@ConvertedJson` annotation instead. diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/micronaut-amazon-awssdk-dynamodb.gradle b/subprojects/micronaut-amazon-awssdk-dynamodb/micronaut-amazon-awssdk-dynamodb.gradle index ba8ae5814..d89e8d3e8 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/micronaut-amazon-awssdk-dynamodb.gradle +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/micronaut-amazon-awssdk-dynamodb.gradle @@ -23,10 +23,14 @@ dependencies { implementation "space.jasan:groovy-closure-support:$closureSupportVersion" implementation "io.projectreactor:reactor-core:$projectReactorVersion" + // required by the com.agorapulse.micronaut.amazon.awssdk.dynamodb.convert.ConvertedToJsonAttributeConverter + compileOnly 'io.micronaut:micronaut-jackson-databind' + testAnnotationProcessor project(':micronaut-amazon-awssdk-dynamodb-annotation-processor') testImplementation project(':micronaut-amazon-awssdk-dynamodb-annotation-processor') testImplementation project(':micronaut-amazon-awssdk-integration-testing') testImplementation 'io.micronaut.rxjava2:micronaut-rxjava2' + testImplementation 'io.micronaut:micronaut-jackson-databind' } if (project.findProperty('test.aws.dynamodb.v2') == 'async') { diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/annotation/ConvertedJson.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/annotation/ConvertedJson.java new file mode 100644 index 000000000..2d7a60b7d --- /dev/null +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/annotation/ConvertedJson.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2018-2024 Agorapulse. + * + * 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 + * + * https://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 com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation; + +import java.lang.annotation.*; + +/** + * Specifies that the property is persisted as JSON string. Requires micronaut-jackson-databind to be on the classpath. + * + */ +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD}) +public @interface ConvertedJson { + +} diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonAttributeConverter.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonAttributeConverter.java new file mode 100644 index 000000000..15030f6b5 --- /dev/null +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonAttributeConverter.java @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2018-2024 Agorapulse. + * + * 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 + * + * https://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 com.agorapulse.micronaut.amazon.awssdk.dynamodb.convert; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import io.micronaut.jackson.ObjectMapperFactory; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.io.IOException; + +/** + * Converter which converts objects to JSON strings. + * @param the type of the object + */ +public class ConvertedJsonAttributeConverter implements AttributeConverter { + + private static final ObjectWriter OBJECT_WRITER; + private static final ObjectReader OBJECT_READER; + + static { + ObjectMapper mapper = new ObjectMapperFactory().objectMapper(null, null); + OBJECT_READER = mapper.reader(); + OBJECT_WRITER = mapper.writer(); + } + + private final Class type; + + public ConvertedJsonAttributeConverter(Class type) { + this.type = type; + } + + @Override + public AttributeValue transformFrom(T input) { + try { + return AttributeValue.fromS(OBJECT_WRITER.writeValueAsString(input)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Cannot write value as JSON: " + input, e); + } + } + + @Override + public T transformTo(AttributeValue input) { + try { + return OBJECT_READER.readValue(input.s(), type); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot read value: " + input.s(), e); + } + } + + @Override + public EnhancedType type() { + return EnhancedType.of(type); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.S; + } + } diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/EmptySafeStringSetConverter.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/EmptySafeStringSetConverter.java new file mode 100644 index 000000000..3b88bac36 --- /dev/null +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/EmptySafeStringSetConverter.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2018-2024 Agorapulse. + * + * 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 + * + * https://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 com.agorapulse.micronaut.amazon.awssdk.dynamodb.convert; + +import io.micronaut.core.util.CollectionUtils; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.HashSet; +import java.util.Set; + +/** + * Set string datatype in dynamo does not allow empty values. + * Without this custom converter if an empty set is present in entity an error will occur : 'dynamodb an string set may not be empty' + * @see ... + */ +public class EmptySafeStringSetConverter implements AttributeConverter> { + @Override + public AttributeValue transformFrom(Set set) { + return CollectionUtils.isEmpty(set) + ? AttributeValues.nullAttributeValue() + : AttributeValue.fromSs(set.stream().toList()); + } + + @Override + public Set transformTo(AttributeValue rawValue) { + return new HashSet<>(rawValue.ss()); + } + + @Override + public EnhancedType> type() { + return EnhancedType.setOf(String.class); + } + + @Override + public AttributeValueType attributeValueType() { + return AttributeValueType.SS; + } +} diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/LegacyAttributeConverterProvider.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/LegacyAttributeConverterProvider.java index 6d3fc9986..359ff051e 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/LegacyAttributeConverterProvider.java +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/LegacyAttributeConverterProvider.java @@ -33,7 +33,8 @@ public class LegacyAttributeConverterProvider implements AttributeConverterProvider { private final List> customConverters = Arrays.asList( - new DateToStringAttributeConverter() + new DateToStringAttributeConverter(), + new EmptySafeStringSetConverter() ); private final Map, AttributeConverter> customConvertersMap; diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/schema/BeanIntrospectionTableSchema.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/schema/BeanIntrospectionTableSchema.java index 37e5d87c3..ca4e111cc 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/schema/BeanIntrospectionTableSchema.java +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/main/java/com/agorapulse/micronaut/amazon/awssdk/dynamodb/schema/BeanIntrospectionTableSchema.java @@ -18,6 +18,7 @@ package com.agorapulse.micronaut.amazon.awssdk.dynamodb.schema; import com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation.*; +import com.agorapulse.micronaut.amazon.awssdk.dynamodb.convert.ConvertedJsonAttributeConverter; import com.agorapulse.micronaut.amazon.awssdk.dynamodb.convert.LegacyAttributeConverterProvider; import io.micronaut.context.BeanContext; import io.micronaut.core.annotation.AnnotationMetadataProvider; @@ -336,7 +337,9 @@ private static Optional> createAttributeConverterFr ) { return findAnnotation(propertyDescriptor, DynamoDbConvertedBy.class, ConvertedBy.class) .flatMap(AnnotationValue::classValue) - .map(clazz -> (AttributeConverter

) fromContextOrNew(clazz, beanContext).get()); + .map(clazz -> (AttributeConverter

) fromContextOrNew(clazz, beanContext).get()) + .or(() -> findAnnotation(propertyDescriptor, ConvertedJson.class) + .map(anno -> (AttributeConverter

) new ConvertedJsonAttributeConverter<>(propertyDescriptor.getType()))); } /** diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DefaultDynamoDBServiceSpec.groovy b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DefaultDynamoDBServiceSpec.groovy index b27ea7954..0fbf2b98b 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DefaultDynamoDBServiceSpec.groovy +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DefaultDynamoDBServiceSpec.groovy @@ -89,7 +89,7 @@ class DefaultDynamoDBServiceSpec extends Specification { unknownMethodsService.delete('1', '1', '1') then: IllegalArgumentException e3 = thrown(IllegalArgumentException) - e3.message == '''Unknown property somethingElse for DynamoDBEntity{parentId='null', id='null', rangeIndex='null', date=null, number=0, mapProperty={}}''' + e3.message == '''Unknown property somethingElse for DynamoDBEntity{parentId='null', id='null', rangeIndex='null', date=null, number=0, mapProperty={}, stringSetProperty=[]}''' when: unknownMethodsService.get('1', '1', '1') diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBEntity.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBEntity.java index ac0e10102..f8bddbc6d 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBEntity.java +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBEntity.java @@ -17,6 +17,7 @@ */ package com.agorapulse.micronaut.amazon.awssdk.dynamodb; +import com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation.ConvertedJson; import com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation.PartitionKey; import com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation.Projection; import com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation.SecondaryPartitionKey; @@ -26,10 +27,12 @@ import software.amazon.awssdk.services.dynamodb.model.ProjectionType; import java.util.Date; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; @Introspected // <1> public class DynamoDBEntity implements PlaybookAware { @@ -45,6 +48,8 @@ public class DynamoDBEntity implements PlaybookAware { private Integer number = 0; private Map> mapProperty = new LinkedHashMap<>(); + private Set stringSetProperty = new HashSet<>(); + private Options options = new Options(); @PartitionKey // <2> public String getParentId() { @@ -105,6 +110,23 @@ public void setMapProperty(Map> mapProperty) { this.mapProperty = mapProperty; } + public Set getStringSetProperty() { + return stringSetProperty; + } + + public void setStringSetProperty(Set stringSetProperty) { + this.stringSetProperty = stringSetProperty; + } + + @ConvertedJson + public Options getOptions() { + return options; + } + + public void setOptions(Options options) { + this.options = options; + } + //CHECKSTYLE:OFF @Override public boolean equals(Object o) { @@ -116,12 +138,13 @@ public boolean equals(Object o) { Objects.equals(rangeIndex, that.rangeIndex) && Objects.equals(date, that.date) && Objects.equals(number, that.number) && - Objects.equals(mapProperty, that.mapProperty); + Objects.equals(mapProperty, that.mapProperty) && + Objects.equals(stringSetProperty, that.stringSetProperty); } @Override public int hashCode() { - return Objects.hash(parentId, id, rangeIndex, date, number, mapProperty); + return Objects.hash(parentId, id, rangeIndex, date, number, mapProperty, stringSetProperty); } @Override @@ -133,6 +156,7 @@ public String toString() { ", date=" + date + ", number=" + number + ", mapProperty=" + mapProperty + + ", stringSetProperty=" + stringSetProperty + '}'; } //CHECKSTYLE:ON diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBLocalTest.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBLocalTest.java index e3de78e4f..5b5927878 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBLocalTest.java +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/DynamoDBLocalTest.java @@ -111,6 +111,12 @@ private DynamoDBEntity createEntity(String parentId, String id, String rangeInde entity.setId(id); entity.setRangeIndex(rangeIndex); entity.setDate(date); + + Options options = new Options(); + options.setOne("one"); + options.setTwo("two"); + entity.setOptions(options); + return entity; } } diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/Options.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/Options.java new file mode 100644 index 000000000..d234a2b0f --- /dev/null +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/Options.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2018-2024 Agorapulse. + * + * 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 + * + * https://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 com.agorapulse.micronaut.amazon.awssdk.dynamodb; + +import io.micronaut.core.annotation.Introspected; + +import java.util.Objects; + +@Introspected +public class Options { + + private String one; + private String two; + + public String getTwo() { + return two; + } + + public void setTwo(String two) { + this.two = two; + } + + public String getOne() { + return one; + } + + public void setOne(String one) { + this.one = one; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + Options options = (Options) o; + + return Objects.equals(one, options.one) && Objects.equals(two, options.two); + } + + @Override + public int hashCode() { + return Objects.hash(one, two); + } + +} diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/EntityExample.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/CompressedEntityExample.java similarity index 90% rename from subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/EntityExample.java rename to subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/CompressedEntityExample.java index 74fa2998c..bd88ad54a 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/EntityExample.java +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/CompressedEntityExample.java @@ -22,14 +22,14 @@ import io.micronaut.core.annotation.Introspected; @Introspected -public class EntityExample { +public class CompressedEntityExample { private String id; private String data; - public EntityExample() {} + public CompressedEntityExample() {} - public EntityExample(String id, String data) { + public CompressedEntityExample(String id, String data) { this.id = id; this.data = data; } diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/CompressedStringConverterSpec.groovy b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/CompressedStringConverterSpec.groovy index 65284163f..86e06edf7 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/CompressedStringConverterSpec.groovy +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/CompressedStringConverterSpec.groovy @@ -23,9 +23,6 @@ import groovy.util.logging.Slf4j import io.micronaut.test.extensions.spock.annotation.MicronautTest import software.amazon.awssdk.enhanced.dynamodb.Key import software.amazon.awssdk.services.dynamodb.DynamoDbClient -import software.amazon.awssdk.services.dynamodb.model.AttributeValue -import software.amazon.awssdk.services.dynamodb.model.GetItemRequest -import software.amazon.awssdk.services.dynamodb.model.GetItemResponse import spock.lang.Shared import spock.lang.Specification @@ -37,19 +34,19 @@ class CompressedStringConverterSpec extends Specification { @Inject DynamoDBServiceProvider dynamoDBServiceProvider @Inject DynamoDbClient dynamoDbClient - @Shared DynamoDbService dynamoDbService + @Shared DynamoDbService dynamoDbService void setup() { - dynamoDbService = dynamoDBServiceProvider.findOrCreate(EntityExample) + dynamoDbService = dynamoDBServiceProvider.findOrCreate(CompressedEntityExample) } void 'should persist entity with compressed data field'() { given: String json = this.class.classLoader.getResourceAsStream('example.json').text - EntityExample entity = new EntityExample(UUID.randomUUID().toString(), json) + CompressedEntityExample entity = new CompressedEntityExample(UUID.randomUUID().toString(), json) when: - EntityExample saved = dynamoDbService.save(entity) + CompressedEntityExample saved = dynamoDbService.save(entity) then: saved.id == entity.id @@ -60,7 +57,7 @@ class CompressedStringConverterSpec extends Specification { when: int uncompressedSize = entity.data.length() - int compressedSize = getRawDynamoDbItem(entity.id).item().get('data').b().asByteArray().size() + int compressedSize = RawDataUtil.getRawDynamoDbItem(dynamoDbClient, CompressedEntityExample, entity.id).data.b().asByteArray().size() log.info 'uncompressed size: {} bytes', uncompressedSize log.info 'compressed size: {} bytes', compressedSize @@ -69,14 +66,4 @@ class CompressedStringConverterSpec extends Specification { compressedSize < uncompressedSize } - private GetItemResponse getRawDynamoDbItem(String id) { - GetItemRequest request = GetItemRequest - .builder() - .key([id: AttributeValue.builder().s(id).build()]) - .tableName(EntityExample.simpleName) - .build() as GetItemRequest - - return dynamoDbClient.getItem(request) - } - } diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonAttributeConverterSpec.groovy b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonAttributeConverterSpec.groovy new file mode 100644 index 000000000..ea072a9eb --- /dev/null +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonAttributeConverterSpec.groovy @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2018-2024 Agorapulse. + * + * 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 + * + * https://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 com.agorapulse.micronaut.amazon.awssdk.dynamodb.convert + +import com.agorapulse.micronaut.amazon.awssdk.dynamodb.DynamoDBServiceProvider +import com.agorapulse.micronaut.amazon.awssdk.dynamodb.DynamoDbService +import com.agorapulse.micronaut.amazon.awssdk.dynamodb.Options +import com.fasterxml.jackson.databind.ObjectMapper +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import software.amazon.awssdk.enhanced.dynamodb.Key +import software.amazon.awssdk.services.dynamodb.DynamoDbClient +import spock.lang.Specification + +@MicronautTest +class ConvertedJsonAttributeConverterSpec extends Specification { + + @Inject DynamoDBServiceProvider dynamoDBServiceProvider + @Inject DynamoDbClient dynamoDbClient + @Inject ObjectMapper objectMapper + + DynamoDbService dynamoDbService + + void setup() { + dynamoDbService = dynamoDBServiceProvider.findOrCreate(ConvertedJsonEntityExample) + } + + void 'should persist entity with options stored as JSON'() { + given: + ConvertedJsonEntityExample entity = new ConvertedJsonEntityExample( + UUID.randomUUID().toString(), + new Options(one: '1', two: '2') + ) + when: + ConvertedJsonEntityExample saved = dynamoDbService.save(entity) + then: + saved.id == entity.id + saved.options == entity.options + when: + ConvertedJsonEntityExample loaded = dynamoDbService.get(Key.builder().partitionValue(entity.id).build()) + then: + loaded.id == entity.id + loaded.options == entity.options + + when: + String json = RawDataUtil.getRawDynamoDbItem(dynamoDbClient, ConvertedJsonEntityExample, entity.id).options.s() + Options options = objectMapper.readValue(json, Options) + then: + options == entity.options + } + + void 'should persist entity with null options'() { + given: + ConvertedJsonEntityExample entity = new ConvertedJsonEntityExample( + UUID.randomUUID().toString(), + null + ) + when: + ConvertedJsonEntityExample saved = dynamoDbService.save(entity) + then: + saved.id == entity.id + !saved.options + + when: + ConvertedJsonEntityExample loaded = dynamoDbService.get(Key.builder().partitionValue(entity.id).build()) + then: + loaded.id == entity.id + loaded.options == entity.options + + !RawDataUtil.getRawDynamoDbItem(dynamoDbClient, ConvertedJsonEntityExample, entity.id).options + } + +} diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonEntityExample.java b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonEntityExample.java new file mode 100644 index 000000000..3a28ecc79 --- /dev/null +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/ConvertedJsonEntityExample.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2018-2024 Agorapulse. + * + * 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 + * + * https://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 com.agorapulse.micronaut.amazon.awssdk.dynamodb.convert; + +import com.agorapulse.micronaut.amazon.awssdk.dynamodb.Options; +import com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation.ConvertedJson; +import com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation.PartitionKey; +import io.micronaut.core.annotation.Introspected; + +@Introspected +public class ConvertedJsonEntityExample { + + private String id; + private Options options; + + public ConvertedJsonEntityExample() {} + + public ConvertedJsonEntityExample(String id, Options options) { + this.id = id; + this.options = options; + } + + @PartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @ConvertedJson + public Options getOptions() { + return options; + } + + public void setOptions(Options options) { + this.options = options; + } +} diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/RawDataUtil.groovy b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/RawDataUtil.groovy new file mode 100644 index 000000000..eed5881f6 --- /dev/null +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/convert/RawDataUtil.groovy @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2018-2024 Agorapulse. + * + * 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 + * + * https://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 com.agorapulse.micronaut.amazon.awssdk.dynamodb.convert + +import groovy.transform.CompileStatic +import software.amazon.awssdk.services.dynamodb.DynamoDbClient +import software.amazon.awssdk.services.dynamodb.model.AttributeValue + +@CompileStatic +class RawDataUtil { + + static Map getRawDynamoDbItem(DynamoDbClient client, Class type, String id) { + return client.getItem { builder -> + builder.key([id: AttributeValue.builder().s(id).build()]).tableName(type.simpleName) + }.item() + } + +} diff --git a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/schema/BeanIntrospectionTableSchemaSpec.groovy b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/schema/BeanIntrospectionTableSchemaSpec.groovy index f7188a9ea..438fa6eb5 100644 --- a/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/schema/BeanIntrospectionTableSchemaSpec.groovy +++ b/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy/com/agorapulse/micronaut/amazon/awssdk/dynamodb/schema/BeanIntrospectionTableSchemaSpec.groovy @@ -40,7 +40,7 @@ class BeanIntrospectionTableSchemaSpec extends Specification { when: BeanIntrospectionTableSchema schema = BeanIntrospectionTableSchema.create(DynamoDBEntity, context, cache) then: - schema.attributeNames().size() == 7 + schema.attributeNames().size() == 9 } void 'read table schema for java class with nested beans'() {