diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/util/CrystalWrap.kt b/crystal-map-api/src/main/java/com/schwarz/crystalapi/util/CrystalWrap.kt index a33e3cd4..4494c189 100644 --- a/crystal-map-api/src/main/java/com/schwarz/crystalapi/util/CrystalWrap.kt +++ b/crystal-map-api/src/main/java/com/schwarz/crystalapi/util/CrystalWrap.kt @@ -39,26 +39,32 @@ object CrystalWrap { } } - inline fun getList( + inline fun getList( changes: MutableMap, doc: MutableMap, fieldName: String, - mapper: ((List>?) -> List) + mapper: ((MutableMap?) -> T?) ): List? = (changes[fieldName] ?: doc[fieldName])?.let { value -> catchTypeConversionError(fieldName, value) { - mapper.invoke(value as List>) + (value as List).mapNotNull { + catchTypeConversionError(fieldName, it) { + mapper.invoke(it as MutableMap) + } + } } } - inline fun getList( + inline fun getList( changes: MutableMap, doc: MutableMap, fieldName: String, typeConverter: ITypeConverter ): List? = (changes[fieldName] ?: doc[fieldName])?.let { value -> catchTypeConversionError(fieldName, value) { - ((value as List).map { it as U }).mapNotNull { - typeConverter.read(it) + (value as List).mapNotNull { + catchTypeConversionError(fieldName, it) { + typeConverter.read(it as U) + } } } } @@ -69,7 +75,11 @@ object CrystalWrap { fieldName: String ): List? = (changes[fieldName] ?: doc[fieldName])?.let { value -> catchTypeConversionError(fieldName, value) { - (value as List).map { it as T } + (value as List).mapNotNull { + catchTypeConversionError(fieldName, it) { + it as T + } + } } } @@ -134,12 +144,50 @@ object CrystalWrap { } } - inline fun catchTypeConversionError(fieldName: String, value: Any, task: () -> T): T? = try { + inline fun ensureType( + map: HashMap, + key: String, + typeConverter: ITypeConverter + ) { + val value = map[key] + catchTypeConversionError(key, value) { + if (value != null && value is DomainType) { + val converted = typeConverter.write(value) + converted?.let { map.replace(key, it) } + } + } + } + + inline fun ensureListType( + map: HashMap, + key: String, + typeConverter: ITypeConverter + ) { + val value = map[key] + if (value != null && value is List<*>) { + val converted = value.map { + if (it != null && it is DomainType) { + catchTypeConversionError(key, it) { + typeConverter.write(it) + } + } else { + it + } + } + map.replace(key, converted) + } + } + + inline fun catchTypeConversionError( + fieldName: String, + value: Any?, + task: () -> T + ): T? = try { task() - } catch (cce: ClassCastException) { + } catch (e: Exception) { PersistenceConfig.onTypeConversionError( com.schwarz.crystalapi.TypeConversionErrorWrapper( - cce, + e, fieldName, value, T::class diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/CblDefaultGeneration.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/CblDefaultGeneration.kt index de4fd648..9146fb63 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/CblDefaultGeneration.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/CblDefaultGeneration.kt @@ -1,15 +1,17 @@ package com.schwarz.crystalprocessor.generation.model import com.schwarz.crystalprocessor.model.entity.BaseEntityHolder +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderForEntityGeneration import com.schwarz.crystalprocessor.util.ConversionUtil import com.schwarz.crystalprocessor.util.TypeUtil import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.TypeName object CblDefaultGeneration { - fun addDefaults(holder: BaseEntityHolder, useNullableMap: Boolean): FunSpec { + fun addDefaults(holder: BaseEntityHolder, useNullableMap: Boolean, typeConvertersByConvertedClass: Map): FunSpec { val type = if (useNullableMap) TypeUtil.mutableMapStringAnyNullable() else TypeUtil.mutableMapStringAny() val valueType = @@ -19,23 +21,35 @@ object CblDefaultGeneration { if (useNullableMap) TypeUtil.anyNullable() else TypeUtil.any() val builder = - FunSpec.builder("addDefaults").addModifiers(KModifier.PRIVATE) + FunSpec.builder("addDefaults").addModifiers(KModifier.PRIVATE).addParameter("map", type) + + builder.addStatement("val result = mutableMapOf()") for (fieldHolder in holder.fields.values) { if (fieldHolder.isDefault) { - builder.addStatement( - "this.%N = ${ConversionUtil.convertStringToDesiredFormat( + fieldHolder.crystalWrapSetStatement( + builder, + "result", + typeConvertersByConvertedClass, + ConversionUtil.convertStringToDesiredFormat( fieldHolder.typeMirror, fieldHolder.defaultValue - )}", - fieldHolder.accessorSuffix() + ) ) } } + + builder.addCode( + CodeBlock.builder() + .beginControlFlow("result.forEach") + .beginControlFlow("if(it.value != null)").addStatement("map[it.key] = it.value!!").endControlFlow() + .endControlFlow() + .build() + ) return builder.build() } - fun addAddCall(): CodeBlock { - return CodeBlock.builder().addStatement("addDefaults()").build() + fun addAddCall(nameOfMap: String): CodeBlock { + return CodeBlock.builder().addStatement("addDefaults(%N)", nameOfMap).build() } } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/EnsureTypesGeneration.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/EnsureTypesGeneration.kt index da3f2028..68cca4ff 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/EnsureTypesGeneration.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/EnsureTypesGeneration.kt @@ -19,55 +19,17 @@ object EnsureTypesGeneration { val explicitType = if (useNullableMap) TypeUtil.hashMapStringAnyNullable() else TypeUtil.hashMapStringAny() val type = if (useNullableMap) TypeUtil.mapStringAnyNullable() else TypeUtil.mapStringAny() - val typeConversionReturnType = - if (useNullableMap) TypeUtil.anyNullable() else TypeUtil.any() val ensureTypes = FunSpec.builder("ensureTypes").addParameter("doc", type).returns(type) ensureTypes.addStatement("val %N = %T()", RESULT_VAL_NAME, explicitType) ensureTypes.addStatement("%N.putAll(doc)", RESULT_VAL_NAME) for (field in holder.fields.values) { - if (field.isNonConvertibleClass) { - if (field.isIterable) { - ensureTypes.addStatement( - "%T.getList<%T>(mutableMapOf(), %N, %N)", - CrystalWrap::class, - field.fieldType, - RESULT_VAL_NAME, - field.constantName - ) - } else { - ensureTypes.addStatement( - "%T.get<%T>(mutableMapOf(), %N, %N)", - CrystalWrap::class, - field.fieldType, - RESULT_VAL_NAME, - field.constantName - ) - } - } else if (field.isTypeOfSubEntity) { - if (field.isIterable) { - ensureTypes.addStatement( - "%T.getList(mutableMapOf(), %N, %N, {%T.fromMap(it) ?: emptyList()})", - CrystalWrap::class, - RESULT_VAL_NAME, - field.constantName, - field.subEntityTypeName - ) - } else { - ensureTypes.addStatement( - "%T.get(mutableMapOf(), %N, %N, {%T.fromMap(it)})", - CrystalWrap::class, - RESULT_VAL_NAME, - field.constantName, - field.subEntityTypeName - ) - } - } else { + if (!field.isNonConvertibleClass && !field.isTypeOfSubEntity) { val typeConverterHolder = typeConvertersByConvertedClass.get(field.fieldType)!! if (field.isIterable) { ensureTypes.addStatement( - "%T.getList(mutableMapOf(), %N, %N, %T)", + "%T.ensureListType(%N, %N, %T)", CrystalWrap::class, RESULT_VAL_NAME, field.constantName, @@ -75,7 +37,7 @@ object EnsureTypesGeneration { ) } else { ensureTypes.addStatement( - "%T.get(mutableMapOf(), %N, %N, %T)", + "%T.ensureType(%N, %N, %T)", CrystalWrap::class, RESULT_VAL_NAME, field.constantName, diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/EntityGeneration.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/EntityGeneration.kt index 551037bf..8c7286fc 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/EntityGeneration.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/EntityGeneration.kt @@ -69,7 +69,7 @@ class EntityGeneration { .addSuperinterface(MandatoryCheck::class) .addProperty(holder.dbNameProperty()) .addFunction(EnsureTypesGeneration.ensureTypes(holder, false, typeConvertersByConvertedClass)) - .addFunction(CblDefaultGeneration.addDefaults(holder, false)) + .addFunction(CblDefaultGeneration.addDefaults(holder, false, typeConvertersByConvertedClass)) .addFunction(CblConstantGeneration.addConstants(holder, false)) .addFunction(ValidateMethodGeneration.generate(holder, true)) .addProperty( diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/RebindMethodGeneration.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/RebindMethodGeneration.kt index 120e6fbe..efe03046 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/RebindMethodGeneration.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/RebindMethodGeneration.kt @@ -11,7 +11,7 @@ class RebindMethodGeneration { val type = if (clearMDocChanges) TypeUtil.mapStringAny() else TypeUtil.mapStringAnyNullable() val rebind = FunSpec.builder("rebind").addParameter("doc", type) .addStatement("mDoc = %T()", explicitType) - .addCode(CblDefaultGeneration.addAddCall()) + .addCode(CblDefaultGeneration.addAddCall("mDoc")) .addCode( CodeBlock.builder() .beginControlFlow("if(doc != null)") diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/WrapperGeneration.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/WrapperGeneration.kt index 07a05274..f45aae7f 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/WrapperGeneration.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/WrapperGeneration.kt @@ -23,7 +23,7 @@ class WrapperGeneration { .addSuperinterface(holder.interfaceTypeName) .addSuperinterface(MandatoryCheck::class) .addFunction(EnsureTypesGeneration.ensureTypes(holder, true, typeConvertersByConvertedClass)) - .addFunction(CblDefaultGeneration.addDefaults(holder, true)) + .addFunction(CblDefaultGeneration.addDefaults(holder, true, typeConvertersByConvertedClass)) .addFunction(CblConstantGeneration.addConstants(holder, true)) .addFunction(SetAllMethodGeneration().generate(holder, false)) .addFunction(MapSupportGeneration.toMap(holder)) diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblFieldHolder.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblFieldHolder.kt index f569a280..b8cb6410 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblFieldHolder.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblFieldHolder.kt @@ -90,7 +90,21 @@ class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix deprecated?.addDeprecated(dbField, propertyBuilder) - val mDocPhrase = if (useMDocChanges) "mDocChanges, mDoc" else "mDoc, mutableMapOf()" + crystalWrapGetStatement(getter, if (useMDocChanges) "mDocChanges, mDoc" else "mDoc, mutableMapOf()", typeConvertersByConvertedClass) + crystalWrapSetStatement(setter, if (useMDocChanges) "mDocChanges" else "mDoc", typeConvertersByConvertedClass, "value") + + if (comment.isNotEmpty()) { + propertyBuilder.addKdoc(KDocGeneration.generate(comment)) + } + + return propertyBuilder.setter(setter.build()).getter(getter.build()).build() + } + + fun crystalWrapGetStatement( + getter: FunSpec.Builder, + mDocPhrase: String, + typeConvertersByConvertedClass: Map + ) { if (isNonConvertibleClass) { if (isIterable) { getter.addStatement( @@ -101,12 +115,6 @@ class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix fieldType, constantName ) - setter.addStatement( - "%T.setList(%N, %N, value)", - CrystalWrap::class, - if (useMDocChanges) "mDocChanges" else "mDoc", - constantName - ) } else { getter.addStatement( "return %T.get<%T>($mDocPhrase, %N)".forceCastIfMandatory( @@ -116,17 +124,11 @@ class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix fieldType, constantName ) - setter.addStatement( - "%T.set(%N, %N, value)", - CrystalWrap::class, - if (useMDocChanges) "mDocChanges" else "mDoc", - constantName - ) } } else if (isTypeOfSubEntity) { if (isIterable) { getter.addStatement( - "return %T.getList<%T>($mDocPhrase, %N, {%T.fromMap(it) ?: emptyList()})".forceCastIfMandatory( + "return %T.getList<%T>($mDocPhrase, %N, {%T.fromMap(it)})".forceCastIfMandatory( mandatory ), CrystalWrap::class, @@ -134,13 +136,6 @@ class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix constantName, subEntityTypeName ) - setter.addStatement( - "%T.setList(%N, %N, value, {%T.toMap(it)})", - CrystalWrap::class, - if (useMDocChanges) "mDocChanges" else "mDoc", - constantName, - subEntityTypeName - ) } else { getter.addStatement( "return %T.get<%T>($mDocPhrase, %N, {%T.fromMap(it)})".forceCastIfMandatory( @@ -151,13 +146,6 @@ class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix constantName, subEntityTypeName ) - setter.addStatement( - "%T.set(%N, %N, value, {%T.toMap(it)})", - CrystalWrap::class, - if (useMDocChanges) "mDocChanges" else "mDoc", - constantName, - subEntityTypeName - ) } } else { val typeConverterHolder = @@ -171,14 +159,6 @@ class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix constantName, typeConverterHolder.instanceClassTypeName ) - - setter.addStatement( - "%T.setList(%N, %N, value, %T)", - CrystalWrap::class, - if (useMDocChanges) "mDocChanges" else "mDoc", - constantName, - typeConverterHolder.instanceClassTypeName - ) } else { getter.addStatement( "return %T.get($mDocPhrase, %N, %T)".forceCastIfMandatory( @@ -188,22 +168,77 @@ class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix constantName, typeConverterHolder.instanceClassTypeName ) + } + } + } + fun crystalWrapSetStatement( + setter: FunSpec.Builder, + mDocPhrase: String, + typeConvertersByConvertedClass: Map, + valueName: String + ) { + if (isNonConvertibleClass) { + if (isIterable) { + setter.addStatement( + "%T.setList(%N, %N, %L)", + CrystalWrap::class, + mDocPhrase, + constantName, + valueName + ) + } else { + setter.addStatement( + "%T.set(%N, %N, %L)", + CrystalWrap::class, + mDocPhrase, + constantName, + valueName + ) + } + } else if (isTypeOfSubEntity) { + if (isIterable) { + setter.addStatement( + "%T.setList(%N, %N, %L, {%T.toMap(it)})", + CrystalWrap::class, + mDocPhrase, + constantName, + valueName, + subEntityTypeName + ) + } else { + setter.addStatement( + "%T.set(%N, %N, %L, {%T.toMap(it)})", + CrystalWrap::class, + mDocPhrase, + constantName, + valueName, + subEntityTypeName + ) + } + } else { + val typeConverterHolder = + typeConvertersByConvertedClass.get(fieldType)!! + if (isIterable) { setter.addStatement( - "%T.set(%N, %N, value, %T)", + "%T.setList(%N, %N, %L, %T)", CrystalWrap::class, - if (useMDocChanges) "mDocChanges" else "mDoc", + mDocPhrase, constantName, + valueName, + typeConverterHolder.instanceClassTypeName + ) + } else { + setter.addStatement( + "%T.set(%N, %N, %L, %T)", + CrystalWrap::class, + mDocPhrase, + constantName, + valueName, typeConverterHolder.instanceClassTypeName ) } } - - if (comment.isNotEmpty()) { - propertyBuilder.addKdoc(KDocGeneration.generate(comment)) - } - - return propertyBuilder.setter(setter.build()).getter(getter.build()).build() } override fun builderSetter( @@ -244,18 +279,6 @@ class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix return listOf(fieldAccessorConstant) } - fun evaluateClazzForTypeConversion(): TypeName { - return if (isIterable) { - if (TypeUtil.isMap(fieldType)) { - TypeUtil.string() - } else { - fieldType - } - } else { - TypeUtil.parseMetaType(typeMirror, isIterable, false, subEntitySimpleName) - } - } - private fun String.forceCastIfMandatory(mandatory: Boolean): String { if (mandatory) { return "$this!!" diff --git a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/LocalDateConverter.kt b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/LocalDateConverter.kt new file mode 100644 index 00000000..1907bede --- /dev/null +++ b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/LocalDateConverter.kt @@ -0,0 +1,13 @@ +package com.schwarz.crystaldemo.customtypes + +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.TypeConverter +import java.time.LocalDate + +@TypeConverter +abstract class LocalDateConverter : ITypeConverter { + override fun write(value: LocalDate?): String? = + value?.toString() + + override fun read(value: String?): LocalDate? = value?.let { LocalDate.parse(it) } +} diff --git a/demo/src/main/java/com/schwarz/crystaldemo/entity/Product.kt b/demo/src/main/java/com/schwarz/crystaldemo/entity/Product.kt index 708bdded..8e9f6b08 100644 --- a/demo/src/main/java/com/schwarz/crystaldemo/entity/Product.kt +++ b/demo/src/main/java/com/schwarz/crystaldemo/entity/Product.kt @@ -8,12 +8,12 @@ import com.schwarz.crystalapi.Entity import com.schwarz.crystalapi.Field import com.schwarz.crystalapi.Fields import com.schwarz.crystalapi.MapWrapper -import com.schwarz.crystalapi.SchemaClass import com.schwarz.crystalapi.Reduce import com.schwarz.crystalapi.Reduces +import com.schwarz.crystalapi.SchemaClass import com.schwarz.crystalapi.query.Queries import com.schwarz.crystalapi.query.Query -import java.util.Date +import java.time.LocalDate @Entity(database = "mydb_db") @MapWrapper @@ -39,10 +39,16 @@ import java.util.Date list = true, comment = ["I'm also comfortable with pseudo %2D placeholders"] ), + Field( + name = "top_comment", + type = UserComment::class + ), Field(name = "image", type = Blob::class), Field(name = "identifiers", type = String::class, list = true), Field(name = "category", type = ProductCategory::class), - Field(name = "some_date", type = Date::class) + Field(name = "some_date", type = LocalDate::class), + Field(name = "some_dates", type = LocalDate::class, list = true), + Field(name = "field_with_default", type = String::class, defaultValue = "foobar") ) @Queries( Query(fields = ["type"]), diff --git a/demo/src/test/java/kaufland/com/demo/entity/ProductEntityTest.kt b/demo/src/test/java/kaufland/com/demo/entity/ProductEntityTest.kt index bdfd8c68..81d61a10 100644 --- a/demo/src/test/java/kaufland/com/demo/entity/ProductEntityTest.kt +++ b/demo/src/test/java/kaufland/com/demo/entity/ProductEntityTest.kt @@ -4,30 +4,28 @@ import com.schwarz.crystalapi.PersistenceConfig import com.schwarz.crystalapi.TypeConversionErrorWrapper import com.schwarz.crystaldemo.UnitTestConnector import com.schwarz.crystaldemo.entity.ProductCategory.AMAZING_PRODUCT -import com.schwarz.crystaldemo.logger.TestAppender import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.BeforeClass import org.junit.Test -import org.mockito.internal.matchers.Null -import org.slf4j.LoggerFactory +import java.time.LocalDate +import kotlin.system.measureTimeMillis -private val logger = - LoggerFactory.getLogger(ProductEntityTestConnector::class.java) as ch.qos.logback.classic.Logger +object LastErrorWrapper { + var value: TypeConversionErrorWrapper? = null -private val dataTypeErrorMsg: (String?, String?, String?) -> String - get() = { fieldName, value, `class` -> - "Field $fieldName manipulated: Tried to cast $value into $`class`" + fun clear() { + value = null } +} + +object ErrorProducingObject object ProductEntityTestConnector : UnitTestConnector() { - init { - TestAppender().run { - name = this::class.java.simpleName - logger.addAppender(this) - start() - } - } override fun queryDoc( dbName: String, @@ -39,19 +37,7 @@ object ProductEntityTestConnector : UnitTestConnector() { } override fun invokeOnError(errorWrapper: TypeConversionErrorWrapper) { - if (errorWrapper.exception is ClassCastException) { - logger.error( - dataTypeErrorMsg.invoke( - errorWrapper.fieldName, - if (errorWrapper.value != null) { - errorWrapper.value?.javaClass?.kotlin?.simpleName ?: "" - } else { - Null.NULL.toString().lowercase() - }, - errorWrapper.`class`.simpleName - ) - ) - } + LastErrorWrapper.value = errorWrapper } } @@ -65,6 +51,11 @@ class ProductEntityTest { } } + @Before + fun before() { + LastErrorWrapper.clear() + } + @Test fun `findByTypeAndCategory should use the expected query-params`() { val category = AMAZING_PRODUCT @@ -75,66 +66,415 @@ class ProductEntityTest { assertEquals(ProductEntity.DOC_TYPE, queryParams.type) } - /** - * Can happen if combined db data is changed wilfully. - */ @Test - fun `data type changed at runtime test suppress exception`() { + fun `entity with an invalid type for fields without type conversions should produce error on get`() { val map: MutableMap = mutableMapOf( "name" to 1 ) - ProductEntity.create(map) - assertEquals( - (logger.getAppender(TestAppender::class.java.simpleName) as TestAppender).lastLoggedEvent?.message, - dataTypeErrorMsg.invoke("name", 1::class.simpleName, String::class.simpleName) - ) + + val entity = ProductEntity.create(map) + + assertNull(LastErrorWrapper.value) + assertThrows(Exception::class.java) { entity.name } } - /** - * Can happen if combined db data is changed wilfully. - */ @Test - fun `list data type changed at runtime test suppress exception`() { + fun `entity with an invalid type for list fields without type conversions should produce error on get`() { val map: MutableMap = mutableMapOf( "identifiers" to 1 ) - ProductEntity.create(map) - assertEquals( - (logger.getAppender(TestAppender::class.java.simpleName) as TestAppender).lastLoggedEvent?.message, - dataTypeErrorMsg.invoke("identifiers", 1::class.simpleName, List::class.simpleName) - ) + + val entity = ProductEntity.create(map) + assertNull(LastErrorWrapper.value) + + val result = entity.identifiers + + assertNull(result) + val errorWrapper = LastErrorWrapper.value + assertNotNull(errorWrapper) + assertEquals("identifiers", errorWrapper?.fieldName) + assertEquals(1, errorWrapper?.value) + assertEquals(List::class, errorWrapper?.`class`) } - /** - * Can happen if combined db data is changed wilfully. - */ @Test - fun `list item data type changed at runtime test suppress exception`() { + fun `entity with an invalid list type for list fields without type conversions should produce error on get`() { val map: MutableMap = mutableMapOf( "identifiers" to listOf(1) ) - ProductEntity.create(map) - assertEquals( - (logger.getAppender(TestAppender::class.java.simpleName) as TestAppender).lastLoggedEvent?.message, - dataTypeErrorMsg.invoke("identifiers", "SingletonList", List::class.simpleName) + + val entity = ProductEntity.create(map) + + assertNull(LastErrorWrapper.value) + + val result = entity.identifiers + + assertEquals(emptyList(), result) + val errorWrapper = LastErrorWrapper.value + assertNotNull(errorWrapper) + assertEquals("identifiers", errorWrapper?.fieldName) + assertEquals(1, errorWrapper?.value) + assertEquals(String::class, errorWrapper?.`class`) + } + + @Test + fun `entity with an invalid type for fields that are subentities produce error on get`() { + val map: MutableMap = mutableMapOf( + "top_comment" to 1 + ) + + val entity = ProductEntity.create(map) + + assertNull(LastErrorWrapper.value) + + val result = entity.topComment + + assertNull(result) + val errorWrapper = LastErrorWrapper.value + assertNotNull(errorWrapper) + assertEquals("top_comment", errorWrapper?.fieldName) + assertEquals(1, errorWrapper?.value) + assertEquals(UserCommentWrapper::class, errorWrapper?.`class`) + } + + @Test + fun `entity with an invalid list type for fields that are subentities returns emptyList`() { + val map: MutableMap = mutableMapOf( + "comments" to 1 + ) + + val entity = ProductEntity.create(map) + + assertNull(LastErrorWrapper.value) + + val result = entity.comments + + assertNull(result) + val errorWrapper = LastErrorWrapper.value + assertNotNull(errorWrapper) + assertEquals("comments", errorWrapper?.fieldName) + assertEquals(1, errorWrapper?.value) + assertEquals(List::class, errorWrapper?.`class`) + } + + @Test + fun `entity with an invalid list type for fields that are subentities throws on get`() { + val map: MutableMap = mutableMapOf( + "comments" to listOf(1) + ) + + val entity = ProductEntity.create(map) + + assertNull(LastErrorWrapper.value) + + val result = entity.comments + + assertEquals(emptyList(), result) + val errorWrapper = LastErrorWrapper.value + assertNotNull(errorWrapper) + assertEquals("comments", errorWrapper?.fieldName) + assertEquals(1, errorWrapper?.value) + assertEquals(UserCommentWrapper::class, errorWrapper?.`class`) + } + + @Test + fun `entity with an invalid type for fields with type conversions should produce error on get`() { + val map: MutableMap = mutableMapOf( + "some_date" to true + ) + + val entity = ProductEntity.create(map) + + assertNull(LastErrorWrapper.value) + + val result = entity.someDate + + assertNull(result) + val errorWrapper = LastErrorWrapper.value + assertNotNull(errorWrapper) + assertEquals("some_date", errorWrapper?.fieldName) + assertEquals(true, errorWrapper?.value) + assertEquals(LocalDate::class, errorWrapper?.`class`) + } + + @Test + fun `entity with an invalid list type for fields with type conversions produce error on get`() { + val map: MutableMap = mutableMapOf( + "some_dates" to listOf(true) + ) + + val entity = ProductEntity.create(map) + + assertNull(LastErrorWrapper.value) + + val result = entity.someDates + + assertEquals(emptyList(), result) + val errorWrapper = LastErrorWrapper.value + assertNotNull(errorWrapper) + assertEquals("some_dates", errorWrapper?.fieldName) + assertEquals(true, errorWrapper?.value) + assertEquals(LocalDate::class, errorWrapper?.`class`) + } + + @Test + fun `entity with an invalid value for fields with type conversions should produce error on get`() { + val map: MutableMap = mutableMapOf( + "some_date" to "foobar" + ) + + val entity = ProductEntity.create(map) + + assertNull(LastErrorWrapper.value) + + val result = entity.someDate + + assertNull(result) + val errorWrapper = LastErrorWrapper.value + assertNotNull(errorWrapper) + assertEquals("some_date", errorWrapper?.fieldName) + assertEquals("foobar", errorWrapper?.value) + assertEquals(LocalDate::class, errorWrapper?.`class`) + } + + @Test + fun `entity with a list of invalid values for fields with type conversions produce error on get`() { + val map: MutableMap = mutableMapOf( + "some_dates" to listOf("foobar") + ) + + val entity = ProductEntity.create(map) + + assertNull(LastErrorWrapper.value) + + val result = entity.someDates + + assertEquals(emptyList(), result) + val errorWrapper = LastErrorWrapper.value + assertNotNull(errorWrapper) + assertEquals("some_dates", errorWrapper?.fieldName) + assertEquals("foobar", errorWrapper?.value) + assertEquals(LocalDate::class, errorWrapper?.`class`) + } + + @Test + fun `creating an entity with an invalid deserialized type should produce error on get`() { + val map: MutableMap = mutableMapOf( + "some_date" to ErrorProducingObject + ) + + val entity = ProductEntity.create(map) + + assertNull(LastErrorWrapper.value) + + val result = entity.someDate + + assertNull(result) + val errorWrapper = LastErrorWrapper.value + assertNotNull(errorWrapper) + assertEquals("some_date", errorWrapper?.fieldName) + assertEquals(ErrorProducingObject, errorWrapper?.value) + assertEquals(LocalDate::class, errorWrapper?.`class`) + } + + @Test + fun `entity with a list of invalid deserialized types produce error on get`() { + val map: MutableMap = mutableMapOf( + "some_dates" to listOf(ErrorProducingObject) + ) + + val entity = ProductEntity.create(map) + + assertNull(LastErrorWrapper.value) + + val result = entity.someDates + + assertEquals(emptyList(), result) + val errorWrapper = LastErrorWrapper.value + assertNotNull(errorWrapper) + assertEquals("some_dates", errorWrapper?.fieldName) + assertEquals(ErrorProducingObject, errorWrapper?.value) + assertEquals(LocalDate::class, errorWrapper?.`class`) + } + + @Test + fun `entity with an valid type for fields without type conversions should return`() { + val map: MutableMap = mutableMapOf( + "name" to "Fritz" + ) + + val entity = ProductEntity.create(map) + + assertEquals("Fritz", entity.name) + assertNull(LastErrorWrapper.value) + } + + @Test + fun `entity with an valid type for list fields without type conversions should return`() { + val map: MutableMap = mutableMapOf( + "identifiers" to listOf("1", "2") + ) + + val entity = ProductEntity.create(map) + assertNull(LastErrorWrapper.value) + + assertEquals(listOf("1", "2"), entity.identifiers) + assertNull(LastErrorWrapper.value) + } + + @Test + fun `entity with an valid type for fields that are subentities should return`() { + val map: MutableMap = mutableMapOf( + "top_comment" to mapOf("comment" to "foobar") + ) + + val entity = ProductEntity.create(map) + val result = entity.topComment + + assertNotNull(result) + assertEquals("foobar", result?.comment) + assertNull(LastErrorWrapper.value) + } + + @Test + fun `entity with an valid list type for fields that are subentities returns`() { + val map: MutableMap = mutableMapOf( + "comments" to listOf(mapOf("comment" to "foobar")) ) + + val entity = ProductEntity.create(map) + val result = entity.comments + + assertNotNull(result) + assertEquals("foobar", result?.first()?.comment) + assertNull(LastErrorWrapper.value) } @Test - fun `data type consistent`() { + fun `entity with an valid type for fields with type conversions should return`() { val map: MutableMap = mutableMapOf( - "name" to "1" + "some_date" to "2023-01-01" ) - ProductEntity.create(map) - assertNull((logger.getAppender(TestAppender::class.java.simpleName) as TestAppender).lastLoggedEvent?.message) + + val entity = ProductEntity.create(map) + val result = entity.someDate + + assertNotNull(result) + assertEquals(LocalDate.of(2023, 1, 1), result) + assertNull(LastErrorWrapper.value) } @Test - fun `list data type consistent`() { + fun `entity with an valid list type for fields with type conversions should return`() { val map: MutableMap = mutableMapOf( - "identifiers" to listOf("Foo") + "some_dates" to listOf("2023-01-01") ) - ProductEntity.create(map) - assertNull((logger.getAppender(TestAppender::class.java.simpleName) as TestAppender).lastLoggedEvent?.message) + + val entity = ProductEntity.create(map) + val result = entity.someDates + + assertNotNull(result) + assertEquals(LocalDate.of(2023, 1, 1), result?.first()) + assertNull(LastErrorWrapper.value) + } + + @Test + fun `entity with an valid deserialized type for fields with type conversions should return`() { + val map: MutableMap = mutableMapOf( + "some_date" to LocalDate.of(2023, 1, 1) + ) + + val entity = ProductEntity.create(map) + val result = entity.someDate + + assertNotNull(result) + assertEquals(LocalDate.of(2023, 1, 1), result) + assertNull(LastErrorWrapper.value) + } + + @Test + fun `entity with an valid deserialized list type for fields with type conversions should return`() { + val map: MutableMap = mutableMapOf( + "some_dates" to listOf(LocalDate.of(2023, 1, 1)) + ) + + val entity = ProductEntity.create(map) + val result = entity.someDates + + assertNotNull(result) + assertEquals(LocalDate.of(2023, 1, 1), result?.first()) + assertNull(LastErrorWrapper.value) + } + + @Test + fun `default values and constants should be set`() { + val entity = ProductEntity.create() + + assertEquals(entity.type, "product") + assertEquals(entity.fieldWithDefault, "foobar") + val toMapResult = entity.toMap() + assertEquals("product", toMapResult["type"]) + assertEquals("foobar", toMapResult["field_with_default"]) + } + + @Test + fun `default values should be overwritable through setter`() { + val entity = ProductEntity.create() + + entity.fieldWithDefault = "barfoo" + + assertEquals(entity.fieldWithDefault, "barfoo") + val toMapResult = entity.toMap() + assertEquals("barfoo", toMapResult["field_with_default"]) + } + + @Test + fun `default values should be overwritable from doc`() { + val map = mutableMapOf("field_with_default" to "barfoo") + val entity = ProductEntity.create(map) + + assertEquals(entity.fieldWithDefault, "barfoo") + val toMapResult = entity.toMap() + assertEquals("barfoo", toMapResult["field_with_default"]) + } + + @Test + fun `constant values should not be overwritable from doc`() { + val map = mutableMapOf("type" to "barfoo") + val entity = ProductEntity.create(map) + + assertEquals(entity.type, "product") + val toMapResult = entity.toMap() + assertEquals("product", toMapResult["type"]) + } + + @Test + fun `additional fields in the map should not impact the entity`() { + val map = mutableMapOf("type" to "barfoo", "this_field_is_not_in_the_schema" to "1234") + val entity = ProductEntity.create(map) + + assertEquals(entity.type, "product") + val toMapResult = entity.toMap() + assertEquals("product", toMapResult["type"]) + } + + @Test + fun `creating and reading 1000 docs with 1000 positions should take less than 600ms`() { + val positions = List(1000) { mutableMapOf("comment" to "$it") } + val someDates = List(1000) { LocalDate.now().toString() } + val maps = List(1000) { + mutableMapOf( + "comments" to positions, + "some_dates" to someDates + ) + } + + val duration = measureTimeMillis { + val entities = maps.map { ProductEntity.create(it) } + entities.flatMap { it.comments ?: emptyList() } + entities.flatMap { it.someDates ?: emptyList() } + } + + assertTrue("Expecting time for creating and reading to be < 600ms but was $duration", duration < 600) } } diff --git a/demo/src/test/java/kaufland/com/demo/entity/ProductWrapperTest.kt b/demo/src/test/java/kaufland/com/demo/entity/ProductWrapperTest.kt index cb95792d..25428da8 100644 --- a/demo/src/test/java/kaufland/com/demo/entity/ProductWrapperTest.kt +++ b/demo/src/test/java/kaufland/com/demo/entity/ProductWrapperTest.kt @@ -5,7 +5,7 @@ import com.schwarz.crystaldemo.UnitTestConnector import org.junit.Assert.assertEquals import org.junit.BeforeClass import org.junit.Test -import java.util.Date +import java.time.LocalDate class ProductWrapperTest { companion object { @@ -29,6 +29,7 @@ class ProductWrapperTest { map.remove(ProductWrapper.TYPE) assertEquals( mapOf( + ProductWrapper.FIELD_WITH_DEFAULT to "foobar", ProductWrapper.NAME to "name", ProductWrapper.COMMENTS to listOf(), ProductWrapper.IDENTIFIERS to listOf("1", "2") @@ -50,7 +51,7 @@ class ProductWrapperTest { ) .setCategory(ProductCategory.AMAZING_PRODUCT) .setIdentifiers(listOf("1", "2")) - .setSomeDate(Date()) + .setSomeDate(LocalDate.now()) .exit() val map = product.toMap() as MutableMap