diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/GenerateAccessor.java b/crystal-map-api/src/main/java/com/schwarz/crystalapi/GenerateAccessor.java index 399f3cea..7056a46b 100644 --- a/crystal-map-api/src/main/java/com/schwarz/crystalapi/GenerateAccessor.java +++ b/crystal-map-api/src/main/java/com/schwarz/crystalapi/GenerateAccessor.java @@ -11,4 +11,7 @@ public @interface GenerateAccessor { Class value() default Void.class; + + // We need to explicitly specify this since the information is lost during annotation processing + boolean isNullableSuspendFun() default false; } diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/ITypeConverter.kt b/crystal-map-api/src/main/java/com/schwarz/crystalapi/ITypeConverter.kt new file mode 100644 index 00000000..3b17d9c7 --- /dev/null +++ b/crystal-map-api/src/main/java/com/schwarz/crystalapi/ITypeConverter.kt @@ -0,0 +1,6 @@ +package com.schwarz.crystalapi + +interface ITypeConverter { + fun write(value: KotlinType?): MapType? + fun read(value: MapType?): KotlinType? +} diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/ITypeConverterExporter.kt b/crystal-map-api/src/main/java/com/schwarz/crystalapi/ITypeConverterExporter.kt new file mode 100644 index 00000000..d7bf7adf --- /dev/null +++ b/crystal-map-api/src/main/java/com/schwarz/crystalapi/ITypeConverterExporter.kt @@ -0,0 +1,21 @@ +package com.schwarz.crystalapi + +import kotlin.reflect.KClass + +interface ITypeConverterExporter { + + val typeConverters: Map, ITypeConverter<*, *>> + + val typeConverterImportables: List +} + +data class TypeConverterImportable( + val typeConverterInstanceClassName: ClassNameDefinition, + val domainClassName: ClassNameDefinition, + val mapClassName: ClassNameDefinition +) + +data class ClassNameDefinition( + val packageName: String, + val className: String +) diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/PersistenceConfig.kt b/crystal-map-api/src/main/java/com/schwarz/crystalapi/PersistenceConfig.kt index 1074698f..8a834354 100644 --- a/crystal-map-api/src/main/java/com/schwarz/crystalapi/PersistenceConfig.kt +++ b/crystal-map-api/src/main/java/com/schwarz/crystalapi/PersistenceConfig.kt @@ -1,13 +1,10 @@ package com.schwarz.crystalapi -import kotlin.reflect.KClass - object PersistenceConfig { private var mConnector: Connector? = null private var mSuspendingConnector: SuspendingConnector? = null interface Connector : TypeConversionErrorCallback { - val typeConversions: Map, TypeConversion> fun getDocument( id: String, dbName: String, @@ -40,8 +37,6 @@ object PersistenceConfig { interface SuspendingConnector : TypeConversionErrorCallback { - val typeConversions: Map, TypeConversion> - suspend fun getDocument( id: String, dbName: String, @@ -88,15 +83,6 @@ object PersistenceConfig { return mSuspendingConnector!! } - fun getTypeConversion(type: KClass<*>): TypeConversion? { - if (mConnector != null) { - return connector.typeConversions[type] - } else if (mSuspendingConnector != null) { - return suspendingConnector.typeConversions[type] - } - throw RuntimeException("no database connector configured.. call PersistenceConfig.configure") - } - fun onTypeConversionError(errorWrapper: TypeConversionErrorWrapper) { (mConnector ?: mSuspendingConnector)?.let { it.invokeOnError(errorWrapper) diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConversion.java b/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConversion.java deleted file mode 100644 index 8957adfa..00000000 --- a/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConversion.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.schwarz.crystalapi; - -import org.jetbrains.annotations.Nullable; - -public interface TypeConversion { - - @Nullable - Object write(@Nullable Object value); - - @Nullable - Object read(@Nullable Object value); - -} diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConverter.kt b/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConverter.kt new file mode 100644 index 00000000..85fea6dd --- /dev/null +++ b/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConverter.kt @@ -0,0 +1,5 @@ +package com.schwarz.crystalapi + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class TypeConverter diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConverterExporter.kt b/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConverterExporter.kt new file mode 100644 index 00000000..64b5f67d --- /dev/null +++ b/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConverterExporter.kt @@ -0,0 +1,5 @@ +package com.schwarz.crystalapi + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class TypeConverterExporter diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConverterImporter.kt b/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConverterImporter.kt new file mode 100644 index 00000000..26650abd --- /dev/null +++ b/crystal-map-api/src/main/java/com/schwarz/crystalapi/TypeConverterImporter.kt @@ -0,0 +1,7 @@ +package com.schwarz.crystalapi + +import kotlin.reflect.KClass + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class TypeConverterImporter(val typeConverterExporter: KClass) diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/typeconverters/EnumConverter.kt b/crystal-map-api/src/main/java/com/schwarz/crystalapi/typeconverters/EnumConverter.kt new file mode 100644 index 00000000..44119924 --- /dev/null +++ b/crystal-map-api/src/main/java/com/schwarz/crystalapi/typeconverters/EnumConverter.kt @@ -0,0 +1,18 @@ +package com.schwarz.crystalapi.typeconverters + +import com.schwarz.crystalapi.ITypeConverter +import kotlin.reflect.KClass + +class EnumConverter> (private val enumClass: KClass) : ITypeConverter { + override fun write(value: T?): String? = + value?.toString() + + override fun read(value: String?): T? = + value?.let { + if (it.isBlank()) { + null + } else { + java.lang.Enum.valueOf(enumClass.java, value) + } + } +} 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 cf6efd26..613cb2c3 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 @@ -1,139 +1,150 @@ +@file:Suppress("UNCHECKED_CAST") + package com.schwarz.crystalapi.util +import com.schwarz.crystalapi.ITypeConverter import com.schwarz.crystalapi.PersistenceConfig -import java.lang.Exception -import kotlin.reflect.KClass object CrystalWrap { - inline fun get( + inline fun get( changes: MutableMap, - doc: MutableMap, + doc: MutableMap, fieldName: String, - clazz: KClass<*>, - noinline mapper: ((MutableMap?) -> T?)? = null - ): T? { - return (changes[fieldName] ?: doc[fieldName])?.let { value -> - mapper?.let { - mapper.invoke(value as? MutableMap) - } ?: read(value, fieldName, clazz) - } ?: null + mapper: ((MutableMap?) -> T?) + ): T? = (changes[fieldName] ?: doc[fieldName])?.let { value -> + catchTypeConversionError(fieldName, value) { + mapper.invoke(value as MutableMap) + } } - inline fun validate( - doc: MutableMap, - mandatoryFields: Array - ) { - for (mandatoryField in mandatoryFields) { - doc[mandatoryField]!! + inline fun get( + changes: MutableMap, + doc: MutableMap, + fieldName: String, + typeConverter: ITypeConverter + ): T? = (changes[fieldName] ?: doc[fieldName])?.let { value -> + catchTypeConversionError(fieldName, value) { + typeConverter.read(value as U) + } + } + + inline fun get( + changes: MutableMap, + doc: MutableMap, + fieldName: String + ): T? = (changes[fieldName] ?: doc[fieldName])?.let { value -> + catchTypeConversionError(fieldName, value) { + value as T } } inline fun getList( changes: MutableMap, - doc: MutableMap, + doc: MutableMap, + fieldName: String, + mapper: ((List>?) -> List) + ): List? = (changes[fieldName] ?: doc[fieldName])?.let { value -> + catchTypeConversionError(fieldName, value) { + mapper.invoke(value as List>) + } + } + + inline fun getList( + changes: MutableMap, + doc: MutableMap, fieldName: String, - clazz: KClass<*>, - noinline mapper: ((List>?) -> List)? = null - ): List? { - return (changes[fieldName] ?: doc[fieldName])?.let { value -> - mapper?.let { - mapper.invoke(value as? List>) - } ?: read(value, fieldName, clazz) - } ?: null + typeConverter: ITypeConverter + ): List? = (changes[fieldName] ?: doc[fieldName])?.let { value -> + catchTypeConversionError(fieldName, value) { + ((value as List).map { it as U }).mapNotNull { + typeConverter.read(it) + } + } + } + + inline fun getList( + changes: MutableMap, + doc: MutableMap, + fieldName: String + ): List? = (changes[fieldName] ?: doc[fieldName])?.let { value -> + catchTypeConversionError(fieldName, value) { + value as List + } } - fun set( + inline fun set( changes: MutableMap, fieldName: String, value: T, - clazz: KClass<*>, - mapper: ((T) -> MutableMap)? = null + mapper: ((T) -> MutableMap) + ) { + changes[fieldName] = mapper.invoke(value) + } + + inline fun set( + changes: MutableMap, + fieldName: String, + value: T?, + typeConverter: ITypeConverter ) { - val valueToSet = mapper?.let { it.invoke(value) } ?: write(value, fieldName, clazz) - changes[fieldName] = valueToSet + changes[fieldName] = typeConverter.write(value) + } + + inline fun set( + changes: MutableMap, + fieldName: String, + value: T? + ) { + changes[fieldName] = value } inline fun setList( changes: MutableMap, fieldName: String, value: List?, - clazz: KClass<*>, - noinline mapper: ((List) -> List>)? = null + mapper: ((List) -> List>) ) { - val valueToSet = - mapper?.let { if (value != null) it.invoke(value) else emptyList() } ?: write( - value, - fieldName, - clazz - ) - changes[fieldName] = valueToSet + changes[fieldName] = if (value != null) mapper.invoke(value) else emptyList() } - fun ensureTypes(map: Map>, doc: Map): Map { - val result = mutableMapOf() - for (entry in map) { - write(doc[entry.key], entry.key, entry.value)?.let { - result[entry.key] = it - } - } - return result + inline fun setList( + changes: MutableMap, + fieldName: String, + value: List?, + typeConverter: ITypeConverter + ) { + changes[fieldName] = value?.map { typeConverter.write(it) } } - fun addDefaults(list: List>, doc: MutableMap) { - for (entry in list) { - val key = entry[0] as String - val clazz = entry[1] as KClass<*> - val value = entry[2] as Any - if (doc[key] == null) { - write(value, key, clazz)?.let { - doc[key] = it as V - } - } - } + inline fun setList( + changes: MutableMap, + fieldName: String, + value: List? + ) { + changes[fieldName] = value } - fun read( - value: Any?, - fieldName: String, - clazz: KClass<*> - ): T? { - return try { - val conversion = - PersistenceConfig.getTypeConversion(clazz) ?: return value as T? - return conversion.read(value) as T? - } catch (ex: Exception) { - PersistenceConfig.onTypeConversionError( - com.schwarz.crystalapi.TypeConversionErrorWrapper( - ex, - fieldName, - value, - clazz - ) - ) - null + fun validate( + doc: MutableMap, + mandatoryFields: Array + ) { + for (mandatoryField in mandatoryFields) { + doc[mandatoryField]!! } } - fun write( - value: Any?, - fieldName: String, - clazz: KClass<*> - ): T? { - return try { - val conversion = - PersistenceConfig.getTypeConversion(clazz) ?: return value as T? - return conversion.write(value) as T? - } catch (ex: Exception) { - PersistenceConfig.onTypeConversionError( - com.schwarz.crystalapi.TypeConversionErrorWrapper( - ex, - fieldName, - value, - clazz - ) + inline fun catchTypeConversionError(fieldName: String, value: Any, task: () -> T): T? = try { + task() + } catch (cce: ClassCastException) { + PersistenceConfig.onTypeConversionError( + com.schwarz.crystalapi.TypeConversionErrorWrapper( + cce, + fieldName, + value, + T::class ) - null - } + ) + null } } diff --git a/crystal-map-api/src/test/java/com/schwarz/crystalapi/typeconverters/EnumConverterTest.kt b/crystal-map-api/src/test/java/com/schwarz/crystalapi/typeconverters/EnumConverterTest.kt new file mode 100644 index 00000000..b3626fba --- /dev/null +++ b/crystal-map-api/src/test/java/com/schwarz/crystalapi/typeconverters/EnumConverterTest.kt @@ -0,0 +1,47 @@ +package com.schwarz.crystalapi.typeconverters + +import com.schwarz.crystalapi.ITypeConverter +import junit.framework.TestCase.assertTrue +import org.junit.Test + +enum class TestEnum { + FOO, BAR +} + +object TestEnumConverter : ITypeConverter by EnumConverter(TestEnum::class) + +class EnumConverterTest { + + @Test + fun `should correctly read an enum value`() { + val result = TestEnumConverter.read("FOO") + + assertTrue(result == TestEnum.FOO) + } + + @Test(expected = IllegalArgumentException::class) + fun `should throw an exception for incorrect string value`() { + TestEnumConverter.read("FOZ") + } + + @Test + fun `should read null for null string value`() { + val result = TestEnumConverter.read(null) + + assertTrue(result == null) + } + + @Test + fun `should correctly write an enum value`() { + val result = TestEnumConverter.write(TestEnum.FOO) + + assertTrue(result == "FOO") + } + + @Test + fun `should write null for null enum value`() { + val result = TestEnumConverter.write(null) + + assertTrue(result == null) + } +} diff --git a/crystal-map-couchbase-connector/src/main/java/com/schwarz/crystalcouchbaseconnector/Couchbase2Connector.kt b/crystal-map-couchbase-connector/src/main/java/com/schwarz/crystalcouchbaseconnector/Couchbase2Connector.kt index 6f884f4d..cca37ba4 100644 --- a/crystal-map-couchbase-connector/src/main/java/com/schwarz/crystalcouchbaseconnector/Couchbase2Connector.kt +++ b/crystal-map-couchbase-connector/src/main/java/com/schwarz/crystalcouchbaseconnector/Couchbase2Connector.kt @@ -3,65 +3,13 @@ package com.schwarz.crystalcouchbaseconnector import com.couchbase.lite.* import com.schwarz.crystalapi.PersistenceConfig import com.schwarz.crystalapi.PersistenceException -import com.schwarz.crystalapi.TypeConversion import java.util.* import kotlin.jvm.Throws -import kotlin.reflect.KClass abstract class Couchbase2Connector : PersistenceConfig.Connector { - private val mTypeConversions = HashMap, TypeConversion>() - protected abstract fun getDatabase(name: String): Database - init { - mTypeConversions[Int::class] = object : TypeConversion { - - override fun write(value: Any?): Any? { - return value - } - - override fun read(value: Any?): Any? { - if (value is Number) { - return value.toInt() - } - if (value is Iterable<*>) { - val result = ArrayList() - for (itValue in value) { - itValue?.let { - read(itValue)?.let { it1 -> result.add(it1) } - } - } - return result - } - return value - } - } - mTypeConversions[Double::class] = object : TypeConversion { - override fun write(value: Any?): Any? { - return value - } - - override fun read(value: Any?): Any? { - if (value is Number) { - return value.toDouble() - } - if (value is Iterable<*>) { - val result = ArrayList() - for (itValue in value) { - itValue?.let { - read(itValue)?.let { it1 -> result.add(it1) } - } - } - return result - } - return value - } - } - } - - override val typeConversions: Map, TypeConversion> = mTypeConversions - override fun getDocument(id: String, dbName: String, onlyInclude: List?): Map? { val document = getDatabase(dbName).getDocument(id) ?: return null diff --git a/crystal-map-processor/build.gradle b/crystal-map-processor/build.gradle index 43b2332e..5d8483fc 100644 --- a/crystal-map-processor/build.gradle +++ b/crystal-map-processor/build.gradle @@ -7,8 +7,8 @@ apply plugin: 'java' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'com.squareup:kotlinpoet:1.6.0' - implementation 'com.squareup:kotlinpoet-metadata:1.6.0' + implementation 'com.squareup:kotlinpoet:1.15.0' + implementation 'com.squareup:kotlinpoet-metadata:1.15.0' implementation project(path: ':crystal-map-api', configuration: 'default') implementation 'org.apache.commons:commons-lang3:3.4' implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.13.3" diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/CoachBaseBinderProcessor.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/CoachBaseBinderProcessor.kt index dcf6ad02..5640c783 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/CoachBaseBinderProcessor.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/CoachBaseBinderProcessor.kt @@ -8,6 +8,9 @@ import com.schwarz.crystalapi.MapWrapper import com.schwarz.crystalapi.Reduce import com.schwarz.crystalapi.Reduces import com.schwarz.crystalapi.SchemaClass +import com.schwarz.crystalapi.TypeConverter +import com.schwarz.crystalapi.TypeConverterExporter +import com.schwarz.crystalapi.TypeConverterImporter import com.schwarz.crystalapi.mapify.Mapper import com.schwarz.crystalapi.query.Queries import com.schwarz.crystalapi.query.Query @@ -109,7 +112,10 @@ class CoachBaseBinderProcessor : AbstractProcessor() { GenerateAccessor::class.java.canonicalName, Mapper::class.java.canonicalName, Reduces::class.java.canonicalName, - Reduce::class.java.canonicalName + Reduce::class.java.canonicalName, + TypeConverter::class.java.canonicalName, + TypeConverterExporter::class.java.canonicalName, + TypeConverterImporter::class.java.canonicalName ).toMutableSet() } } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/CodeGenerator.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/CodeGenerator.kt index d402a132..68418ad2 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/CodeGenerator.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/CodeGenerator.kt @@ -20,7 +20,7 @@ class CodeGenerator(private val filer: Filer) { } val codePath = processingEnvironment.options[CoachBaseBinderProcessor.KAPT_KOTLIN_GENERATED_OPTION_NAME] - val fileWithHeader = entityToGenerate.toBuilder().addComment(HEADER).build() + val fileWithHeader = entityToGenerate.toBuilder().addFileComment(HEADER).build() // used for kapt returns null for legacy annotationprocessor declarations if (codePath != null) { 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 d3c2ed84..de4fd648 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,6 +1,5 @@ package com.schwarz.crystalprocessor.generation.model -import com.schwarz.crystalapi.util.CrystalWrap import com.schwarz.crystalprocessor.model.entity.BaseEntityHolder import com.schwarz.crystalprocessor.util.ConversionUtil import com.schwarz.crystalprocessor.util.TypeUtil @@ -20,26 +19,23 @@ object CblDefaultGeneration { if (useNullableMap) TypeUtil.anyNullable() else TypeUtil.any() val builder = - FunSpec.builder("addDefaults").addModifiers(KModifier.PRIVATE).addParameter("map", type) + FunSpec.builder("addDefaults").addModifiers(KModifier.PRIVATE) - builder.addStatement("%T.addDefaults<%T, %T>(listOf(", CrystalWrap::class, typeConversionReturnType, valueType) for (fieldHolder in holder.fields.values) { if (fieldHolder.isDefault) { builder.addStatement( - "arrayOf(%N, %T::class, ${ConversionUtil.convertStringToDesiredFormat( + "this.%N = ${ConversionUtil.convertStringToDesiredFormat( fieldHolder.typeMirror, fieldHolder.defaultValue - )}),", - fieldHolder.constantName, - fieldHolder.fieldType + )}", + fieldHolder.accessorSuffix() ) } } - builder.addStatement("), map)") return builder.build() } - fun addAddCall(nameOfMap: String): CodeBlock { - return CodeBlock.builder().addStatement("addDefaults(%N)", nameOfMap).build() + fun addAddCall(): CodeBlock { + return CodeBlock.builder().addStatement("addDefaults()").build() } } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/CommonInterfaceGeneration.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/CommonInterfaceGeneration.kt index c1550958..dde0cbb9 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/CommonInterfaceGeneration.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/CommonInterfaceGeneration.kt @@ -2,6 +2,7 @@ package com.schwarz.crystalprocessor.generation.model import com.schwarz.crystalprocessor.model.entity.BaseEntityHolder import com.schwarz.crystalprocessor.model.entity.BaseModelHolder +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderForEntityGeneration import com.schwarz.crystalprocessor.util.TypeUtil import com.squareup.kotlinpoet.* import java.util.* @@ -9,7 +10,7 @@ import java.util.* private const val GENERATED_REPRESENT_NAME = "Represent" class CommonInterfaceGeneration { - fun generateModel(holder: BaseEntityHolder, useSuspend: Boolean): FileSpec { + fun generateModel(holder: BaseEntityHolder, useSuspend: Boolean, typeConvertersByConvertedClass: Map): FileSpec { val interfaceSpec = TypeSpec.interfaceBuilder(holder.interfaceSimpleName) interfaceSpec.addSuperinterface(TypeUtil.mapSupport()) @@ -31,19 +32,19 @@ class CommonInterfaceGeneration { if (holder is BaseModelHolder) { companionSpec.addFunctions(fromMap(holder)) - generateRepresent(holder, interfaceSpec, useSuspend) + generateRepresent(holder, interfaceSpec, useSuspend, typeConvertersByConvertedClass) } interfaceSpec.addType(companionSpec.build()) return FileSpec.get(holder.sourcePackage, interfaceSpec.build()) } - private fun generateRepresent(holder: BaseModelHolder, parent: TypeSpec.Builder, useSuspend: Boolean) { + private fun generateRepresent(holder: BaseModelHolder, parent: TypeSpec.Builder, useSuspend: Boolean, typeConvertersByConvertedClass: Map) { val typeBuilder = TypeSpec.classBuilder(GENERATED_REPRESENT_NAME) .addSuperinterface(TypeUtil.mapSupport()) .addModifiers(KModifier.PRIVATE) .addSuperinterface(holder.interfaceTypeName) - .addFunction(EnsureTypesGeneration.ensureTypes(holder, true)) + .addFunction(EnsureTypesGeneration.ensureTypes(holder, true, typeConvertersByConvertedClass)) .addFunction(CblConstantGeneration.addConstants(holder, true)) .addFunction(SetAllMethodGeneration().generate(holder, false)) .addFunction(MapSupportGeneration.toMap(holder)) @@ -62,7 +63,7 @@ class CommonInterfaceGeneration { } for (fieldHolder in holder.allFields) { - typeBuilder.addProperty(fieldHolder.property(null, holder.abstractParts, false, holder.deprecated)) + typeBuilder.addProperty(fieldHolder.property(null, holder.abstractParts, false, holder.deprecated, typeConvertersByConvertedClass)) } val companionSpec = TypeSpec.companionObjectBuilder() 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 d326e75c..da3f2028 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 @@ -2,26 +2,88 @@ package com.schwarz.crystalprocessor.generation.model import com.schwarz.crystalapi.util.CrystalWrap import com.schwarz.crystalprocessor.model.entity.BaseEntityHolder +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderForEntityGeneration import com.schwarz.crystalprocessor.util.TypeUtil import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.TypeName + +private const val RESULT_VAL_NAME = "result" object EnsureTypesGeneration { - fun ensureTypes(holder: BaseEntityHolder, useNullableMap: Boolean): FunSpec { + fun ensureTypes( + holder: BaseEntityHolder, + useNullableMap: Boolean, + typeConvertersByConvertedClass: Map + ): FunSpec { 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 result = %T()", explicitType) - ensureTypes.addStatement("result.putAll(doc)") + ensureTypes.addStatement("val %N = %T()", RESULT_VAL_NAME, explicitType) + ensureTypes.addStatement("%N.putAll(doc)", RESULT_VAL_NAME) - ensureTypes.addStatement("result.putAll(%T.ensureTypes<%T>(mapOf(", CrystalWrap::class, typeConversionReturnType) for (field in holder.fields.values) { - ensureTypes.addStatement("%N to %T::class,", field.constantName, field.evaluateClazzForTypeConversion()) + 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 { + val typeConverterHolder = + typeConvertersByConvertedClass.get(field.fieldType)!! + if (field.isIterable) { + ensureTypes.addStatement( + "%T.getList(mutableMapOf(), %N, %N, %T)", + CrystalWrap::class, + RESULT_VAL_NAME, + field.constantName, + typeConverterHolder.instanceClassTypeName + ) + } else { + ensureTypes.addStatement( + "%T.get(mutableMapOf(), %N, %N, %T)", + CrystalWrap::class, + RESULT_VAL_NAME, + field.constantName, + typeConverterHolder.instanceClassTypeName + ) + } + } } - ensureTypes.addStatement("), doc))") ensureTypes.addStatement("return result") return ensureTypes.build() 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 4f5e93ec..551037bf 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 @@ -16,6 +16,8 @@ import com.schwarz.crystalapi.Entity import com.schwarz.crystalapi.MandatoryCheck import com.schwarz.crystalapi.PersistenceConfig import com.schwarz.crystalapi.PersistenceException +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderForEntityGeneration +import com.squareup.kotlinpoet.TypeName class EntityGeneration { @@ -30,7 +32,11 @@ class EntityGeneration { ) .build() - fun generateModel(holder: EntityHolder, useSuspend: Boolean): FileSpec { + fun generateModel( + holder: EntityHolder, + useSuspend: Boolean, + typeConvertersByConvertedClass: Map + ): FileSpec { val companionSpec = TypeSpec.companionObjectBuilder() companionSpec.superclass(TypeUtil.crystalCreator(TypeUtil.any(), holder.entityTypeName)) companionSpec.addProperty(idConstant()) @@ -40,7 +46,7 @@ class EntityGeneration { companionSpec.addFunction(findByIds(holder, useSuspend)) for (query in holder.queries) { - query.queryFun(holder.dbName, holder, useSuspend).let { + query.queryFun(holder.dbName, holder, useSuspend, typeConvertersByConvertedClass).let { companionSpec.addFunction(it) } } @@ -62,7 +68,7 @@ class EntityGeneration { .addSuperinterface(holder.interfaceTypeName) .addSuperinterface(MandatoryCheck::class) .addProperty(holder.dbNameProperty()) - .addFunction(EnsureTypesGeneration.ensureTypes(holder, false)) + .addFunction(EnsureTypesGeneration.ensureTypes(holder, false, typeConvertersByConvertedClass)) .addFunction(CblDefaultGeneration.addDefaults(holder, false)) .addFunction(CblConstantGeneration.addConstants(holder, false)) .addFunction(ValidateMethodGeneration.generate(holder, true)) @@ -119,7 +125,8 @@ class EntityGeneration { holder.dbName, holder.abstractParts, true, - holder.deprecated + holder.deprecated, + typeConvertersByConvertedClass ) ) } 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 d11cf87d..120e6fbe 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,13 +11,13 @@ 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( CodeBlock.builder() .beginControlFlow("if(doc != null)") .addStatement("mDoc.putAll(doc)") .endControlFlow().build() ) - .addCode(CblDefaultGeneration.addAddCall("mDoc")) .addCode(CblConstantGeneration.addAddCall("mDoc")) if (clearMDocChanges) { diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/TypeConverterExporterObjectGeneration.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/TypeConverterExporterObjectGeneration.kt new file mode 100644 index 00000000..6851cc2d --- /dev/null +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/TypeConverterExporterObjectGeneration.kt @@ -0,0 +1,116 @@ +package com.schwarz.crystalprocessor.generation.model + +import com.schwarz.crystalapi.ClassNameDefinition +import com.schwarz.crystalapi.ITypeConverterExporter +import com.schwarz.crystalapi.TypeConverterImportable +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterExporterHolder +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolder +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.STAR +import com.squareup.kotlinpoet.TypeSpec + +object TypeConverterExporterObjectGeneration { + + val map: Map = mapOf() + + fun generateTypeConverterExporterObject( + typeConverterExporterHolder: TypeConverterExporterHolder, + typeConverterHolders: List + ): FileSpec { + val typeSpec = TypeSpec.classBuilder(typeConverterExporterHolder.name + "Instance") + .addSuperinterface( + ClassName( + typeConverterExporterHolder.sourcePackageName, + typeConverterExporterHolder.name + ) + ) + .addSuperinterface(ITypeConverterExporter::class) + .addProperty( + getTypeConvertersSpec(typeConverterHolders) + ) + .addProperty( + getTypeConverterImportablesSpec(typeConverterHolders) + ) + .build() + + return FileSpec.get(typeConverterExporterHolder.sourcePackageName, typeSpec) + } + + private fun getTypeConvertersSpec(typeConverterHolders: List): PropertySpec { + val codeBlockBuilder = CodeBlock.Builder() + + codeBlockBuilder.add("return mapOf(\n") + + typeConverterHolders.forEach { + codeBlockBuilder.add( + "%T::class to %T,\n", + it.domainClassTypeName, + it.instanceClassTypeName + ) + } + + codeBlockBuilder.add(")") + + val codeBlock = codeBlockBuilder.build() + + return PropertySpec.builder("typeConverters", typeConverterMapType()) + .getter( + FunSpec.getterBuilder() + .addCode(codeBlock) + .build() + ) + .addModifiers(KModifier.OVERRIDE) + .build() + } + + private fun getTypeConverterImportablesSpec(typeConverterHolders: List): PropertySpec { + val codeBlockBuilder = CodeBlock.Builder() + + codeBlockBuilder.add("return listOf(\n") + + typeConverterHolders.forEach { + codeBlockBuilder.add( + "%T(\n%T(%S, %S), \n%T(%S, %S), \n%T(%S, %S)\n),\n", + TypeConverterImportable::class, + ClassNameDefinition::class, + it.instanceClassTypeName.packageName, + it.instanceClassTypeName.simpleName, + ClassNameDefinition::class, + it.domainClassTypeName.packageName, + it.domainClassTypeName.simpleName, + ClassNameDefinition::class, + it.mapClassTypeName.packageName, + it.mapClassTypeName.simpleName + ) + } + + codeBlockBuilder.add(")") + + val codeBlock = codeBlockBuilder.build() + + return PropertySpec.builder("typeConverterImportables", typeConverterImportablesListType()) + .getter( + FunSpec.getterBuilder() + .addCode(codeBlock) + .build() + ) + .addModifiers(KModifier.OVERRIDE) + .build() + } + + private fun typeConverterMapType() = ClassName("kotlin.collections", "Map") + .parameterizedBy( + ClassName("kotlin.reflect", "KClass").parameterizedBy(STAR), + ClassName("com.schwarz.crystalapi", "ITypeConverter").parameterizedBy(STAR, STAR) + ) + private fun typeConverterImportablesListType() = ClassName("kotlin.collections", "List") + .parameterizedBy( + ClassName("com.schwarz.crystalapi", "TypeConverterImportable") + ) +} diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/TypeConverterObjectGeneration.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/TypeConverterObjectGeneration.kt new file mode 100644 index 00000000..cea2b7a0 --- /dev/null +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/model/TypeConverterObjectGeneration.kt @@ -0,0 +1,16 @@ +package com.schwarz.crystalprocessor.generation.model + +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolder +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.TypeSpec + +object TypeConverterObjectGeneration { + + fun generateTypeConverterObject(typeConverterHolder: TypeConverterHolder): FileSpec { + val typeSpec = TypeSpec.objectBuilder(typeConverterHolder.instanceClassTypeName) + .superclass(typeConverterHolder.classTypeName) + .build() + + return FileSpec.get(typeConverterHolder.instanceClassTypeName.packageName, typeSpec) + } +} 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 135ea122..07a05274 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 @@ -4,13 +4,14 @@ import com.schwarz.crystalapi.MandatoryCheck import com.schwarz.crystalprocessor.generation.MapifyableImplGeneration import com.schwarz.crystalprocessor.model.entity.BaseEntityHolder import com.schwarz.crystalprocessor.model.entity.WrapperEntityHolder +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderForEntityGeneration import com.schwarz.crystalprocessor.util.TypeUtil import com.squareup.kotlinpoet.* import java.util.* class WrapperGeneration { - fun generateModel(holder: WrapperEntityHolder, useSuspend: Boolean): FileSpec { + fun generateModel(holder: WrapperEntityHolder, useSuspend: Boolean, typeConvertersByConvertedClass: Map): FileSpec { val companionSpec = TypeSpec.companionObjectBuilder() companionSpec.superclass(TypeUtil.wrapperCompanion(holder.entityTypeName)) @@ -21,7 +22,7 @@ class WrapperGeneration { .addModifiers(KModifier.PUBLIC) .addSuperinterface(holder.interfaceTypeName) .addSuperinterface(MandatoryCheck::class) - .addFunction(EnsureTypesGeneration.ensureTypes(holder, true)) + .addFunction(EnsureTypesGeneration.ensureTypes(holder, true, typeConvertersByConvertedClass)) .addFunction(CblDefaultGeneration.addDefaults(holder, true)) .addFunction(CblConstantGeneration.addConstants(holder, true)) .addFunction(SetAllMethodGeneration().generate(holder, false)) @@ -50,7 +51,7 @@ class WrapperGeneration { for (fieldHolder in holder.allFields) { companionSpec.addProperties(fieldHolder.createFieldConstant()) - typeBuilder.addProperty(fieldHolder.property(null, holder.abstractParts, false, holder.deprecated)) + typeBuilder.addProperty(fieldHolder.property(null, holder.abstractParts, false, holder.deprecated, typeConvertersByConvertedClass)) fieldHolder.builderSetter(null, holder.sourcePackage, holder.entitySimpleName, false, holder.deprecated)?.let { builderBuilder.addFunction(it) } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/accessor/CblGenerateAccessorHolder.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/accessor/CblGenerateAccessorHolder.kt index 93384b3e..b8632ef4 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/accessor/CblGenerateAccessorHolder.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/accessor/CblGenerateAccessorHolder.kt @@ -32,6 +32,14 @@ class CblGenerateAccessorHolder( memberFunction.name ) + val isNullableSuspendFun = memberFunction.generateAccessor?.isNullableSuspendFun ?: false + + if (isNullableSuspendFun) { + methodBuilder.returns(memberFunction.returnTypeName.copy(nullable = true)) + } else { + methodBuilder.returns(memberFunction.returnTypeName) + } + return methodBuilder.build() } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblBaseFieldHolder.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblBaseFieldHolder.kt index e911917a..9419b938 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblBaseFieldHolder.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblBaseFieldHolder.kt @@ -7,6 +7,9 @@ import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeName import com.schwarz.crystalapi.Field +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderForEntityGeneration +import com.schwarz.crystalprocessor.model.typeconverter.nonConvertibleClassesTypeNames +import com.squareup.kotlinpoet.ParameterizedTypeName import org.apache.commons.lang3.text.WordUtils import javax.lang.model.type.TypeMirror @@ -41,6 +44,12 @@ abstract class CblBaseFieldHolder(val dbField: String, private val mField: Field abstract val fieldType: TypeName + val isNonConvertibleClass: Boolean + get() { + val rawFieldType = (fieldType as? ParameterizedTypeName)?.rawType ?: fieldType + return nonConvertibleClassesTypeNames.contains(rawFieldType) + } + fun accessorSuffix(): String { return WordUtils.uncapitalize( WordUtils.capitalize(dbField.replace("_".toRegex(), " ")).replace(" ".toRegex(), "") @@ -53,7 +62,8 @@ abstract class CblBaseFieldHolder(val dbField: String, private val mField: Field dbName: String?, possibleOverrides: Set, useMDocChanges: Boolean, - deprecated: DeprecatedModel? + deprecated: DeprecatedModel?, + typeConvertersByConvertedClass: Map ): PropertySpec abstract fun builderSetter( diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblConstantHolder.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblConstantHolder.kt index 8d9f20ce..65e44f18 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblConstantHolder.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/field/CblConstantHolder.kt @@ -11,6 +11,8 @@ import java.util.Arrays import com.schwarz.crystalapi.Field import com.schwarz.crystalapi.util.CrystalWrap +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderForEntityGeneration +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterProcessingException /** * Created by sbra0902 on 21.06.17. @@ -34,11 +36,39 @@ class CblConstantHolder(field: Field) : CblBaseFieldHolder(field.name, field) { return builder.build() } - override fun property(dbName: String?, possibleOverrides: Set, useMDocChanges: Boolean, deprecated: DeprecatedModel?): PropertySpec { + override fun property( + dbName: String?, + possibleOverrides: Set, + useMDocChanges: Boolean, + deprecated: DeprecatedModel?, + typeConvertersByConvertedClass: Map + ): PropertySpec { val mDocPhrase = if (useMDocChanges) "mDocChanges, mDoc" else "mDoc, mutableMapOf()" - - val builder = PropertySpec.builder(accessorSuffix(), fieldType, KModifier.PUBLIC, KModifier.OVERRIDE) - .getter(FunSpec.getterBuilder().addStatement("return %T.get<%T>($mDocPhrase, %N, %T::class)!!", CrystalWrap::class, fieldType, constantName, fieldType).build()) + val builder = + PropertySpec.builder(accessorSuffix(), fieldType, KModifier.PUBLIC, KModifier.OVERRIDE) + + if (isNonConvertibleClass) { + builder.getter( + FunSpec.getterBuilder().addStatement( + "return %T.get<%T>($mDocPhrase, %N)!!", + CrystalWrap::class, + fieldType, + constantName + ).build() + ) + } else { + val typeConverterHolder = typeConvertersByConvertedClass[fieldType] + ?: throw TypeConverterProcessingException("Missing type conversion for $fieldType") + + builder.getter( + FunSpec.getterBuilder().addStatement( + "return %T.get($mDocPhrase, %N, %T)!!", + CrystalWrap::class, + constantName, + typeConverterHolder.instanceClassTypeName + ).build() + ) + } deprecated?.addDeprecated(dbField, builder) if (comment.isNotEmpty()) { @@ -49,17 +79,30 @@ class CblConstantHolder(field: Field) : CblBaseFieldHolder(field.name, field) { } override fun createFieldConstant(): List { - val fieldAccessorConstant = PropertySpec.builder(constantName, String::class, KModifier.FINAL, KModifier.PUBLIC).initializer("%S", dbField).addAnnotation(JvmField::class).build() + val fieldAccessorConstant = + PropertySpec.builder(constantName, String::class, KModifier.FINAL, KModifier.PUBLIC) + .initializer("%S", dbField).addAnnotation(JvmField::class).build() return Arrays.asList( fieldAccessorConstant, - PropertySpec.builder(constantValueAccessorName, typeMirror.asTypeName().javaToKotlinType(), KModifier.FINAL, KModifier.PUBLIC).initializer( + PropertySpec.builder( + constantValueAccessorName, + typeMirror.asTypeName().javaToKotlinType(), + KModifier.FINAL, + KModifier.PUBLIC + ).initializer( ConversionUtil.convertStringToDesiredFormat(typeMirror, constantValue) ).addAnnotation(JvmField::class).build() ) } - override fun builderSetter(dbName: String?, packageName: String, entitySimpleName: String, useMDocChanges: Boolean, deprecated: DeprecatedModel?): FunSpec? { + override fun builderSetter( + dbName: String?, + packageName: String, + entitySimpleName: String, + useMDocChanges: Boolean, + deprecated: DeprecatedModel? + ): FunSpec? { return null } } 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 c1bb3e9e..f569a280 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 @@ -1,15 +1,16 @@ package com.schwarz.crystalprocessor.model.field +import com.schwarz.crystalapi.Field +import com.schwarz.crystalapi.util.CrystalWrap import com.schwarz.crystalprocessor.generation.model.KDocGeneration import com.schwarz.crystalprocessor.model.deprecated.DeprecatedModel +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderForEntityGeneration import com.schwarz.crystalprocessor.util.TypeUtil import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeName -import com.schwarz.crystalapi.Field -import com.schwarz.crystalapi.util.CrystalWrap import org.apache.commons.lang3.StringUtils class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix: String) : @@ -25,10 +26,10 @@ class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix override var isIterable: Boolean = false - private val subEntityTypeName: TypeName + val subEntityTypeName: TypeName get() = ClassName(subEntityPackage!!, subEntitySimpleName!!) - private val isTypeOfSubEntity: Boolean + val isTypeOfSubEntity: Boolean get() = !StringUtils.isBlank(subEntitySimpleName) override val fieldType: TypeName = @@ -68,7 +69,8 @@ class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix dbName: String?, possibleOverrides: Set, useMDocChanges: Boolean, - deprecated: DeprecatedModel? + deprecated: DeprecatedModel?, + typeConvertersByConvertedClass: Map ): PropertySpec { var returnType = TypeUtil.parseMetaType(typeMirror, isIterable, subEntitySimpleName) @@ -89,70 +91,112 @@ class CblFieldHolder(field: Field, classPaths: List, subEntityNameSuffix deprecated?.addDeprecated(dbField, propertyBuilder) val mDocPhrase = if (useMDocChanges) "mDocChanges, mDoc" else "mDoc, mutableMapOf()" - - if (isTypeOfSubEntity) { + if (isNonConvertibleClass) { + if (isIterable) { + getter.addStatement( + "return %T.getList<%T>($mDocPhrase, %N)".forceCastIfMandatory( + mandatory + ), + CrystalWrap::class, + 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( + mandatory + ), + CrystalWrap::class, + 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::class, {%T.fromMap(it) ?: emptyList()})".forceCastIfMandatory(mandatory), + "return %T.getList<%T>($mDocPhrase, %N, {%T.fromMap(it) ?: emptyList()})".forceCastIfMandatory( + mandatory + ), CrystalWrap::class, subEntityTypeName, constantName, - subEntityTypeName, subEntityTypeName ) setter.addStatement( - "%T.setList(%N, %N, value, %T::class, {%T.toMap(it)})", + "%T.setList(%N, %N, value, {%T.toMap(it)})", CrystalWrap::class, if (useMDocChanges) "mDocChanges" else "mDoc", constantName, - subEntityTypeName, subEntityTypeName ) } else { getter.addStatement( - "return %T.get<%T>($mDocPhrase, %N, %T::class, {%T.fromMap(it)})".forceCastIfMandatory(mandatory), + "return %T.get<%T>($mDocPhrase, %N, {%T.fromMap(it)})".forceCastIfMandatory( + mandatory + ), CrystalWrap::class, subEntityTypeName, constantName, - subEntityTypeName, subEntityTypeName ) setter.addStatement( - "%T.set(%N, %N, value, %T::class, {%T.toMap(it)})", + "%T.set(%N, %N, value, {%T.toMap(it)})", CrystalWrap::class, if (useMDocChanges) "mDocChanges" else "mDoc", constantName, - subEntityTypeName, subEntityTypeName ) } } else { - val forTypeConversion = evaluateClazzForTypeConversion() + val typeConverterHolder = + typeConvertersByConvertedClass.get(fieldType)!! if (isIterable) { getter.addStatement( - "return %T.getList<%T>($mDocPhrase, %N, %T::class)".forceCastIfMandatory(mandatory), + "return %T.getList($mDocPhrase, %N, %T)".forceCastIfMandatory( + mandatory + ), CrystalWrap::class, - fieldType, constantName, - forTypeConversion + 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<%T>($mDocPhrase, %N, %T::class)".forceCastIfMandatory(mandatory), + "return %T.get($mDocPhrase, %N, %T)".forceCastIfMandatory( + mandatory + ), CrystalWrap::class, - fieldType, constantName, - forTypeConversion + typeConverterHolder.instanceClassTypeName ) - } - setter.addStatement( - "%T.set(%N, %N, value, %T::class)", - CrystalWrap::class, - if (useMDocChanges) "mDocChanges" else "mDoc", - constantName, - forTypeConversion - ) + setter.addStatement( + "%T.set(%N, %N, value, %T)", + CrystalWrap::class, + if (useMDocChanges) "mDocChanges" else "mDoc", + constantName, + typeConverterHolder.instanceClassTypeName + ) + } } if (comment.isNotEmpty()) { diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/query/CblQueryHolder.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/query/CblQueryHolder.kt index e6d87d20..c1a02a1c 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/query/CblQueryHolder.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/query/CblQueryHolder.kt @@ -10,7 +10,8 @@ import com.squareup.kotlinpoet.jvm.throws import com.schwarz.crystalapi.PersistenceConfig import com.schwarz.crystalapi.PersistenceException import com.schwarz.crystalapi.query.Query -import com.schwarz.crystalapi.util.CrystalWrap +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderForEntityGeneration +import com.squareup.kotlinpoet.TypeName import org.apache.commons.lang3.text.WordUtils /** @@ -22,7 +23,12 @@ class CblQueryHolder(private val mQuery: Query) { val fields: Array get() = mQuery.fields - fun queryFun(dbName: String, entityHolder: BaseEntityHolder, useSuspend: Boolean): FunSpec { + fun queryFun( + dbName: String, + entityHolder: BaseEntityHolder, + useSuspend: Boolean, + typeConvertersByConvertedClass: Map + ): FunSpec { val builder = FunSpec.builder(queryFunName) .addModifiers(KModifier.PUBLIC) .addAnnotation(JvmStatic::class) @@ -46,7 +52,7 @@ class CblQueryHolder(private val mQuery: Query) { entityHolder.fields[it]?.apply { builder.addParameter(dbField, fieldType) - builder.addQueryParamComparisonStatement(this, dbField) + builder.addQueryParamComparisonStatement(this, dbField, typeConvertersByConvertedClass) } entityHolder.fieldConstants[it]?.apply { builder.addStatement( @@ -71,17 +77,24 @@ class CblQueryHolder(private val mQuery: Query) { private fun FunSpec.Builder.addQueryParamComparisonStatement( fieldHolder: CblFieldHolder, - value: String + value: String, + typeConvertersByConvertedClass: Map ) { - val classForTypeConversion = fieldHolder.evaluateClazzForTypeConversion() - addStatement( - "queryParams[%N] = %T.write(%N, %N, %T::class) ?:\nthrow PersistenceException(\"Invalid·type-conversion:·value·must·not·be·null\")", - fieldHolder.constantName, - CrystalWrap::class, - value, - fieldHolder.constantName, - classForTypeConversion - ) + if (fieldHolder.isNonConvertibleClass) { + addStatement( + "queryParams[%N] = %N", + fieldHolder.constantName, + value + ) + } else { + addStatement( + "queryParams[%N] = %T.write(%N) ?:\nthrow PersistenceException(\"Invalid·type-conversion:·value·must·not·be·null\")", + fieldHolder.constantName, + typeConvertersByConvertedClass.get(fieldHolder.fieldType)!!.instanceClassTypeName, + value + + ) + } } private val queryFunName: String = "findBy${ diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/source/SourceModel.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/source/SourceModel.kt index e343f08b..e1d70be3 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/source/SourceModel.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/source/SourceModel.kt @@ -20,6 +20,7 @@ import com.schwarz.crystalapi.Reduces import com.schwarz.crystalapi.deprecated.Deprecated import com.schwarz.crystalapi.query.Queries import com.schwarz.crystalapi.query.Query +import com.sun.tools.javac.code.Type import org.apache.commons.lang3.text.WordUtils import org.jetbrains.annotations.Nullable import javax.lang.model.element.Element @@ -110,6 +111,15 @@ data class SourceModel(private val sourceElement: Element) : ISourceModel, IClas } } + val returnType = if (isSuspend) { + val continuationParam = (it.parameters.last()) as Symbol.VarSymbol + val continuationParamType = continuationParam.type as Type.ClassType + val wildcardTypeParam = continuationParamType.allparams().first() as Type.WildcardType + wildcardTypeParam.type.asTypeName().javaToKotlinType() + } else { + it.returnType.asTypeName().javaToKotlinType().copy(it.getAnnotation(Nullable::class.java) != null) + } + relevantStaticsFunctions.add( SourceMemberFunction( name = it.simpleName.toString(), @@ -117,7 +127,7 @@ data class SourceModel(private val sourceElement: Element) : ISourceModel, IClas parameters = parameter, generateAccessor = accessor, docIdSegment = docSegment, - returnTypeName = it.returnType.asTypeName().javaToKotlinType().copy(it.getAnnotation(Nullable::class.java) != null) + returnTypeName = returnType ) ) } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/NonConvertibleClasses.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/NonConvertibleClasses.kt new file mode 100644 index 00000000..02844193 --- /dev/null +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/NonConvertibleClasses.kt @@ -0,0 +1,13 @@ +package com.schwarz.crystalprocessor.model.typeconverter + +import com.squareup.kotlinpoet.asTypeName + +val nonConvertibleClasses = listOf( + String::class, + Boolean::class, + Number::class, + Map::class, + Any::class +) + +val nonConvertibleClassesTypeNames = nonConvertibleClasses.map { it.asTypeName() } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterExporterHolder.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterExporterHolder.kt new file mode 100644 index 00000000..19c5ad92 --- /dev/null +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterExporterHolder.kt @@ -0,0 +1,13 @@ +package com.schwarz.crystalprocessor.model.typeconverter + +import com.sun.tools.javac.code.Symbol +import javax.lang.model.element.Element + +class TypeConverterExporterHolder(val sourceElement: Element) { + + private val sourceTypeElement: Symbol.ClassSymbol = sourceElement as Symbol.ClassSymbol + private val sourcePackage: Symbol.PackageSymbol = sourceTypeElement.packge() + + val name: String get() = sourceTypeElement.simpleName.toString() + val sourcePackageName get() = sourcePackage.toString() +} diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterHolder.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterHolder.kt new file mode 100644 index 00000000..dddeeb50 --- /dev/null +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterHolder.kt @@ -0,0 +1,22 @@ +package com.schwarz.crystalprocessor.model.typeconverter + +import com.squareup.kotlinpoet.ClassName + +interface TypeConverterHolderForEntityGeneration { + val instanceClassTypeName: ClassName + val domainClassTypeName: ClassName + val mapClassTypeName: ClassName +} + +data class ImportedTypeConverterHolder( + override val instanceClassTypeName: ClassName, + override val domainClassTypeName: ClassName, + override val mapClassTypeName: ClassName +) : TypeConverterHolderForEntityGeneration + +data class TypeConverterHolder( + val classTypeName: ClassName, + override val instanceClassTypeName: ClassName, + override val domainClassTypeName: ClassName, + override val mapClassTypeName: ClassName +) : TypeConverterHolderForEntityGeneration diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterHolderFactory.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterHolderFactory.kt new file mode 100644 index 00000000..0a86372b --- /dev/null +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterHolderFactory.kt @@ -0,0 +1,57 @@ +package com.schwarz.crystalprocessor.model.typeconverter + +import com.schwarz.crystalapi.ClassNameDefinition +import com.schwarz.crystalapi.ITypeConverterExporter +import com.schwarz.crystalapi.TypeConverterImportable +import com.schwarz.crystalapi.TypeConverterImporter +import com.schwarz.crystalprocessor.util.FieldExtractionUtil +import com.squareup.kotlinpoet.ClassName +import com.sun.tools.javac.code.Symbol +import kotlinx.metadata.KmClassifier +import kotlinx.metadata.KmTypeProjection +import javax.lang.model.element.Element + +object TypeConverterHolderFactory { + + fun typeConverterHolder(sourceElement: Element): TypeConverterHolder { + val sourceTypeElement = sourceElement as Symbol.ClassSymbol + val sourcePackageName = sourceTypeElement.packge().toString() + val className = sourceTypeElement.simpleName.toString() + val typeConverterKmType = sourceElement.getTypeConverterInterface()!! + val (domainClassType, mapClassType) = typeConverterKmType.arguments + return TypeConverterHolder( + ClassName(sourcePackageName, className), + ClassName(sourcePackageName, className + "Instance"), + domainClassType.resolveToString().toTypeName(), + mapClassType.resolveToString().toTypeName() + ) + } + + fun importedTypeConverterHolders(element: Element): List { + val annotation = element.getAnnotation(TypeConverterImporter::class.java) + val typeConverterExporterClassName = FieldExtractionUtil.typeMirror(annotation).first().toString() + val typeConverterExporterClazz = javaClass.classLoader.loadClass(typeConverterExporterClassName) + val importables = (typeConverterExporterClazz.constructors[0].newInstance() as ITypeConverterExporter) + .typeConverterImportables + return importables.map { importedTypeConverterHolder(it) } + } + + fun importedTypeConverterHolder(typeConverterImportable: TypeConverterImportable) = + ImportedTypeConverterHolder( + typeConverterImportable.typeConverterInstanceClassName.toClassName(), + typeConverterImportable.domainClassName.toClassName(), + typeConverterImportable.mapClassName.toClassName() + ) + + private fun ClassNameDefinition.toClassName() = ClassName(packageName, className) + + private fun String.toTypeName() = split('.').let { + ClassName(it.subList(0, it.size - 1).joinToString("."), it.last()) + } + + private fun KmTypeProjection.resolveToString(): String { + val classifier = type!!.classifier as KmClassifier.Class + val typeName = classifier.name.replace('/', '.') + return typeName + } +} diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterProcessingException.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterProcessingException.kt new file mode 100644 index 00000000..8df8a0dd --- /dev/null +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterProcessingException.kt @@ -0,0 +1,3 @@ +package com.schwarz.crystalprocessor.model.typeconverter + +class TypeConverterProcessingException(msg: String) : Throwable(msg) diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterUtils.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterUtils.kt new file mode 100644 index 00000000..e6dcaaa7 --- /dev/null +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/typeconverter/TypeConverterUtils.kt @@ -0,0 +1,20 @@ +package com.schwarz.crystalprocessor.model.typeconverter + +import com.schwarz.crystalapi.ITypeConverter +import kotlinx.metadata.KmClassifier +import kotlinx.metadata.KmType +import kotlinx.metadata.jvm.KotlinClassMetadata +import javax.lang.model.element.Element + +fun Element.getTypeConverterInterface(): KmType? { + val kmClass = getAnnotation(Metadata::class.java).toKmClass() + return kmClass.supertypes.find { + val classifier = it.classifier + classifier is KmClassifier.Class && typeConverterKmClass.name == classifier.name + } +} + +private val typeConverterKmClass = ITypeConverter::class.java.getAnnotation(Metadata::class.java).toKmClass() + +private fun Metadata.toKmClass() = + (KotlinClassMetadata.read(this) as KotlinClassMetadata.Class).kmClass diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorkSet.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorkSet.kt index de66f5cc..0bcaf151 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorkSet.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorkSet.kt @@ -8,6 +8,10 @@ import com.schwarz.crystalprocessor.model.entity.SchemaClassHolder import com.schwarz.crystalprocessor.model.entity.WrapperEntityHolder import com.schwarz.crystalprocessor.model.source.ReducedSourceModel import com.schwarz.crystalprocessor.model.source.SourceModel +import com.schwarz.crystalprocessor.model.typeconverter.ImportedTypeConverterHolder +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterExporterHolder +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolder +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderFactory import com.schwarz.crystalprocessor.processing.WorkSet import com.schwarz.crystalprocessor.validation.model.ModelValidation import com.schwarz.crystalprocessor.validation.model.PreModelValidation @@ -18,7 +22,10 @@ class ModelWorkSet( val allEntityElements: Set, val allWrapperElements: Set, val allSchemaClassElements: Set, - val allBaseModelElements: Set + val allBaseModelElements: Set, + val allTypeConverterElements: Set, + val allTypeConverterExporterElements: Set, + val allTypeConverterImporterElements: Set ) : WorkSet { @@ -30,42 +37,83 @@ class ModelWorkSet( private val baseModels: MutableMap = HashMap() + private val typeConverterModels: MutableMap = HashMap() + + private val typeConverterExporterModels: MutableMap = + HashMap() + + private val importedTypeConverterModels: MutableList = + mutableListOf() + override fun preValidate(logger: Logger) { - for (element in hashSetOf(*allBaseModelElements.toTypedArray(), *allEntityElements.toTypedArray(), *allWrapperElements.toTypedArray())) { + for (element in hashSetOf( + *allBaseModelElements.toTypedArray(), + *allEntityElements.toTypedArray(), + *allWrapperElements.toTypedArray() + )) { PreModelValidation.validate(element, logger) } + allTypeConverterElements.forEach { + PreModelValidation.validateTypeConverter(it, logger) + } + allTypeConverterExporterElements.forEach { + PreModelValidation.validateTypeConverterExporter(it, logger) + } + allTypeConverterImporterElements.forEach { + PreModelValidation.validateTypeConverterImporter(it, logger) + } } override fun loadModels(logger: Logger, env: ProcessingEnvironment) { val allWrapperPaths = allWrapperElements.map { element -> element.toString() } for (element in allBaseModelElements) { - val baseModel = EntityFactory.createBaseModelHolder(SourceModel(element), allWrapperPaths) + val baseModel = + EntityFactory.createBaseModelHolder(SourceModel(element), allWrapperPaths) baseModels[element.toString()] = baseModel } // we can resolve the based on chain when all base models are parsed. for (baseModel in baseModels.values) { - EntityFactory.addBasedOn(baseModel.sourceElement!!, baseModels, baseModel) + EntityFactory.addBasedOn(baseModel.sourceElement, baseModels, baseModel) } for (element in allEntityElements) { - val entityModel = EntityFactory.createEntityHolder(SourceModel(element), allWrapperPaths, baseModels) + val entityModel = + EntityFactory.createEntityHolder(SourceModel(element), allWrapperPaths, baseModels) entityModels[element.toString()] = entityModel entityModel.reducesModels.forEach { - val reduced = EntityFactory.createEntityHolder(ReducedSourceModel(entityModel.sourceElement, it), allWrapperPaths, baseModels) + val reduced = EntityFactory.createEntityHolder( + ReducedSourceModel( + entityModel.sourceElement, + it + ), + allWrapperPaths, + baseModels + ) reduced.isReduced = true entityModels[reduced.entitySimpleName] = reduced } } for (element in allWrapperElements) { - val wrapperModel = EntityFactory.createChildEntityHolder(SourceModel(element), allWrapperPaths, baseModels) + val wrapperModel = EntityFactory.createChildEntityHolder( + SourceModel(element), + allWrapperPaths, + baseModels + ) wrapperModels[element.toString()] = wrapperModel wrapperModel.reducesModels.forEach { - val reduced = EntityFactory.createEntityHolder(ReducedSourceModel(wrapperModel.sourceElement, it), allWrapperPaths, baseModels) + val reduced = EntityFactory.createEntityHolder( + ReducedSourceModel( + wrapperModel.sourceElement, + it + ), + allWrapperPaths, + baseModels + ) reduced.isReduced = true entityModels[reduced.entitySimpleName] = reduced } @@ -73,11 +121,36 @@ class ModelWorkSet( val allSchemaClassPaths = allSchemaClassElements.map { element -> element.toString() } for (element in allSchemaClassElements) { - val schemaModel = EntityFactory.createSchemaEntityHolder(SourceModel(element), allSchemaClassPaths, baseModels) + val schemaModel = EntityFactory.createSchemaEntityHolder( + SourceModel(element), + allSchemaClassPaths, + baseModels + ) schemaModels[element.toString()] = schemaModel } - ModelValidation(logger, baseModels, wrapperModels, entityModels).postValidate() + allTypeConverterImporterElements.forEach { element -> + importedTypeConverterModels.addAll(TypeConverterHolderFactory.importedTypeConverterHolders(element)) + } + + allTypeConverterElements.forEach { + val typeConverterHolder = TypeConverterHolderFactory.typeConverterHolder(it) + typeConverterModels[it.toString()] = typeConverterHolder + } + + allTypeConverterExporterElements.forEach { + val typeConverterExporterHolder = TypeConverterExporterHolder(it) + typeConverterExporterModels[it.toString()] = typeConverterExporterHolder + } + + ModelValidation( + logger, + baseModels, + wrapperModels, + entityModels, + typeConverterModels.values.toList(), + importedTypeConverterModels + ).postValidate() } val entities: List @@ -94,4 +167,12 @@ class ModelWorkSet( val bases: List get() = baseModels.values.toList() + + val typeConverters: List + get() = typeConverterModels.values.toList() + + val importedTypeConverters: List get() = importedTypeConverterModels + + val typeConverterExporters: List + get() = typeConverterExporterModels.values.toList() } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorker.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorker.kt index f6ed39a6..773e62b1 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorker.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorker.kt @@ -2,8 +2,11 @@ package com.schwarz.crystalprocessor.processing.model import com.schwarz.crystalapi.BaseModel import com.schwarz.crystalapi.Entity -import com.schwarz.crystalapi.SchemaClass import com.schwarz.crystalapi.MapWrapper +import com.schwarz.crystalapi.SchemaClass +import com.schwarz.crystalapi.TypeConverter +import com.schwarz.crystalapi.TypeConverterExporter +import com.schwarz.crystalapi.TypeConverterImporter import com.schwarz.crystalprocessor.CoachBaseBinderProcessor import com.schwarz.crystalprocessor.Logger import com.schwarz.crystalprocessor.documentation.DocumentationGenerator @@ -12,11 +15,15 @@ import com.schwarz.crystalprocessor.generation.CodeGenerator import com.schwarz.crystalprocessor.generation.model.CommonInterfaceGeneration import com.schwarz.crystalprocessor.generation.model.EntityGeneration import com.schwarz.crystalprocessor.generation.model.SchemaGeneration +import com.schwarz.crystalprocessor.generation.model.TypeConverterExporterObjectGeneration +import com.schwarz.crystalprocessor.generation.model.TypeConverterObjectGeneration import com.schwarz.crystalprocessor.generation.model.WrapperGeneration import com.schwarz.crystalprocessor.meta.SchemaGenerator import com.schwarz.crystalprocessor.model.entity.BaseEntityHolder +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderForEntityGeneration import com.schwarz.crystalprocessor.processing.Worker import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.TypeName import javax.annotation.processing.ProcessingEnvironment import javax.annotation.processing.RoundEnvironment @@ -47,21 +54,31 @@ class ModelWorker(override val logger: Logger, override val codeGenerator: CodeG } override fun doWork(workSet: ModelWorkSet, useSuspend: Boolean) { + val typeConvertersByConvertedClass: Map = (workSet.typeConverters + workSet.importedTypeConverters).associateBy { it.domainClassTypeName } + + workSet.typeConverters.forEach { + codeGenerator.generate(TypeConverterObjectGeneration.generateTypeConverterObject(it), processingEnv) + } + + workSet.typeConverterExporters.forEach { + codeGenerator.generate(TypeConverterExporterObjectGeneration.generateTypeConverterExporterObject(it, workSet.typeConverters), processingEnv) + } + val generatedInterfaces = mutableSetOf() for (baseModelHolder in workSet.bases) { - generateInterface(generatedInterfaces, baseModelHolder, useSuspend) + generateInterface(generatedInterfaces, baseModelHolder, useSuspend, typeConvertersByConvertedClass) } - process(workSet.entities, generatedInterfaces, useSuspend) { - EntityGeneration().generateModel(it, useSuspend) + process(workSet.entities, generatedInterfaces, useSuspend, typeConvertersByConvertedClass) { + EntityGeneration().generateModel(it, useSuspend, typeConvertersByConvertedClass) } - process(workSet.wrappers, generatedInterfaces, useSuspend) { - WrapperGeneration().generateModel(it, useSuspend) + process(workSet.wrappers, generatedInterfaces, useSuspend, typeConvertersByConvertedClass) { + WrapperGeneration().generateModel(it, useSuspend, typeConvertersByConvertedClass) } - process(workSet.schemas, generatedInterfaces, useSuspend) { + process(workSet.schemas, generatedInterfaces, useSuspend, typeConvertersByConvertedClass) { SchemaGeneration().generateModel(it, workSet.schemaClassPaths) } @@ -70,13 +87,19 @@ class ModelWorker(override val logger: Logger, override val codeGenerator: CodeG schemaGenerator?.generate() } - private fun process(models: List, generatedInterfaces: MutableSet, useSuspend: Boolean, generate: (T) -> FileSpec) { + private fun process( + models: List, + generatedInterfaces: MutableSet, + useSuspend: Boolean, + typeConvertersByConvertedClass: Map, + generate: (T) -> FileSpec + ) { for (model in models) { - generateInterface(generatedInterfaces, model, useSuspend) + generateInterface(generatedInterfaces, model, useSuspend, typeConvertersByConvertedClass) documentationGenerator?.addEntitySegments(model) schemaGenerator?.addEntity(model) entityRelationshipGenerator?.addEntityNodes(model) - generate(model)?.apply { + generate(model).apply { codeGenerator.generate(this, processingEnv) } } @@ -85,10 +108,11 @@ class ModelWorker(override val logger: Logger, override val codeGenerator: CodeG private fun generateInterface( generatedInterfaces: MutableSet, holder: BaseEntityHolder, - useSuspend: Boolean + useSuspend: Boolean, + typeConvertersByConvertedClass: Map ) { if (generatedInterfaces.contains(holder.sourceClazzSimpleName).not()) { - codeGenerator.generate(CommonInterfaceGeneration().generateModel(holder, useSuspend), processingEnv) + codeGenerator.generate(CommonInterfaceGeneration().generateModel(holder, useSuspend, typeConvertersByConvertedClass), processingEnv) generatedInterfaces.add(holder.sourceClazzSimpleName) } } @@ -97,6 +121,9 @@ class ModelWorker(override val logger: Logger, override val codeGenerator: CodeG allEntityElements = roundEnv.getElementsAnnotatedWith(Entity::class.java), allWrapperElements = roundEnv.getElementsAnnotatedWith(MapWrapper::class.java), allSchemaClassElements = roundEnv.getElementsAnnotatedWith(SchemaClass::class.java), - allBaseModelElements = roundEnv.getElementsAnnotatedWith(BaseModel::class.java) + allBaseModelElements = roundEnv.getElementsAnnotatedWith(BaseModel::class.java), + allTypeConverterElements = roundEnv.getElementsAnnotatedWith(TypeConverter::class.java), + allTypeConverterExporterElements = roundEnv.getElementsAnnotatedWith(TypeConverterExporter::class.java), + allTypeConverterImporterElements = roundEnv.getElementsAnnotatedWith(TypeConverterImporter::class.java) ) } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/util/FieldExtractionUtil.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/util/FieldExtractionUtil.kt index ee42dc8b..359b0d40 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/util/FieldExtractionUtil.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/util/FieldExtractionUtil.kt @@ -5,6 +5,7 @@ import javax.lang.model.type.MirroredTypeException import javax.lang.model.type.TypeMirror import com.schwarz.crystalapi.Field +import com.schwarz.crystalapi.TypeConverterImporter import com.schwarz.crystalapi.mapify.Mapifyable import com.schwarz.crystalapi.deprecated.Deprecated import javax.lang.model.type.MirroredTypesException @@ -56,4 +57,18 @@ object FieldExtractionUtil { return result } + + fun typeMirror(annotation: TypeConverterImporter): List { + val result = mutableListOf() + + try { + if (annotation.typeConverterExporter != null) { + throw Exception("Expected to get a MirroredTypesException") + } + } catch (mte: MirroredTypesException) { + result.addAll(mte.typeMirrors) + } + + return result + } } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/validation/model/ModelValidation.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/validation/model/ModelValidation.kt index 09b0a607..08d6716d 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/validation/model/ModelValidation.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/validation/model/ModelValidation.kt @@ -6,15 +6,38 @@ import com.schwarz.crystalprocessor.model.entity.BaseModelHolder import com.schwarz.crystalprocessor.model.entity.EntityHolder import com.schwarz.crystalprocessor.model.entity.WrapperEntityHolder import com.schwarz.crystalapi.Reduce +import com.schwarz.crystalapi.TypeConverter import com.schwarz.crystalapi.deprecated.DeprecatedField - -class ModelValidation(val logger: Logger, val baseModels: MutableMap, val wrapperModels: MutableMap, val entityModels: MutableMap) { +import com.schwarz.crystalprocessor.model.typeconverter.ImportedTypeConverterHolder +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolder +import com.schwarz.crystalprocessor.model.typeconverter.TypeConverterHolderForEntityGeneration +import com.schwarz.crystalprocessor.model.typeconverter.nonConvertibleClassesTypeNames +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.ParameterizedTypeName + +class ModelValidation( + val logger: Logger, + val baseModels: MutableMap, + val wrapperModels: MutableMap, + val entityModels: MutableMap, + val typeConverterModels: List, + val importedTypeConverterModels: List +) { + + private val allTypeConverterModels: List = + typeConverterModels + importedTypeConverterModels private fun validateQuery(baseEntityHolder: BaseEntityHolder) { for (query in baseEntityHolder.queries) { for (field in query.fields) { - if (!baseEntityHolder.fields.containsKey(field) && !baseEntityHolder.fieldConstants.containsKey(field)) { - baseEntityHolder.sourceElement.logError(logger, "query param [$field] is not a part of this entity") + if (!baseEntityHolder.fields.containsKey(field) && !baseEntityHolder.fieldConstants.containsKey( + field + ) + ) { + baseEntityHolder.sourceElement.logError( + logger, + "query param [$field] is not a part of this entity" + ) } } } @@ -23,40 +46,71 @@ class ModelValidation(val logger: Logger, val baseModels: MutableMap deprecated.replacedByTypeMirror?.toString()?.apply { - if (this != Void::class.java.canonicalName && !wrapperModels.containsKey(this) && !entityModels.containsKey(this)) { - baseEntityHolder.sourceElement.logError(logger, "replacement [$this] is not an entity/wrapper") + if (this != Void::class.java.canonicalName && !wrapperModels.containsKey(this) && !entityModels.containsKey( + this + ) + ) { + baseEntityHolder.sourceElement.logError( + logger, + "replacement [$this] is not an entity/wrapper" + ) } } - validateDeprecatedFields(deprecated.deprecatedFields, deprecated.replacedByTypeMirror?.toString(), baseEntityHolder) + validateDeprecatedFields( + deprecated.deprecatedFields, + deprecated.replacedByTypeMirror?.toString(), + baseEntityHolder + ) } } - private fun validateDeprecatedFields(deprecatedFields: Map, replacingModel: String?, model: BaseEntityHolder) { + private fun validateDeprecatedFields( + deprecatedFields: Map, + replacingModel: String?, + model: BaseEntityHolder + ) { val replacingModel: BaseEntityHolder? = replacingModel?.let { wrapperModels[it] ?: entityModels[it] } - val fieldsAccessorsDocId: List = model.docId?.distinctFieldAccessors(model) ?: emptyList() + val fieldsAccessorsDocId: List = + model.docId?.distinctFieldAccessors(model) ?: emptyList() for (field in deprecatedFields) { if (!model.fields.containsKey(field.key) && !model.fieldConstants.containsKey(field.key) && model.isReduced.not()) { - model.sourceElement.logError(logger, "replacement field [${field.key}] does not exists") + model.sourceElement.logError( + logger, + "replacement field [${field.key}] does not exists" + ) } field.value.replacedBy?.let { replacement -> if (replacement.isNotEmpty()) { - val replacingIncludedInModel = model.fields.containsKey(replacement) || model.fieldConstants.containsKey(replacement) - val replacementIncludedReplacingModel = replacingModel?.let { it.fields.containsKey(replacement) || it.fieldConstants?.containsKey(replacement) == true } + val replacingIncludedInModel = + model.fields.containsKey(replacement) || model.fieldConstants.containsKey( + replacement + ) + val replacementIncludedReplacingModel = replacingModel?.let { + it.fields.containsKey(replacement) || it.fieldConstants?.containsKey( + replacement + ) == true + } ?: false if (!replacingIncludedInModel && !replacementIncludedReplacingModel && model.isReduced.not()) { - model.sourceElement.logError(logger, "replacement [$replacement] for field [${field.key}] does not exists") + model.sourceElement.logError( + logger, + "replacement [$replacement] for field [${field.key}] does not exists" + ) } } } if (fieldsAccessorsDocId.contains(field.key)) { - model.sourceElement.logError(logger, "deprecated field is a part of DocId which is not possible since DocId is a final value. Use Deprecation on Entity/Wrapper level instead") + model.sourceElement.logError( + logger, + "deprecated field is a part of DocId which is not possible since DocId is a final value. Use Deprecation on Entity/Wrapper level instead" + ) } } } @@ -65,45 +119,114 @@ class ModelValidation(val logger: Logger, val baseModels: MutableMap + + val fieldType = fieldEntry.value.fieldType + val rawFieldType = (fieldType as? ParameterizedTypeName)?.rawType ?: fieldType + + if (allTypeConverterModels.all { it.domainClassTypeName != rawFieldType }) { + baseEntityHolder.sourceElement.logError( + logger, + "[${baseEntityHolder.entitySimpleName}] No ${TypeConverter::class.java.name} found for Type ${fieldEntry.value.fieldType}" + ) + } + } + } + + private fun validateTypeConversions() { + typeConverterModels.forEach { + if (!nonConvertibleClassesTypeNames.contains(it.mapClassTypeName)) { + logger.error("Invalid map type ${it.mapClassTypeName} found in TypeConverter ${it.classTypeName}. Should be one of $nonConvertibleClassesTypeNames", null) + } + } + + val typeConverterMap: MutableMap = + importedTypeConverterModels.associateBy { it.domainClassTypeName }.toMutableMap() + + typeConverterModels.forEach { + val existingTypeConverter = typeConverterMap[it.domainClassTypeName] + if (existingTypeConverter != null) { + logger.error( + "Duplicate TypeConverters for domain class ${it.domainClassTypeName}. " + + "Cannot add ${it.classTypeName} since already defined by ${existingTypeConverter.instanceClassTypeName}", + null + ) + } else { + typeConverterMap.put(it.domainClassTypeName, it) + } + } + } + fun postValidate(): Boolean { + validateTypeConversions() + for (wrapper in wrapperModels) { validateQuery(wrapper.value) validateDeprecated(wrapper.value) validateDocId(wrapper.value) validateReduces(wrapper.value) + validateTypeConversionTypes(wrapper.value) } for (entity in entityModels) { @@ -111,6 +234,7 @@ class ModelValidation(val logger: Logger, val baseModels: MutableMap diagnostic.getMessage(Locale.GERMAN).equals("Entity should not have a contructor"))); + Assert.assertTrue(compilation.diagnostics().stream().anyMatch(diagnostic -> diagnostic.getMessage(Locale.GERMAN).equals("Entity should not have a constructor"))); } - - - } diff --git a/crystal-map-processor/src/test/java/com/schwarz/CouchbaseBaseBinderProcessorKotlinTest.kt b/crystal-map-processor/src/test/java/com/schwarz/CouchbaseBaseBinderProcessorKotlinTest.kt index cbbc7857..d93cc2ba 100644 --- a/crystal-map-processor/src/test/java/com/schwarz/CouchbaseBaseBinderProcessorKotlinTest.kt +++ b/crystal-map-processor/src/test/java/com/schwarz/CouchbaseBaseBinderProcessorKotlinTest.kt @@ -1,5 +1,7 @@ package com.schwarz +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.TypeConverter import com.schwarz.crystalprocessor.CoachBaseBinderProcessor import com.schwarz.testdata.TestDataHelper import com.tschuchort.compiletesting.JvmCompilationResult @@ -22,21 +24,24 @@ class CouchbaseBaseBinderProcessorKotlinTest { @Test fun testSuccessSimpleReduce() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithSimpleReduce")) + val compilation = + compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithSimpleReduce")) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @Test fun testSuccessMapperWithGetterAndSetter() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("MapperWithGetterAndSetter")) + val compilation = + compileKotlin(TestDataHelper.clazzAsJavaFileObjects("MapperWithGetterAndSetter")) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @Test fun testSuccessMapperWithTypeParam() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("MapperWithTypeParam")) + val compilation = + compileKotlin(TestDataHelper.clazzAsJavaFileObjects("MapperWithTypeParam")) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @@ -57,66 +62,92 @@ class CouchbaseBaseBinderProcessorKotlinTest { @Test fun testSucessWithQueriesAndEnums() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithQueriesAndEnums")) + val compilation = + compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithQueriesAndEnums")) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @Test fun testSuccessWithGenerateAccessor() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithGenerateAccessor")) + val compilation = + compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithGenerateAccessor")) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @Test fun testSuccessWithQueriesAndSuspendFunctions() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithQueries"), useSuspend = true) + val compilation = compileKotlin( + TestDataHelper.clazzAsJavaFileObjects("EntityWithQueries"), + useSuspend = true + ) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @Test fun testSuccessWithGenerateAccessorAndSuspendFunctions() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithGenerateAccessor"), useSuspend = true) + val compilation = compileKotlin( + TestDataHelper.clazzAsJavaFileObjects("EntityWithGenerateAccessor"), + useSuspend = true + ) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @Test fun testSuccessDeprecatedGeneration() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithDeprecatedFields"), useSuspend = true) + val compilation = compileKotlin( + TestDataHelper.clazzAsJavaFileObjects("EntityWithDeprecatedFields"), + useSuspend = true + ) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @Test fun testSuccessDeprecatedWithReduceGeneration() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithDeprecatedFieldsAndReduce"), useSuspend = true) + val compilation = compileKotlin( + TestDataHelper.clazzAsJavaFileObjects("EntityWithDeprecatedFieldsAndReduce"), + useSuspend = true + ) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @Test fun testSuccessDocIdGeneration() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithDocId"), useSuspend = true) + val compilation = compileKotlin( + TestDataHelper.clazzAsJavaFileObjects("EntityWithDocId"), + useSuspend = true + ) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @Test fun testSuccessDocIdSegmentGeneration() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithDocIdAndDocIdSegments"), useSuspend = true) + val compilation = compileKotlin( + TestDataHelper.clazzAsJavaFileObjects("EntityWithDocIdAndDocIdSegments"), + useSuspend = true + ) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @Test fun testFailedWrongDeprecatedGeneration() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithWrongConfiguredDeprecatedFields"), useSuspend = true) + val compilation = compileKotlin( + TestDataHelper.clazzAsJavaFileObjects("EntityWithWrongConfiguredDeprecatedFields"), + useSuspend = true + ) Assert.assertEquals(compilation.exitCode, KotlinCompilation.ExitCode.COMPILATION_ERROR) Assert.assertTrue(compilation.messages.contains("replacement [name2] for field [name] does not exists")) } @Test fun testSuccessDeprecatedClassGeneration() { - val compilation = compileKotlin(TestDataHelper.clazzAsJavaFileObjects("EntityWithDeprecatedClass"), useSuspend = true) + val compilation = compileKotlin( + TestDataHelper.clazzAsJavaFileObjects("EntityWithDeprecatedClass"), + useSuspend = true + ) Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) } @@ -124,7 +155,8 @@ class CouchbaseBaseBinderProcessorKotlinTest { fun testKotlinAbstractGeneration() { val subEntity = SourceFile.kotlin( "Sub.kt", - ENTITY_HEADER + + PACKAGE_HEADER + + ENTITY_HEADER + "@Entity\n" + "@Fields(\n" + "Field(name = \"test\", type = String::class),\n" + @@ -149,7 +181,8 @@ class CouchbaseBaseBinderProcessorKotlinTest { fun testKotlinAbstractGenerationWithLongFields() { val subEntity = SourceFile.kotlin( "Sub.kt", - ENTITY_HEADER + + PACKAGE_HEADER + + ENTITY_HEADER + "@Entity\n" + "@Fields(\n" + "Field(name = \"test_test_test\", type = String::class),\n" + @@ -210,7 +243,8 @@ class CouchbaseBaseBinderProcessorKotlinTest { fun testKotlinPrivateGeneration() { val subEntity = SourceFile.kotlin( "Sub.kt", - ENTITY_HEADER + + PACKAGE_HEADER + + ENTITY_HEADER + "@Entity\n" + "@Fields(\n" + "Field(name = \"test\", type = String::class),\n" + @@ -235,7 +269,8 @@ class CouchbaseBaseBinderProcessorKotlinTest { fun testKotlinConstructorFailGeneration() { val subEntity = SourceFile.kotlin( "Sub.kt", - ENTITY_HEADER + + PACKAGE_HEADER + + ENTITY_HEADER + "@Entity\n" + "@Fields(\n" + "Field(name = \"test\", type = String::class),\n" + @@ -252,10 +287,103 @@ class CouchbaseBaseBinderProcessorKotlinTest { val compilation = compileKotlin(subEntity) Assert.assertEquals(compilation.exitCode, KotlinCompilation.ExitCode.COMPILATION_ERROR) - Assert.assertTrue(compilation.messages.contains("Entity should not have a contructor")) + Assert.assertTrue(compilation.messages.contains("Entity should not have a constructor")) } - private fun compileKotlin(vararg sourceFiles: SourceFile, useSuspend: Boolean = false): JvmCompilationResult { + @Test + fun testTypeConverterGeneration() { + val expected = File("src/test/resources/ExpectedTypeConverter.txt").readLines() + val typeConverter = SourceFile.kotlin( + "DateTypeConverter.kt", + PACKAGE_HEADER + + TYPE_CONVERTER_HEADER + + "import java.time.OffsetDateTime\n" + + "@TypeConverter\n" + + "abstract class DateTypeConverter : ITypeConverter {\n" + + "override fun write(value: OffsetDateTime?): String? = value?.toString()\n" + + "override fun read(value: String?): OffsetDateTime? = value?.let { OffsetDateTime.parse(it) }\n" + + "}" + ) + + val compilation = compileKotlin(typeConverter) + + Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) + val actual = compilation.generatedFiles.find { it.name == "DateTypeConverterInstance.kt" } + ?.readLines() + Assert.assertEquals(expected, actual) + } + + @Test + fun testTypeConverterFinalClass() { + val typeConverter = SourceFile.kotlin( + "DateTypeConverter.kt", + PACKAGE_HEADER + + TYPE_CONVERTER_HEADER + + "import java.time.OffsetDateTime\n" + + "@TypeConverter\n" + + "class DateTypeConverter : ITypeConverter {\n" + + "override fun write(value: OffsetDateTime?): String? = value?.toString()\n" + + "override fun read(value: String?): OffsetDateTime? = value?.let { OffsetDateTime.parse(it) }\n" + + "}" + ) + + val compilation = compileKotlin(typeConverter) + + Assert.assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, compilation.exitCode) + Assert.assertTrue(compilation.messages.contains("TypeConverter can not be final")) + } + + @Test + fun testTypeConverterImplementsInterface() { + val typeConverter = SourceFile.kotlin( + "DateTypeConverter.kt", + PACKAGE_HEADER + + TYPE_CONVERTER_HEADER + + "import java.time.OffsetDateTime\n" + + "@TypeConverter\n" + + "open class DateTypeConverter {\n" + + "fun write(value: OffsetDateTime?): String? = value?.toString()\n" + + "fun read(value: String?): OffsetDateTime? = value?.let { OffsetDateTime.parse(it) }\n" + + "}" + ) + + val compilation = compileKotlin(typeConverter) + + Assert.assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, compilation.exitCode) + Assert.assertTrue(compilation.messages.contains("Class annotated with ${TypeConverter::class.simpleName} must implement the ${ITypeConverter::class.simpleName} interface")) + } + + @Test + fun testTypeConverterExporterGeneration() { + val expected = File("src/test/resources/ExpectedTypeConverterExporter.txt").readLines().map { it.trim() } + val sourceFileContents = PACKAGE_HEADER + + TYPE_CONVERTER_EXPORTER_HEADER + + TYPE_CONVERTER_HEADER + + "import java.time.OffsetDateTime\n" + + "@TypeConverter\n" + + "abstract class DateTypeConverter : ITypeConverter {\n" + + "override fun write(value: OffsetDateTime?): String? = value?.toString()\n" + + "override fun read(value: String?): OffsetDateTime? = value?.let { OffsetDateTime.parse(it) }\n" + + "}\n" + + "@TypeConverterExporter\n" + + "interface TestTypeConverters" + val typeConverter = SourceFile.kotlin( + "TestTypeConverters.kt", + sourceFileContents + ) + + val compilation = compileKotlin(typeConverter) + + Assert.assertEquals(KotlinCompilation.ExitCode.OK, compilation.exitCode) + val actual = compilation.generatedFiles.find { it.name == "TestTypeConvertersInstance.kt" } + ?.readLines()?.map { it.trim() } + Assert.assertEquals(expected, actual) + } + + private fun compileKotlin( + vararg sourceFiles: SourceFile, + useSuspend: Boolean = false + ): JvmCompilationResult { return KotlinCompilation().apply { sources = sourceFiles.toList() @@ -271,11 +399,28 @@ class CouchbaseBaseBinderProcessorKotlinTest { } companion object { - const val ENTITY_HEADER: String = + + const val PACKAGE_HEADER: String = "package com.kaufland.testModels\n" + - "\n" + - "import com.schwarz.crystalapi.Entity\n" + + "\n" + + const val ENTITY_HEADER: String = + "import com.schwarz.crystalapi.Entity\n" + "import com.schwarz.crystalapi.Field\n" + "import com.schwarz.crystalapi.Fields\n" + + const val TYPE_CONVERTER_HEADER: String = + "import com.schwarz.crystalapi.ITypeConverter\n" + + "import com.schwarz.crystalapi.TypeConverter\n" + + const val TYPE_CONVERTER_EXPORTER_HEADER: String = + "import com.schwarz.crystalapi.ITypeConverterExporter\n" + + "import com.schwarz.crystalapi.TypeConverterExporter\n" + + const val TYPE_CONVERTER_IMPORTER_HEADER: String = + "package com.kaufland.testModels\n" + + "\n" + + "import com.schwarz.crystalapi.ITypeConverterImporter\n" + + "import com.schwarz.crystalapi.TypeConverterImporter\n" } } diff --git a/crystal-map-processor/src/test/resources/EntityWithQueriesAndEnums.kt b/crystal-map-processor/src/test/resources/EntityWithQueriesAndEnums.kt index a5d2122a..b53ab241 100644 --- a/crystal-map-processor/src/test/resources/EntityWithQueriesAndEnums.kt +++ b/crystal-map-processor/src/test/resources/EntityWithQueriesAndEnums.kt @@ -1,15 +1,21 @@ import com.schwarz.crystalapi.Entity import com.schwarz.crystalapi.Field import com.schwarz.crystalapi.Fields +import com.schwarz.crystalapi.ITypeConverter import com.schwarz.crystalapi.MapWrapper +import com.schwarz.crystalapi.TypeConverter import com.schwarz.crystalapi.query.Queries import com.schwarz.crystalapi.query.Query +import com.schwarz.crystalapi.typeconverters.EnumConverter enum class TestEnum { TEST_VALUE1, TEST_VALUE2 } +@TypeConverter +open class TestEnumConverter: ITypeConverter by EnumConverter(TestEnum::class) + @Entity(database = "mydb_db") @MapWrapper @Fields( diff --git a/crystal-map-processor/src/test/resources/ExpectedSchema.txt b/crystal-map-processor/src/test/resources/ExpectedSchema.txt index 5e0eb2d7..30ea26d2 100644 --- a/crystal-map-processor/src/test/resources/ExpectedSchema.txt +++ b/crystal-map-processor/src/test/resources/ExpectedSchema.txt @@ -13,24 +13,24 @@ import com.schwarz.crystalapi.schema.Schema import kotlin.Number import kotlin.String -open class SubSchema( - path: String = "" +public open class SubSchema( + path: String = "", ) : Schema { - val DEFAULT_TYPE: String = "test" + public val DEFAULT_TYPE: String = "test" - val type: CMField = CMField("type", path) + public val type: CMField = CMField("type", path) - val test_test_test: CMField = CMField("test_test_test", path) + public val test_test_test: CMField = CMField("test_test_test", path) - val list: CMList = CMList("list", path) + public val list: CMList = CMList("list", path) - val someObject: CMObject = CMObject( + public val someObject: CMObject = CMObject( com.kaufland.testModels.TestObjectSchema(if (path.isBlank()) "someObject" else "$path.someObject"), path, ) - val objects: CMObjectList = CMObjectList( + public val objects: CMObjectList = CMObjectList( com.kaufland.testModels.TestObjectSchema(if (path.isBlank()) "objects" else "$path.objects"), "objects", diff --git a/crystal-map-processor/src/test/resources/ExpectedTypeConverter.txt b/crystal-map-processor/src/test/resources/ExpectedTypeConverter.txt new file mode 100644 index 00000000..de153b77 --- /dev/null +++ b/crystal-map-processor/src/test/resources/ExpectedTypeConverter.txt @@ -0,0 +1,8 @@ +// DO NOT EDIT THIS FILE. +// Generated using Crystal-Map +// +// Do not edit this class!!!!. +// +package com.kaufland.testModels + +public object DateTypeConverterInstance : DateTypeConverter() \ No newline at end of file diff --git a/crystal-map-processor/src/test/resources/ExpectedTypeConverterExporter.txt b/crystal-map-processor/src/test/resources/ExpectedTypeConverterExporter.txt new file mode 100644 index 00000000..24612f89 --- /dev/null +++ b/crystal-map-processor/src/test/resources/ExpectedTypeConverterExporter.txt @@ -0,0 +1,31 @@ +// DO NOT EDIT THIS FILE. +// Generated using Crystal-Map +// +// Do not edit this class!!!!. +// +package com.kaufland.testModels + +import com.schwarz.crystalapi.ClassNameDefinition +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.ITypeConverterExporter +import com.schwarz.crystalapi.TypeConverterImportable +import java.time.OffsetDateTime +import kotlin.collections.List +import kotlin.collections.Map +import kotlin.reflect.KClass + +public class TestTypeConvertersInstance : TestTypeConverters, ITypeConverterExporter { + override val typeConverters: Map, ITypeConverter<*, *>> + get() = mapOf( + OffsetDateTime::class to DateTypeConverterInstance, + ) + + override val typeConverterImportables: List + get() = listOf( + TypeConverterImportable( + ClassNameDefinition("com.kaufland.testModels", "DateTypeConverterInstance"), + ClassNameDefinition("java.time", "OffsetDateTime"), + ClassNameDefinition("kotlin", "String") + ), + ) +} \ No newline at end of file diff --git a/demo/build.gradle b/demo/build.gradle index b864e720..cc7b965e 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -92,6 +92,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation project(path: ':crystal-map-api') implementation project(path: ':crystal-map-couchbase-connector') + implementation project(path: ':dummy') kapt project(path: ':crystal-map-processor') compileOnly project(path: ':crystal-map-processor') implementation 'com.couchbase.lite:couchbase-lite-android:2.1.2' diff --git a/demo/src/main/java/com/schwarz/crystaldemo/Application.kt b/demo/src/main/java/com/schwarz/crystaldemo/Application.kt index 2344f897..6a66efd1 100755 --- a/demo/src/main/java/com/schwarz/crystaldemo/Application.kt +++ b/demo/src/main/java/com/schwarz/crystaldemo/Application.kt @@ -7,14 +7,10 @@ import com.couchbase.lite.Database import com.couchbase.lite.DatabaseConfiguration import com.schwarz.crystalapi.PersistenceConfig import com.schwarz.crystalapi.PersistenceException -import com.schwarz.crystalapi.TypeConversion import com.schwarz.crystalapi.TypeConversionErrorWrapper import com.schwarz.crystalcouchbaseconnector.Couchbase2Connector -import com.schwarz.crystaldemo.customtypes.GenerateClassName -import com.schwarz.crystaldemo.customtypes.GenerateClassNameConversion import com.schwarz.crystaldemo.entity.ProductEntity import com.schwarz.crystaldemo.entity.UserCommentWrapper -import kotlin.reflect.KClass class Application : android.app.Application() { @@ -46,14 +42,6 @@ class Application : android.app.Application() { throw RuntimeException("wrong db name defined!!") } - override val typeConversions: Map, TypeConversion> - get() { - val mutableMapOf = - mutableMapOf, TypeConversion>(GenerateClassName::class to GenerateClassNameConversion()) - mutableMapOf.putAll(super.typeConversions) - return mutableMapOf - } - override fun invokeOnError(errorWrapper: TypeConversionErrorWrapper) { if (errorWrapper.exception is java.lang.ClassCastException) { Log.e( diff --git a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/BlobConverter.kt b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/BlobConverter.kt new file mode 100644 index 00000000..1d62f338 --- /dev/null +++ b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/BlobConverter.kt @@ -0,0 +1,16 @@ +package com.schwarz.crystaldemo.customtypes + +import com.couchbase.lite.Blob +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.TypeConverter + +@TypeConverter +abstract class BlobConverter : ITypeConverter { + override fun write(value: Blob?): String? { + TODO("Not yet implemented") + } + + override fun read(value: String?): Blob? { + TODO("Not yet implemented") + } +} diff --git a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/CaptureStateConverter.kt b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/CaptureStateConverter.kt new file mode 100644 index 00000000..5d043247 --- /dev/null +++ b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/CaptureStateConverter.kt @@ -0,0 +1,9 @@ +package com.schwarz.crystaldemo.customtypes + +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.TypeConverter +import com.schwarz.crystalapi.typeconverters.EnumConverter +import schwarz.fwws.shared.model.CaptureState + +@TypeConverter +abstract class CaptureStateConverter : ITypeConverter by EnumConverter(CaptureState::class) diff --git a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/DoubleConverter.kt b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/DoubleConverter.kt new file mode 100644 index 00000000..26ffeb28 --- /dev/null +++ b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/DoubleConverter.kt @@ -0,0 +1,11 @@ +package com.schwarz.crystaldemo.customtypes + +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.TypeConverter + +@TypeConverter +abstract class DoubleConverter : ITypeConverter { + override fun write(value: Double?): Number? = value + + override fun read(value: Number?): Double? = value?.toDouble() +} diff --git a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/DummyTypeConverterImporter.kt b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/DummyTypeConverterImporter.kt new file mode 100644 index 00000000..aa8263fa --- /dev/null +++ b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/DummyTypeConverterImporter.kt @@ -0,0 +1,6 @@ +package com.schwarz.crystaldemo.customtypes + +import com.schwarz.crystalapi.TypeConverterImporter + +@TypeConverterImporter(DummyTypeConvertersInstance::class) +interface DummyTypeConverterImporter diff --git a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/GenerateClassNameConversion.kt b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/GenerateClassNameConversion.kt deleted file mode 100644 index cacf1d19..00000000 --- a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/GenerateClassNameConversion.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.schwarz.crystaldemo.customtypes - -import com.schwarz.crystalapi.TypeConversion - -class GenerateClassNameConversion : TypeConversion { - - override fun write(value: Any?): Any? { - return when (value) { - is GenerateClassName -> { - value.toString() - } - else -> value - } - } - - override fun read(value: Any?): GenerateClassName = - when (value) { - is String -> { - GenerateClassName(value) - } - else -> GenerateClassName() - } -} diff --git a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/GenerateClassNameConverter.kt b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/GenerateClassNameConverter.kt new file mode 100644 index 00000000..14e8f4ff --- /dev/null +++ b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/GenerateClassNameConverter.kt @@ -0,0 +1,12 @@ +package com.schwarz.crystaldemo.customtypes + +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.TypeConverter + +@TypeConverter +abstract class GenerateClassNameConverter : ITypeConverter { + + override fun write(value: GenerateClassName?): String? = value?.toString() + + override fun read(value: String?): GenerateClassName? = value?.let { GenerateClassName(it) } +} diff --git a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/IntConverter.kt b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/IntConverter.kt new file mode 100644 index 00000000..74a411b4 --- /dev/null +++ b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/IntConverter.kt @@ -0,0 +1,10 @@ +package com.schwarz.crystaldemo.customtypes + +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.TypeConverter + +@TypeConverter +abstract class IntConverter : ITypeConverter { + override fun write(value: Int?): Number? = value + override fun read(value: Number?): Int? = value?.toInt() +} diff --git a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/LongConverter.kt b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/LongConverter.kt new file mode 100644 index 00000000..b6fbdb47 --- /dev/null +++ b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/LongConverter.kt @@ -0,0 +1,11 @@ +package com.schwarz.crystaldemo.customtypes + +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.TypeConverter + +@TypeConverter +abstract class LongConverter : ITypeConverter { + override fun write(value: Long?): Number? = value + + override fun read(value: Number?): Long? = value?.toLong() +} diff --git a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/OffsetDateTimeConverter.kt b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/OffsetDateTimeConverter.kt new file mode 100644 index 00000000..211896da --- /dev/null +++ b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/OffsetDateTimeConverter.kt @@ -0,0 +1,12 @@ +package com.schwarz.crystaldemo.customtypes + +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.TypeConverter +import java.time.OffsetDateTime + +@TypeConverter +open class OffsetDateTimeConverter : ITypeConverter { + override fun write(value: OffsetDateTime?): String? = value?.toString() + + override fun read(value: String?): OffsetDateTime? = value?.let { OffsetDateTime.parse(it) } +} diff --git a/demo/src/main/java/com/schwarz/crystaldemo/customtypes/ProductCategoryConverter.kt b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/ProductCategoryConverter.kt new file mode 100644 index 00000000..0fb69773 --- /dev/null +++ b/demo/src/main/java/com/schwarz/crystaldemo/customtypes/ProductCategoryConverter.kt @@ -0,0 +1,9 @@ +package com.schwarz.crystaldemo.customtypes + +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.TypeConverter +import com.schwarz.crystalapi.typeconverters.EnumConverter +import com.schwarz.crystaldemo.entity.ProductCategory + +@TypeConverter +abstract class ProductCategoryConverter : ITypeConverter by EnumConverter(ProductCategory::class) 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 16106e9c..708bdded 100644 --- a/demo/src/main/java/com/schwarz/crystaldemo/entity/Product.kt +++ b/demo/src/main/java/com/schwarz/crystaldemo/entity/Product.kt @@ -13,6 +13,7 @@ import com.schwarz.crystalapi.Reduce import com.schwarz.crystalapi.Reduces import com.schwarz.crystalapi.query.Queries import com.schwarz.crystalapi.query.Query +import java.util.Date @Entity(database = "mydb_db") @MapWrapper @@ -40,7 +41,8 @@ import com.schwarz.crystalapi.query.Query ), Field(name = "image", type = Blob::class), Field(name = "identifiers", type = String::class, list = true), - Field(name = "category", type = ProductCategory::class) + Field(name = "category", type = ProductCategory::class), + Field(name = "some_date", type = Date::class) ) @Queries( Query(fields = ["type"]), diff --git a/demo/src/main/java/com/schwarz/crystaldemo/entity/ProductCategory.kt b/demo/src/main/java/com/schwarz/crystaldemo/entity/ProductCategory.kt index 3ecebba2..c6e71e31 100644 --- a/demo/src/main/java/com/schwarz/crystaldemo/entity/ProductCategory.kt +++ b/demo/src/main/java/com/schwarz/crystaldemo/entity/ProductCategory.kt @@ -1,26 +1,6 @@ package com.schwarz.crystaldemo.entity -import com.schwarz.crystalapi.TypeConversion - enum class ProductCategory { GREAT_PRODUCT, AMAZING_PRODUCT } - -object ProductCategoryTypeConversion : TypeConversion { - override fun write(value: Any?): Any? = value?.let { - when (it) { - is String -> it - is ProductCategory -> it.name - else -> null - } - } - - override fun read(value: Any?): Any? = value.let { - when (it) { - is String -> ProductCategory.valueOf(it) - is ProductCategory -> it - else -> null - } - } -} diff --git a/demo/src/main/java/com/schwarz/crystaldemo/tesst/Task.kt b/demo/src/main/java/com/schwarz/crystaldemo/tesst/Task.kt index d7df5e65..fa683b61 100644 --- a/demo/src/main/java/com/schwarz/crystaldemo/tesst/Task.kt +++ b/demo/src/main/java/com/schwarz/crystaldemo/tesst/Task.kt @@ -34,6 +34,16 @@ open class Task { fun ultraComplexQuery(storeId: String): String { return "" } + + @GenerateAccessor + suspend fun suspendingUltraComplexQueryReturningList(storeId: String): List { + return listOf("") + } + + @GenerateAccessor(isNullableSuspendFun = true) + suspend fun suspendingUltraComplexQueryReturningNullableList(storeId: String): List? { + return null + } } // // override fun documentId(): String { diff --git a/demo/src/main/java/com/schwarz/crystaldemo/tesst/article/StoreArticle.kt b/demo/src/main/java/com/schwarz/crystaldemo/tesst/article/StoreArticle.kt index bb1ac796..acc50f9e 100644 --- a/demo/src/main/java/com/schwarz/crystaldemo/tesst/article/StoreArticle.kt +++ b/demo/src/main/java/com/schwarz/crystaldemo/tesst/article/StoreArticle.kt @@ -32,6 +32,7 @@ import java.util.* Field(name = "supplier", type = Supplier::class) ) @DocId("art:%storeId%:%article_no%") +@SchemaClass open class StoreArticle { companion object { const val PREFIX: String = "storearticle" diff --git a/demo/src/test/java/kaufland/com/demo/TypeConversionTest.kt b/demo/src/test/java/kaufland/com/demo/TypeConversionTest.kt deleted file mode 100644 index 7ee9d9d1..00000000 --- a/demo/src/test/java/kaufland/com/demo/TypeConversionTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.schwarz.crystaldemo - -import com.schwarz.crystalapi.PersistenceConfig -import com.schwarz.crystalapi.TypeConversion -import com.schwarz.crystalapi.TypeConversionErrorWrapper -import com.schwarz.crystaldemo.customtypes.GenerateClassName -import com.schwarz.crystaldemo.customtypes.GenerateClassNameConversion -import com.schwarz.crystaldemo.entity.TestClassEntity -import org.junit.Assert -import org.junit.Before -import org.junit.Test -import kotlin.reflect.KClass - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see [Testing documentation](http://d.android.com/tools/testing) - */ -class TypeConversionTest { - - @Before - fun init() { - PersistenceConfig.configure(object : PersistenceConfig.Connector { - - override val typeConversions: Map, TypeConversion> - get() = mutableMapOf, TypeConversion>(GenerateClassName::class to GenerateClassNameConversion()) - - override fun getDocument( - id: String, - dbName: String, - onlyInclude: List? - ): Map { - return emptyMap() - } - - override fun getDocuments( - ids: List, - dbName: String, - onlyInclude: List? - ): List?> { - TODO("Not yet implemented") - } - - override fun queryDoc( - dbName: String, - queryParams: Map, - limit: Int?, - onlyInclude: List? - ): List> { - throw Exception("Should not be called") - } - - override fun deleteDocument(id: String, dbName: String) { - throw Exception("should not called") - } - - override fun upsertDocument( - document: MutableMap, - id: String?, - dbName: String - ): Map { - throw Exception("should not called") - } - - override fun invokeOnError(errorWrapper: TypeConversionErrorWrapper) { - throw errorWrapper.exception - } - }) - } - - @Test - @Throws(Exception::class) - fun testCustomTypeConversion() { - val test = - mapOf(TestClassEntity.CLAZZ_NAME to TypeConversionTest::class.simpleName!!) - Assert.assertEquals( - TypeConversionTest::class.simpleName!!, - TestClassEntity(test).toMap()[TestClassEntity.CLAZZ_NAME] - ) - val testClassEntity1 = TestClassEntity.create() - Assert.assertEquals( - TestClassEntity::class.simpleName!!, - testClassEntity1.toMap()[TestClassEntity.CLAZZ_NAME] - ) - } -} diff --git a/demo/src/test/java/kaufland/com/demo/UnitTestConnector.kt b/demo/src/test/java/kaufland/com/demo/UnitTestConnector.kt index 23dd06b2..2dad7cce 100644 --- a/demo/src/test/java/kaufland/com/demo/UnitTestConnector.kt +++ b/demo/src/test/java/kaufland/com/demo/UnitTestConnector.kt @@ -1,13 +1,9 @@ package com.schwarz.crystaldemo import com.schwarz.crystalapi.PersistenceConfig -import com.schwarz.crystalapi.TypeConversion import com.schwarz.crystalapi.TypeConversionErrorWrapper -import kotlin.reflect.KClass -open class UnitTestConnector( - override val typeConversions: Map, TypeConversion> = emptyMap() -) : PersistenceConfig.Connector { +open class UnitTestConnector() : PersistenceConfig.Connector { override fun deleteDocument(id: String, dbName: String) { // Do Nothing } 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 f52d04f7..bdfd8c68 100644 --- a/demo/src/test/java/kaufland/com/demo/entity/ProductEntityTest.kt +++ b/demo/src/test/java/kaufland/com/demo/entity/ProductEntityTest.kt @@ -1,9 +1,7 @@ package com.schwarz.crystaldemo.entity import com.schwarz.crystalapi.PersistenceConfig -import com.schwarz.crystalapi.TypeConversion import com.schwarz.crystalapi.TypeConversionErrorWrapper -import com.schwarz.crystalapi.util.CrystalWrap import com.schwarz.crystaldemo.UnitTestConnector import com.schwarz.crystaldemo.entity.ProductCategory.AMAZING_PRODUCT import com.schwarz.crystaldemo.logger.TestAppender @@ -13,10 +11,7 @@ import org.junit.BeforeClass import org.junit.Test import org.mockito.internal.matchers.Null import org.slf4j.LoggerFactory -import kotlin.reflect.KClass -private val typeConversions: Map, TypeConversion> = - mapOf(ProductCategory::class to ProductCategoryTypeConversion) private val logger = LoggerFactory.getLogger(ProductEntityTestConnector::class.java) as ch.qos.logback.classic.Logger @@ -25,7 +20,7 @@ private val dataTypeErrorMsg: (String?, String?, String?) -> String "Field $fieldName manipulated: Tried to cast $value into $`class`" } -object ProductEntityTestConnector : UnitTestConnector(typeConversions) { +object ProductEntityTestConnector : UnitTestConnector() { init { TestAppender().run { name = this::class.java.simpleName @@ -85,18 +80,61 @@ class ProductEntityTest { */ @Test fun `data type changed at runtime test suppress exception`() { - CrystalWrap.write(1, EXAMPLE_TYPE, String::class) + 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) + ) + } + + /** + * Can happen if combined db data is changed wilfully. + */ + @Test + fun `list data type changed at runtime test suppress exception`() { + 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) + ) + } + + /** + * Can happen if combined db data is changed wilfully. + */ + @Test + fun `list item data type changed at runtime test suppress exception`() { + val map: MutableMap = mutableMapOf( + "identifiers" to listOf(1) + ) + ProductEntity.create(map) assertEquals( (logger.getAppender(TestAppender::class.java.simpleName) as TestAppender).lastLoggedEvent?.message, - dataTypeErrorMsg.invoke(EXAMPLE_TYPE, 1::class.simpleName, String::class.simpleName) + dataTypeErrorMsg.invoke("identifiers", "SingletonList", List::class.simpleName) ) } @Test fun `data type consistent`() { - CrystalWrap.write(1, EXAMPLE_TYPE, Int::class) + val map: MutableMap = mutableMapOf( + "name" to "1" + ) + ProductEntity.create(map) assertNull((logger.getAppender(TestAppender::class.java.simpleName) as TestAppender).lastLoggedEvent?.message) } -} -private const val EXAMPLE_TYPE = "EXAMPLE_TYPE" + @Test + fun `list data type consistent`() { + val map: MutableMap = mutableMapOf( + "identifiers" to listOf("Foo") + ) + ProductEntity.create(map) + assertNull((logger.getAppender(TestAppender::class.java.simpleName) as TestAppender).lastLoggedEvent?.message) + } +} 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 f40c5336..cb95792d 100644 --- a/demo/src/test/java/kaufland/com/demo/entity/ProductWrapperTest.kt +++ b/demo/src/test/java/kaufland/com/demo/entity/ProductWrapperTest.kt @@ -1,23 +1,19 @@ package com.schwarz.crystaldemo.entity import com.schwarz.crystalapi.PersistenceConfig -import com.schwarz.crystalapi.TypeConversion import com.schwarz.crystaldemo.UnitTestConnector -import org.junit.Assert.* +import org.junit.Assert.assertEquals import org.junit.BeforeClass import org.junit.Test -import kotlin.reflect.KClass +import java.util.Date class ProductWrapperTest { companion object { - private val typeConversions: Map, TypeConversion> = mapOf( - ProductCategory::class to ProductCategoryTypeConversion - ) @BeforeClass @JvmStatic fun beforeClass() { - PersistenceConfig.configure(UnitTestConnector(typeConversions)) + PersistenceConfig.configure(UnitTestConnector()) } } @@ -40,4 +36,30 @@ class ProductWrapperTest { map ) } + + @Test + fun `toMap - fromMap should work`() { + val product = ProductWrapper.create().builder() + .setName("name") + .setComments( + listOf( + UserCommentWrapper.create().apply { + comment = "foobar" + } + ) + ) + .setCategory(ProductCategory.AMAZING_PRODUCT) + .setIdentifiers(listOf("1", "2")) + .setSomeDate(Date()) + .exit() + val map = product.toMap() as MutableMap + + val result = ProductWrapper.fromMap(map)!! + + assertEquals(product.name, result.name) + assertEquals(product.comments?.first()?.comment!!, result.comments?.first()?.comment!!) + assertEquals(product.category, result.category) + assertEquals(product.identifiers, result.identifiers) + assertEquals(product.someDate, result.someDate) + } } diff --git a/demo/src/test/java/kaufland/com/demo/mapper/DummyMapperSourceTest.kt b/demo/src/test/java/kaufland/com/demo/mapper/DummyMapperSourceTest.kt index 7c8bda78..d0c409cc 100644 --- a/demo/src/test/java/kaufland/com/demo/mapper/DummyMapperSourceTest.kt +++ b/demo/src/test/java/kaufland/com/demo/mapper/DummyMapperSourceTest.kt @@ -1,22 +1,18 @@ package com.schwarz.crystaldemo.mapper import com.schwarz.crystalapi.PersistenceConfig -import com.schwarz.crystalapi.TypeConversion import com.schwarz.crystalapi.TypeConversionErrorWrapper import com.schwarz.crystaldemo.entity.ProductEntity import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import java.math.BigDecimal -import kotlin.reflect.KClass class DummyMapperSourceTest { @Before fun init() { PersistenceConfig.configure(object : PersistenceConfig.Connector { - override val typeConversions: Map, TypeConversion> - get() = mapOf() override fun getDocument(id: String, dbName: String, onlyInclude: List?): Map? { return null diff --git a/dummy/build.gradle b/dummy/build.gradle new file mode 100644 index 00000000..526d3c44 --- /dev/null +++ b/dummy/build.gradle @@ -0,0 +1,32 @@ + +buildscript { + ext.kotlin_version = '1.9.21' + repositories { + google() + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'kotlin' +apply plugin: 'kotlin-kapt' + +kapt { + correctErrorTypes = true + arguments { + arg("crystal.entityframework.useSuspend", "false") + arg("crystal.entityframework.documentation.generated", "${buildDir.absolutePath}/entity") + arg("crystal.entityframework.documentation.fileName", "demo.html") + arg("crystal.entityframework.schema.generated", "${buildDir.absolutePath}/entity_schema") + arg("crystal.entityframework.schema.fileName", "demo_schema.json") + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(path: ':crystal-map-api') + kapt project(path: ':crystal-map-processor') + compileOnly project(path: ':crystal-map-processor') +} diff --git a/dummy/src/main/kotlin/com/schwarz/crystaldummy/customtypes/DummyDateConverter.kt b/dummy/src/main/kotlin/com/schwarz/crystaldummy/customtypes/DummyDateConverter.kt new file mode 100644 index 00000000..1a6e4349 --- /dev/null +++ b/dummy/src/main/kotlin/com/schwarz/crystaldummy/customtypes/DummyDateConverter.kt @@ -0,0 +1,14 @@ +package com.schwarz.crystaldummy.customtypes + +import com.schwarz.crystalapi.ITypeConverter +import com.schwarz.crystalapi.TypeConverter +import java.time.Instant +import java.util.Date + +@TypeConverter +abstract class DummyDateConverter : ITypeConverter { + override fun write(value: Date?): Number? = + value?.toInstant()?.epochSecond + + override fun read(value: Number?): Date? = value?.toLong()?.let { Date.from(Instant.ofEpochSecond(it)) } +} diff --git a/dummy/src/main/kotlin/com/schwarz/crystaldummy/customtypes/DummyTypeConverters.kt b/dummy/src/main/kotlin/com/schwarz/crystaldummy/customtypes/DummyTypeConverters.kt new file mode 100644 index 00000000..1fc2ab33 --- /dev/null +++ b/dummy/src/main/kotlin/com/schwarz/crystaldummy/customtypes/DummyTypeConverters.kt @@ -0,0 +1,6 @@ +package com.schwarz.crystaldemo.customtypes + +import com.schwarz.crystalapi.TypeConverterExporter + +@TypeConverterExporter +interface DummyTypeConverters diff --git a/settings.gradle b/settings.gradle index 746c68dc..5381ef36 100755 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ include ':crystal-map-versioning-plugin' -include ':crystal-map-api', ':crystal-map-processor', ':demo', ':crystal-map-couchbase-connector' +include ':crystal-map-api', ':crystal-map-processor', ':demo', ':dummy', ':crystal-map-couchbase-connector'