From 9424b51bae77eb6339a78f1f5e15ea3a4c8c93ec Mon Sep 17 00:00:00 2001 From: Ethan Hampton Date: Thu, 13 Aug 2020 19:16:39 -0700 Subject: [PATCH 1/2] Add support for type aliases --- README.md | 2 +- .../pl/touk/krush/model/ColumnProcessor.kt | 11 +++- .../pl/touk/krush/model/EntityDefinition.kt | 3 +- .../pl/touk/krush/model/ModelValidator.kt | 2 +- .../pl/touk/example/ValidTableMapping.kt | 34 ++++++++---- .../krush/model/EntityGraphBuilderTest.kt | 16 ++++++ .../touk/krush/model/EntityGraphSampleData.kt | 54 +++++++++++++++++++ 7 files changed, 107 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1cae346..666fe71 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ Maven: * generates table mappings and functions for mapping from/to data classes * type-safe SQL DSL without reading schema from existing database (code-first) * explicit association fetching (via `leftJoin` / `innerJoin`) -* multiple data types support +* multiple data types support, including type aliases * custom data type support (with `@Converter`), also for wrapped auto-generated ids * you can still persist associations not directly reflected in domain model (eq. article favorites) diff --git a/annotation-processor/src/main/kotlin/pl/touk/krush/model/ColumnProcessor.kt b/annotation-processor/src/main/kotlin/pl/touk/krush/model/ColumnProcessor.kt index 919b3cc..78db652 100644 --- a/annotation-processor/src/main/kotlin/pl/touk/krush/model/ColumnProcessor.kt +++ b/annotation-processor/src/main/kotlin/pl/touk/krush/model/ColumnProcessor.kt @@ -155,8 +155,15 @@ class ColumnProcessor(override val typeEnv: TypeEnvironment, private val annEnv: } private fun ImmutableKmType.toModelType(): Type? { - return (this.classifier as KmClassifier.Class).name - .split("/").let { Type(it.dropLast(1).joinToString(separator = "."), it.last()) } + return when(val classifier = (this.abbreviatedType?.classifier ?: this.classifier)){ + is KmClassifier.Class -> { + classifier.name + .split("/").let { Type(it.dropLast(1).joinToString(separator = "."), it.last()) } + } + is KmClassifier.TypeAlias -> classifier.name + .split("/").let { Type(it.dropLast(1).joinToString(separator = "."), it.last(), this.copy(abbreviatedType = null).toModelType()) } + is KmClassifier.TypeParameter -> TODO() + } } private fun String.name(): Name = typeEnv.elementUtils.getName(this) diff --git a/annotation-processor/src/main/kotlin/pl/touk/krush/model/EntityDefinition.kt b/annotation-processor/src/main/kotlin/pl/touk/krush/model/EntityDefinition.kt index 6d3ddcf..c6db29e 100644 --- a/annotation-processor/src/main/kotlin/pl/touk/krush/model/EntityDefinition.kt +++ b/annotation-processor/src/main/kotlin/pl/touk/krush/model/EntityDefinition.kt @@ -95,7 +95,8 @@ enum class EnumType { data class Type( val packageName: String, - val simpleName: String + val simpleName: String, + val aliasOf: Type? = null ) data class EmbeddableDefinition( diff --git a/annotation-processor/src/main/kotlin/pl/touk/krush/model/ModelValidator.kt b/annotation-processor/src/main/kotlin/pl/touk/krush/model/ModelValidator.kt index fdeb03d..76a9595 100644 --- a/annotation-processor/src/main/kotlin/pl/touk/krush/model/ModelValidator.kt +++ b/annotation-processor/src/main/kotlin/pl/touk/krush/model/ModelValidator.kt @@ -97,7 +97,7 @@ class EntityPropertyTypeValidator : Validator { override fun validate(el: EntityDefinition): ValidationResult { val errors = mutableListOf() - el.properties.filter { !it.hasConverter() && !it.isEnumerated() && it.type !in supportedPropertyTypes }.forEach { + el.properties.filter { !it.hasConverter() && !it.isEnumerated() && it.type !in supportedPropertyTypes && it.type.aliasOf !in supportedPropertyTypes }.forEach { errors.add(ValidationErrorMessage("Entity ${el.qualifiedName} has unsupported property type ${it.type}")) } diff --git a/annotation-processor/src/test/kotlin/pl/touk/example/ValidTableMapping.kt b/annotation-processor/src/test/kotlin/pl/touk/example/ValidTableMapping.kt index ca676a5..a726ee5 100644 --- a/annotation-processor/src/test/kotlin/pl/touk/example/ValidTableMapping.kt +++ b/annotation-processor/src/test/kotlin/pl/touk/example/ValidTableMapping.kt @@ -3,17 +3,8 @@ package pl.touk.example import java.time.LocalDate import java.time.LocalDateTime import java.time.ZonedDateTime -import javax.persistence.Column -import javax.persistence.Embeddable -import javax.persistence.Embedded -import javax.persistence.Entity +import javax.persistence.* import javax.persistence.EnumType.STRING -import javax.persistence.Enumerated -import javax.persistence.GeneratedValue -import javax.persistence.Id -import javax.persistence.JoinColumn -import javax.persistence.OneToOne -import javax.persistence.Table @Entity data class DefaultPropertyNameEntity( @@ -48,6 +39,29 @@ data class NullablePropertyEntity( val prop1: String? ) +typealias StringMap = Map +typealias PlainString = String + +@Entity +data class TypeAliasEntity( + @Id @GeneratedValue + val id: Long?, + @Convert(converter = StringMapConverter::class) + val aliased: StringMap, + val justAString: PlainString +) + +@Converter +class StringMapConverter : AttributeConverter { + override fun convertToDatabaseColumn(attribute: StringMap?): String { + return attribute?.map { it.key + ":" + it.value }?.joinToString("\n") ?: "" + } + + override fun convertToEntityAttribute(dbData: String?): StringMap { + return dbData?.splitToSequence("\n")?.associate { Pair(it.split(":")[0], it.split(":")[1]) } ?: HashMap() + } +} + @Entity data class OneToOneSourceEntity( diff --git a/annotation-processor/src/test/kotlin/pl/touk/krush/model/EntityGraphBuilderTest.kt b/annotation-processor/src/test/kotlin/pl/touk/krush/model/EntityGraphBuilderTest.kt index 579ab4f..3bd4df9 100644 --- a/annotation-processor/src/test/kotlin/pl/touk/krush/model/EntityGraphBuilderTest.kt +++ b/annotation-processor/src/test/kotlin/pl/touk/krush/model/EntityGraphBuilderTest.kt @@ -140,5 +140,21 @@ import javax.lang.model.util.Types .containsKey(enumPropertyEntity(getTypeEnv())) .containsValue(enumPropertyEntityDefinition(getTypeEnv())) } + + @Test + fun shouldHandleTypeAliases(){ + //given + val typealiasGraphBuilder = typealiasGraphBuilder(getTypeEnv()) + + //when + val graphs = typealiasGraphBuilder.build() + + //then + assertThat(graphs).containsKey("pl.touk.example") + + assertThat(graphs["pl.touk.example"]) + .containsKey(typealiasEntity(getTypeEnv())) + .containsValue(typealiasEntityDefinition(getTypeEnv())) + } } diff --git a/annotation-processor/src/test/kotlin/pl/touk/krush/model/EntityGraphSampleData.kt b/annotation-processor/src/test/kotlin/pl/touk/krush/model/EntityGraphSampleData.kt index 0c16d49..6e8f58c 100644 --- a/annotation-processor/src/test/kotlin/pl/touk/krush/model/EntityGraphSampleData.kt +++ b/annotation-processor/src/test/kotlin/pl/touk/krush/model/EntityGraphSampleData.kt @@ -79,6 +79,10 @@ interface EntityGraphSampleData { return getTypeElement("pl.touk.example.PropertyTypeUnsupportedEntity", typeEnvironment.elementUtils) } + fun typealiasEntity(typeEnvironment: TypeEnvironment): TypeElement { + return getTypeElement("pl.touk.example.TypeAliasEntity", typeEnvironment.elementUtils) + } + fun customerGraphBuilder(typeEnvironment: TypeEnvironment): EntityGraphBuilder { val entity = customerTestEntity(typeEnvironment) val id = getVariableElement(entity, typeEnvironment.elementUtils, "id") @@ -451,6 +455,56 @@ interface EntityGraphSampleData { ) } + fun typealiasGraphBuilder(typeEnvironment: TypeEnvironment): EntityGraphBuilder { + val elements = typeEnvironment.elementUtils + + val entity = typealiasEntity(typeEnvironment) + val id = getVariableElement(entity, elements, "id") + val aliased = getVariableElement(entity, elements, "aliased") + val plainString = getVariableElement(entity, elements, "justAString") + + val annEnv = AnnotationEnvironment(entities = listOf(entity), ids = listOf(id), + columns = listOf(aliased, plainString), oneToMany = emptyList(), manyToOne = emptyList(), + manyToMany = emptyList(), oneToOne = emptyList(), embedded = emptyList(), embeddedColumn = emptyList()) + + return EntityGraphBuilder(typeEnvironment, annEnv) + } + + fun typealiasEntityDefinition(typeEnvironment: TypeEnvironment): EntityDefinition { + val entity = typealiasEntity(typeEnvironment) + val id = getVariableElement(entity, typeEnvironment.elementUtils, "id") + val prop1 = getVariableElement(entity, typeEnvironment.elementUtils, "aliased") + val plainString = getVariableElement(entity, typeEnvironment.elementUtils, "justAString") + + return EntityDefinition( + name = entity.simpleName, qualifiedName = entity.qualifiedName, + table = entity.simpleName.asVariable(), + id = autoGenIdDefinition(id, typeEnvironment.elementUtils.getName(id.simpleName)), + properties = listOf( + propertyDefinition( + typeEnvironment, + prop1, + "aliased", + Type( + packageName = "pl.touk.example", + simpleName = "StringMap", + aliasOf = Type("kotlin.collections","Map")), + false) + .copy(converter = ConverterDefinition( + name = "pl.touk.example.StringMapConverter", + targetType = Type(packageName = "kotlin", simpleName = "String" + ))), + propertyDefinition( + typeEnvironment, + plainString, + "justAString", + Type( + packageName = "pl.touk.example", + simpleName = "PlainString", + aliasOf = Type("kotlin", "String")), + false))) + } + private fun autoGenIdDefinition(id: VariableElement, name: Name): IdDefinition { return IdDefinition( name = id.simpleName, From 1115cd753148fea4fff19f34a13aef1bb9b4079d Mon Sep 17 00:00:00 2001 From: Ethan Hampton Date: Thu, 27 Aug 2020 09:21:03 -0700 Subject: [PATCH 2/2] Add typealias support examples This required slight modification to the code generation in order to properly use the underlying type of an alias in certain locations and the name of the typealias in others. Additionally, had to remove support for unwrapping iterables from the string wrapper class but there aren't many use cases for that feature that I can think of. --- .../CopiedReferencesMappingsGenerator.kt | 2 +- .../pl/touk/krush/source/MappingsGenerator.kt | 2 +- .../source/RealReferencesMappingsGenerator.kt | 2 +- .../pl/touk/krush/source/TablesGenerator.kt | 36 +++++++++++++------ .../pl/touk/krush/typealiases/VisitorLog.kt | 26 ++++++++++++++ .../touk/krush/typealiases/VisitorLogTest.kt | 35 ++++++++++++++++++ .../kotlin/pl/touk/krush/WrapperColumn.kt | 11 ++++++ 7 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 example/src/main/kotlin/pl/touk/krush/typealiases/VisitorLog.kt create mode 100644 example/src/test/kotlin/pl/touk/krush/typealiases/VisitorLogTest.kt diff --git a/annotation-processor/src/main/kotlin/pl/touk/krush/source/CopiedReferencesMappingsGenerator.kt b/annotation-processor/src/main/kotlin/pl/touk/krush/source/CopiedReferencesMappingsGenerator.kt index 733ebe4..43d87a7 100644 --- a/annotation-processor/src/main/kotlin/pl/touk/krush/source/CopiedReferencesMappingsGenerator.kt +++ b/annotation-processor/src/main/kotlin/pl/touk/krush/source/CopiedReferencesMappingsGenerator.kt @@ -15,7 +15,7 @@ class CopiedReferencesMappingsGenerator : MappingsGenerator() { val associations = entity.getAssociations(ONE_TO_ONE, ONE_TO_MANY, MANY_TO_MANY) associations.forEach { assoc -> val target = graphs[assoc.target.packageName]?.get(assoc.target) ?: throw EntityNotMappedException(assoc.target) - val entityIdTypeName = entityId.asTypeName() + val entityIdTypeName = entityId.asUnderlyingTypeName() val associationMapName = "${entity.name.asVariable()}_${assoc.name}" val associationMapValueType = if (assoc.type in listOf(ONE_TO_MANY, MANY_TO_MANY)) "MutableSet<${target.name}>" else "${target.name}" diff --git a/annotation-processor/src/main/kotlin/pl/touk/krush/source/MappingsGenerator.kt b/annotation-processor/src/main/kotlin/pl/touk/krush/source/MappingsGenerator.kt index 7f4c3b3..507a12e 100644 --- a/annotation-processor/src/main/kotlin/pl/touk/krush/source/MappingsGenerator.kt +++ b/annotation-processor/src/main/kotlin/pl/touk/krush/source/MappingsGenerator.kt @@ -90,7 +90,7 @@ abstract class MappingsGenerator : SourceGenerator { } private fun buildToEntityMapFunc(entityType: TypeElement, entity: EntityDefinition, graphs: EntityGraphs): FunSpec { - val rootKey = entity.id?.asTypeName() ?: throw MissingIdException(entity) + val rootKey = entity.id?.asUnderlyingTypeName() ?: throw MissingIdException(entity) val rootVal = entity.name.asVariable() val func = FunSpec.builder("to${entity.name}Map") diff --git a/annotation-processor/src/main/kotlin/pl/touk/krush/source/RealReferencesMappingsGenerator.kt b/annotation-processor/src/main/kotlin/pl/touk/krush/source/RealReferencesMappingsGenerator.kt index 4775702..8d61d85 100644 --- a/annotation-processor/src/main/kotlin/pl/touk/krush/source/RealReferencesMappingsGenerator.kt +++ b/annotation-processor/src/main/kotlin/pl/touk/krush/source/RealReferencesMappingsGenerator.kt @@ -23,7 +23,7 @@ class RealReferencesMappingsGenerator : MappingsGenerator() { val associations = entity.getAssociations(ONE_TO_ONE, ONE_TO_MANY, MANY_TO_MANY) associations.forEach { assoc -> val target = graphs[assoc.target.packageName]?.get(assoc.target) ?: throw EntityNotMappedException(assoc.target) - val entityIdTypeName = entityId.asTypeName() + val entityIdTypeName = entityId.asUnderlyingTypeName() val associationMapName = "${entity.name.asVariable()}_${assoc.name}" val associationMapValueType = if (assoc.type in listOf(ONE_TO_MANY, MANY_TO_MANY)) "MutableSet<${target.name}>" else "${target.name}" diff --git a/annotation-processor/src/main/kotlin/pl/touk/krush/source/TablesGenerator.kt b/annotation-processor/src/main/kotlin/pl/touk/krush/source/TablesGenerator.kt index 159c82f..1b01f65 100644 --- a/annotation-processor/src/main/kotlin/pl/touk/krush/source/TablesGenerator.kt +++ b/annotation-processor/src/main/kotlin/pl/touk/krush/source/TablesGenerator.kt @@ -71,7 +71,7 @@ class TablesGenerator : SourceGenerator { entity.getAssociations(AssociationType.MANY_TO_ONE).forEach { assoc -> val name = assoc.name.toString() - val columnType = assoc.targetId.type.asClassName() + val columnType = assoc.targetId.type.asUnderlyingClassName() CodeBlock.builder() val initializer = associationInitializer(assoc, name) tableSpec.addProperty( @@ -84,7 +84,7 @@ class TablesGenerator : SourceGenerator { entity.getAssociations(AssociationType.ONE_TO_ONE).filter {it.mapped}.forEach {assoc -> val name = assoc.name.toString() - val columnType = assoc.targetId.type.asClassName() + val columnType = assoc.targetId.type.asUnderlyingClassName() CodeBlock.builder() val initializer = associationInitializer(assoc, name) tableSpec.addProperty( @@ -103,14 +103,14 @@ class TablesGenerator : SourceGenerator { .superclass(Table::class) .addSuperclassConstructorParameter(CodeBlock.of("%S", assoc.joinTable)) - val sourceType = entity.id.type.asClassName() + val sourceType = entity.id.type.asUnderlyingClassName() manyToManyTableSpec.addProperty( PropertySpec.builder("${rootVal}SourceId", Column::class.asClassName().parameterizedBy(sourceType)) .initializer(manyToManyPropertyInitializer(entity.id, entity, "_source")) .build() ) - val targetIdType = assoc.targetId.type.asClassName() + val targetIdType = assoc.targetId.type.asUnderlyingClassName() val targetEntityDef = graphs.entity(assoc.target.packageName, assoc.target) ?: throw AssociationTargetEntityNotFoundException(assoc.target) manyToManyTableSpec.addProperty( @@ -155,7 +155,7 @@ class TablesGenerator : SourceGenerator { val isGenerated = entity.id?.generatedValue ?: false val persistedName = if (isGenerated) "persisted${entityName.capitalize()}" else entityName val func = FunSpec.builder("insert") - .receiver(Type(entityType.packageName, entity.tableName).asClassName()) + .receiver(Type(entityType.packageName, entity.tableName).asUnderlyingClassName()) .addParameter(entity.name.asVariable(), entityType.asType().asTypeName()) .returns(entityType.asType().asTypeName()) @@ -195,7 +195,7 @@ class TablesGenerator : SourceGenerator { } private fun converterFunc(name: String, type: TypeName, it: ConverterDefinition, fileSpec: FileSpec.Builder) { - val wrapperName = when (it.targetType.asClassName()) { + val wrapperName = when (it.targetType.asUnderlyingClassName()) { STRING -> "stringWrapper" LONG -> "longWrapper" else -> throw TypeConverterNotSupportedException(it.targetType) @@ -215,7 +215,7 @@ class TablesGenerator : SourceGenerator { val codeBlock = if (id.converter != null) { converterPropInitializer(entityName = entity.name, propertyName = id.name, columnName = id.columnName.asVariable()) - } else when (id.asTypeName()) { + } else when (id.asUnderlyingTypeName()) { STRING -> CodeBlock.of("varchar(%S, %L)", id.columnName, id.annotation?.length ?: 255) LONG -> CodeBlock.of("long(%S)", id.columnName) INT -> CodeBlock.of("integer(%S)", id.columnName) @@ -263,7 +263,7 @@ class TablesGenerator : SourceGenerator { private fun enumPropInitializer(property: PropertyDefinition): CodeBlock { val columnName = property.columnName - val enumType = property.type.asClassName() + val enumType = property.type.asUnderlyingClassName() return when (property.enumerated!!.enumType) { EnumType.STRING -> { @@ -275,7 +275,7 @@ class TablesGenerator : SourceGenerator { } private fun typePropInitializer(property: PropertyDefinition): CodeBlock { - return when (property.asTypeName()) { + return when (property.asUnderlyingTypeName()) { STRING -> CodeBlock.of("varchar(%S, %L)", property.columnName, property.annotation?.length ?: 255) LONG -> CodeBlock.of("long(%S)", property.columnName) BOOLEAN -> CodeBlock.of("bool(%S)", property.columnName) @@ -318,7 +318,7 @@ class TablesGenerator : SourceGenerator { private fun idCodeBlock(id: IdDefinition, entityName: Name, columnName: String): CodeBlock { return if (id.converter != null) { converterPropInitializer(entityName = entityName, propertyName = id.name, columnName = columnName) - } else when (id.asTypeName()) { + } else when (id.asUnderlyingTypeName()) { STRING -> CodeBlock.of("varchar(%S, %L)", columnName, id.annotation?.length ?: 255) LONG -> CodeBlock.of("long(%S)", columnName) INT -> CodeBlock.of("integer(%S)", columnName) @@ -329,6 +329,22 @@ class TablesGenerator : SourceGenerator { } } +fun IdDefinition.asUnderlyingTypeName(): TypeName { + return this.type.asUnderlyingClassName() +} + +fun PropertyDefinition.asUnderlyingTypeName(): TypeName { + return this.type.asUnderlyingClassName() +} + +fun Type.asUnderlyingClassName(): ClassName { + return if(this.aliasOf !=null) { + ClassName(this.aliasOf.packageName, this.aliasOf.simpleName) + }else{ + ClassName(this.packageName, this.simpleName) + } +} + fun IdDefinition.asTypeName(): TypeName { return this.type.asClassName() } diff --git a/example/src/main/kotlin/pl/touk/krush/typealiases/VisitorLog.kt b/example/src/main/kotlin/pl/touk/krush/typealiases/VisitorLog.kt new file mode 100644 index 0000000..cbab36b --- /dev/null +++ b/example/src/main/kotlin/pl/touk/krush/typealiases/VisitorLog.kt @@ -0,0 +1,26 @@ +package pl.touk.krush.typealiases + +import javax.persistence.* + +typealias VisitorList = List +typealias PlainString = String + +@Entity +data class VisitorLog( + @Id @GeneratedValue + val id: Long? = null, + @Convert(converter = VisitorListConverter::class) + val visitors: VisitorList, + val guard: PlainString +) + +@Converter +class VisitorListConverter : AttributeConverter { + override fun convertToDatabaseColumn(attribute: VisitorList?): String { + return attribute?.joinToString(",") ?: "" + } + + override fun convertToEntityAttribute(dbData: String?): VisitorList { + return dbData?.split(",") ?: emptyList() + } +} \ No newline at end of file diff --git a/example/src/test/kotlin/pl/touk/krush/typealiases/VisitorLogTest.kt b/example/src/test/kotlin/pl/touk/krush/typealiases/VisitorLogTest.kt new file mode 100644 index 0000000..732f84e --- /dev/null +++ b/example/src/test/kotlin/pl/touk/krush/typealiases/VisitorLogTest.kt @@ -0,0 +1,35 @@ +package pl.touk.krush.typealiases + +import org.assertj.core.api.Assertions +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import org.junit.jupiter.api.Test +import pl.touk.krush.base.BaseDatabaseTest +import pl.touk.krush.types.Event +import pl.touk.krush.types.EventTable +import pl.touk.krush.types.insert +import pl.touk.krush.types.toEventList +import java.time.* +import java.util.* + +class VisitorLogTest : BaseDatabaseTest() { + + @Test + fun shouldHandleTypeAliases() { + transaction { + SchemaUtils.create(VisitorLogTable) + + // given + val log = VisitorLogTable.insert(VisitorLog(visitors = listOf("Krush", "Kotlin", "Gradle" ), guard = "Kelly")) + + //when + val logs = (VisitorLogTable) + .select { VisitorLogTable.guard eq "Kelly" } + .toVisitorLogList() + + //then + Assertions.assertThat(logs).containsOnly(log) + } + } +} diff --git a/runtime/src/main/kotlin/pl/touk/krush/WrapperColumn.kt b/runtime/src/main/kotlin/pl/touk/krush/WrapperColumn.kt index b63abff..c5b592b 100644 --- a/runtime/src/main/kotlin/pl/touk/krush/WrapperColumn.kt +++ b/runtime/src/main/kotlin/pl/touk/krush/WrapperColumn.kt @@ -66,6 +66,17 @@ class StringWrapperColumnType( is String -> instanceCreator(rawColumnType.valueFromDB(value) as String) else -> error("Database value $value of class ${value::class.qualifiedName} is not valid $rawClazz") } + + /* + * Essentially the same implementation as the superclass however it doesn't auto-unwrap Iterable objects + */ + override fun valueToString(value: Any?): String = when (value) { + null -> { + check(nullable) { "NULL in non-nullable column" } + "NULL" + } + else -> nonNullValueToString(value) + } } inline fun Table.longWrapper(