Skip to content

Commit

Permalink
Merge pull request #82 from cyrillhalter/typeconverters
Browse files Browse the repository at this point in the history
Compile-time-safe type converters
  • Loading branch information
marco-wolf-ergon committed Jun 21, 2024
2 parents 9f10311 + 43da82b commit 3535156
Show file tree
Hide file tree
Showing 71 changed files with 1,581 additions and 513 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.schwarz.crystalapi

interface ITypeConverter<KotlinType, MapType> {
fun write(value: KotlinType?): MapType?
fun read(value: MapType?): KotlinType?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.schwarz.crystalapi

import kotlin.reflect.KClass

interface ITypeConverterExporter {

val typeConverters: Map<KClass<*>, ITypeConverter<*, *>>

val typeConverterImportables: List<TypeConverterImportable>
}

data class TypeConverterImportable(
val typeConverterInstanceClassName: ClassNameDefinition,
val domainClassName: ClassNameDefinition,
val mapClassName: ClassNameDefinition
)

data class ClassNameDefinition(
val packageName: String,
val className: String
)
Original file line number Diff line number Diff line change
@@ -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<KClass<*>, TypeConversion>
fun getDocument(
id: String,
dbName: String,
Expand Down Expand Up @@ -40,8 +37,6 @@ object PersistenceConfig {

interface SuspendingConnector : TypeConversionErrorCallback {

val typeConversions: Map<KClass<*>, TypeConversion>

suspend fun getDocument(
id: String,
dbName: String,
Expand Down Expand Up @@ -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)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.schwarz.crystalapi

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class TypeConverter
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.schwarz.crystalapi

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class TypeConverterExporter
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.schwarz.crystalapi

import kotlin.reflect.KClass

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class TypeConverterImporter(val typeConverterExporter: KClass<out ITypeConverterExporter>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.schwarz.crystalapi.typeconverters

import com.schwarz.crystalapi.ITypeConverter
import kotlin.reflect.KClass

class EnumConverter<T : Enum<T>> (private val enumClass: KClass<T>) : ITypeConverter<T, String> {
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)
}
}
}
207 changes: 109 additions & 98 deletions crystal-map-api/src/main/java/com/schwarz/crystalapi/util/CrystalWrap.kt
Original file line number Diff line number Diff line change
@@ -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 <T> get(
inline fun <reified T> get(
changes: MutableMap<String, Any?>,
doc: MutableMap<String, Any>,
doc: MutableMap<String, out Any?>,
fieldName: String,
clazz: KClass<*>,
noinline mapper: ((MutableMap<String, Any?>?) -> T?)? = null
): T? {
return (changes[fieldName] ?: doc[fieldName])?.let { value ->
mapper?.let {
mapper.invoke(value as? MutableMap<String, Any?>)
} ?: read(value, fieldName, clazz)
} ?: null
mapper: ((MutableMap<String, Any?>?) -> T?)
): T? = (changes[fieldName] ?: doc[fieldName])?.let { value ->
catchTypeConversionError(fieldName, value) {
mapper.invoke(value as MutableMap<String, Any?>)
}
}

inline fun validate(
doc: MutableMap<String, Any>,
mandatoryFields: Array<String>
) {
for (mandatoryField in mandatoryFields) {
doc[mandatoryField]!!
inline fun <reified T, reified U> get(
changes: MutableMap<String, Any?>,
doc: MutableMap<String, out Any?>,
fieldName: String,
typeConverter: ITypeConverter<T, U>
): T? = (changes[fieldName] ?: doc[fieldName])?.let { value ->
catchTypeConversionError(fieldName, value) {
typeConverter.read(value as U)
}
}

inline fun <reified T> get(
changes: MutableMap<String, Any?>,
doc: MutableMap<String, out Any?>,
fieldName: String
): T? = (changes[fieldName] ?: doc[fieldName])?.let { value ->
catchTypeConversionError(fieldName, value) {
value as T
}
}

inline fun <T> getList(
changes: MutableMap<String, Any?>,
doc: MutableMap<String, Any>,
doc: MutableMap<String, out Any?>,
fieldName: String,
mapper: ((List<MutableMap<String, Any?>>?) -> List<T>)
): List<T>? = (changes[fieldName] ?: doc[fieldName])?.let { value ->
catchTypeConversionError(fieldName, value) {
mapper.invoke(value as List<MutableMap<String, Any?>>)
}
}

inline fun <T, reified U> getList(
changes: MutableMap<String, Any?>,
doc: MutableMap<String, out Any?>,
fieldName: String,
clazz: KClass<*>,
noinline mapper: ((List<MutableMap<String, Any?>>?) -> List<T>)? = null
): List<T>? {
return (changes[fieldName] ?: doc[fieldName])?.let { value ->
mapper?.let {
mapper.invoke(value as? List<MutableMap<String, Any?>>)
} ?: read(value, fieldName, clazz)
} ?: null
typeConverter: ITypeConverter<T, U>
): List<T>? = (changes[fieldName] ?: doc[fieldName])?.let { value ->
catchTypeConversionError(fieldName, value) {
((value as List<Any>).map { it as U }).mapNotNull {
typeConverter.read(it)
}
}
}

inline fun <reified T> getList(
changes: MutableMap<String, Any?>,
doc: MutableMap<String, out Any?>,
fieldName: String
): List<T>? = (changes[fieldName] ?: doc[fieldName])?.let { value ->
catchTypeConversionError(fieldName, value) {
value as List<T>
}
}

fun <T> set(
inline fun <T> set(
changes: MutableMap<String, Any?>,
fieldName: String,
value: T,
clazz: KClass<*>,
mapper: ((T) -> MutableMap<String, Any>)? = null
mapper: ((T) -> MutableMap<String, Any>)
) {
changes[fieldName] = mapper.invoke(value)
}

inline fun <T> set(
changes: MutableMap<String, Any?>,
fieldName: String,
value: T?,
typeConverter: ITypeConverter<T, *>
) {
val valueToSet = mapper?.let { it.invoke(value) } ?: write<T>(value, fieldName, clazz)
changes[fieldName] = valueToSet
changes[fieldName] = typeConverter.write(value)
}

inline fun <T> set(
changes: MutableMap<String, Any?>,
fieldName: String,
value: T?
) {
changes[fieldName] = value
}

inline fun <T> setList(
changes: MutableMap<String, Any?>,
fieldName: String,
value: List<T>?,
clazz: KClass<*>,
noinline mapper: ((List<T>) -> List<MutableMap<String, Any>>)? = null
mapper: ((List<T>) -> List<MutableMap<String, Any>>)
) {
val valueToSet =
mapper?.let { if (value != null) it.invoke(value) else emptyList() } ?: write<T>(
value,
fieldName,
clazz
)
changes[fieldName] = valueToSet
changes[fieldName] = if (value != null) mapper.invoke(value) else emptyList()
}

fun <T> ensureTypes(map: Map<String, KClass<*>>, doc: Map<String, Any?>): Map<String, Any> {
val result = mutableMapOf<String, Any>()
for (entry in map) {
write<T>(doc[entry.key], entry.key, entry.value)?.let {
result[entry.key] = it
}
}
return result
inline fun <T> setList(
changes: MutableMap<String, Any?>,
fieldName: String,
value: List<T>?,
typeConverter: ITypeConverter<T, *>
) {
changes[fieldName] = value?.map { typeConverter.write(it) }
}

fun <T, V> addDefaults(list: List<Array<Any>>, doc: MutableMap<String, V>) {
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<T>(value, key, clazz)?.let {
doc[key] = it as V
}
}
}
inline fun <T> setList(
changes: MutableMap<String, Any?>,
fieldName: String,
value: List<T>?
) {
changes[fieldName] = value
}

fun <T> 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<String, Any>,
mandatoryFields: Array<String>
) {
for (mandatoryField in mandatoryFields) {
doc[mandatoryField]!!
}
}

fun <T> 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 <reified T> 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
}
}
Loading

0 comments on commit 3535156

Please sign in to comment.