Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/.cspell/dev.dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ lateinit # keyword in Kotlin
metaspace # a memory space in the JVM
mkdirs # gradle API to create directories
sarif # a standard for test results (https://gcc.gnu.org/wiki/SARIF)
typealiases # the plural form of typealias
Comment thread
QuinnB73 marked this conversation as resolved.
usertype # in Hibernate, a type with a custom adapter defined by the user
Xjsr # a jvm flag to enable JSR 305 annotations
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.faire.yawn

import com.faire.yawn.YawnTestUtils.assertGeneratedFile
import com.faire.yawn.criteria.query.JoinTypeSafeCriteriaQuery
import com.faire.yawn.criteria.query.ProjectedTypeSafeCriteriaQuery
import com.faire.yawn.criteria.query.TypeSafeCriteriaQuery
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import kotlin.reflect.KVisibility
import kotlin.reflect.typeOf

internal class YawnEntityProcessorTypeAliasesTest {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we also add a very simple test on yawn-database-test that uses each generated type within a helper function in a query? more of a "documentation" and "double-checking" that these particular type aliases we chose are useful and how each can be expected to be used

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, can do

@Test
fun `generates type aliases for TableDef`() {
assertThat(typeOf<EntityWithElementCollectionTableDefType>()).isEqualTo(
typeOf<EntityWithElementCollectionTableDef<EntityWithElementCollection>>(),
)
}

@Test
fun `generates type aliases for TypeSafeCriteriaQuery`() {
assertThat(typeOf<EntityWithElementCollectionCriteriaQuery>()).isEqualTo(
typeOf<
TypeSafeCriteriaQuery<
EntityWithElementCollection,
EntityWithElementCollectionTableDef<EntityWithElementCollection>,
>,
>(),
)
}

@Test
fun `generates type aliases for JoinTypeSafeCriteriaQuery`() {
assertThat(typeOf<EntityWithElementCollectionJoinCriteriaQuery>()).isEqualTo(
typeOf<
JoinTypeSafeCriteriaQuery<
EntityWithElementCollection,
EntityWithElementCollection,
EntityWithElementCollectionTableDef<EntityWithElementCollection>,
>,
>(),
)
}

@Test
fun `generates type aliases for ProjectedTypeSafeCriteriaQuery`() {
assertThat(typeOf<EntityWithElementCollectionProjectedCriteriaQuery<String>>()).isEqualTo(
typeOf<ProjectedTypeSafeCriteriaQuery<EntityWithElementCollection, *, *, String>>(),
)

assertThat(typeOf<EntityWithElementCollectionProjectedCriteriaQuery<Boolean>>()).isEqualTo(
typeOf<ProjectedTypeSafeCriteriaQuery<EntityWithElementCollection, *, *, Boolean>>(),
)
}

@Test
fun `visibility of type aliases is correct`() {
assertGeneratedFile<InternalEmptyEntityTable> {
containsTypeAlias("InternalEmptyEntityTableDefType", KVisibility.INTERNAL)
containsTypeAlias("InternalEmptyEntityCriteriaQuery", KVisibility.INTERNAL)
containsTypeAlias("InternalEmptyEntityJoinCriteriaQuery", KVisibility.INTERNAL)
containsTypeAlias("InternalEmptyEntityProjectedCriteriaQuery", KVisibility.INTERNAL)
}

assertGeneratedFile<PublicEmptyEntityTable> {
containsTypeAlias("PublicEmptyEntityTableDefType", KVisibility.PUBLIC)
containsTypeAlias("PublicEmptyEntityCriteriaQuery", KVisibility.PUBLIC)
containsTypeAlias("PublicEmptyEntityJoinCriteriaQuery", KVisibility.PUBLIC)
containsTypeAlias("PublicEmptyEntityProjectedCriteriaQuery", KVisibility.PUBLIC)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.faire.yawn
import com.faire.yawn.project.YawnProjectionDef
import com.faire.yawn.project.YawnProjectionRef
import org.assertj.core.api.Assertions.assertThat
import java.io.File
import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass
import kotlin.reflect.KVisibility
Expand All @@ -11,6 +12,9 @@ import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.memberProperties
import kotlin.reflect.typeOf

// Standard KSP output directory - configured by the KSP Gradle plugin
private const val GENERATED_SOURCE_DIR = "build/generated/ksp/main/kotlin"

internal object YawnTestUtils {
inline fun <reified T : Any> assertGeneratedEntity(
expectedVisibility: KVisibility? = null,
Expand Down Expand Up @@ -38,6 +42,20 @@ internal object YawnTestUtils {
)
}

inline fun <reified T : YawnTableRef<*, *>> assertGeneratedFile(
builder: GeneratedFileAssertContext.() -> Unit,
) {
val clazz = T::class
val packagePath = clazz.java.packageName.replace('.', '/')
val fileName = "${clazz.getClassNamePrefix()}Def.kt"

val file = File("$GENERATED_SOURCE_DIR/$packagePath/$fileName")

val lines = file.readLines()

builder(GeneratedFileAssertContext(lines))
}

fun <T : Any> internalAssertGeneratedEntity(
expectedSuperClassDef: KClass<out YawnDef<*, *>>,
expectedSuperClassRef: KClass<out YawnRef<*, *>>,
Expand Down Expand Up @@ -71,9 +89,7 @@ internal object YawnTestUtils {
): KClass<*> {
val classPath = clazz.java.packageName
val defName = expectedSuperClass.simpleName!!.removePrefix("Yawn").removeSuffix("Ref")
val classNamePrefix = clazz.java
.getNestingChain()
.joinToString(separator = "_") { it.simpleName }
val classNamePrefix = clazz.getClassNamePrefix()
val className = "$classNamePrefix$defName"
return Class.forName("$classPath.$className").kotlin
}
Expand Down Expand Up @@ -139,6 +155,15 @@ internal object YawnTestUtils {
assertThat(properties).anyMatch { it.name == columnName && it.returnType.toString() == columnType }
}
}

class GeneratedFileAssertContext(private val lines: List<String>) {
fun containsTypeAlias(name: String, visibility: KVisibility) {
val visibilityKeyword = visibility.name.lowercase()
val expectedPrefix = "$visibilityKeyword typealias $name"

assertThat(lines.singleOrNull { it.startsWith(expectedPrefix) }).isNotNull()
}
}
}

private fun Class<*>.getNestingChain(): List<Class<*>> {
Expand All @@ -154,3 +179,7 @@ private fun Class<*>.getNestingChain(): List<Class<*>> {
private inline fun <reified C> getTypeStringWithSourceReplaced(replacement: String): String {
return typeOf<C>().toString().replace(replacement, "SOURCE")
}

private fun KClass<*>.getClassNamePrefix(): String {
return java.getNestingChain().joinToString(separator = "_") { it.simpleName!! }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.faire.yawn.generators.typealiases

import com.faire.ksp.getUniqueSimpleName
import com.faire.yawn.criteria.query.JoinTypeSafeCriteriaQuery
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.asClassName

/**
* Generates: `typealias DbBookJoinCriteriaQuery = JoinTypeSafeCriteriaQuery<DbBook, DbBook, DbBookTableDef<DbBook>>`
*/
internal object JoinTypeSafeCriteriaQueryTypeAliasGenerator : YawnTableDefTypeAliasGenerator {
override fun getName(entityType: ClassName): String = "${entityType.getUniqueSimpleName()}JoinCriteriaQuery"

override fun getType(entityType: ClassName, tableDefType: ParameterizedTypeName): ParameterizedTypeName {
return JoinTypeSafeCriteriaQuery::class.asClassName().parameterizedBy(
entityType,
entityType,
tableDefType,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.faire.yawn.generators.typealiases

import com.faire.ksp.getUniqueSimpleName
import com.faire.yawn.criteria.query.ProjectedTypeSafeCriteriaQuery
import com.faire.yawn.util.YawnContext
import com.squareup.kotlinpoet.ANY
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeAliasSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName

/**
* Generates: `typealias DbBookProjectedCriteriaQuery<PROJECTION> = ProjectedTypeSafeCriteriaQuery<DbBook, *, *, PROJECTION>`
*/
internal object ProjectedTypeSafeCriteriaQueryTypeAliasGenerator : YawnTableDefTypeAliasGenerator {
private val projectionTypeVariable = TypeVariableName("PROJECTION", ANY.copy(nullable = true))

override fun getName(entityType: ClassName): String = "${entityType.getUniqueSimpleName()}ProjectedCriteriaQuery"

override fun getType(entityType: ClassName, tableDefType: ParameterizedTypeName): ParameterizedTypeName {
return ProjectedTypeSafeCriteriaQuery::class.asClassName().parameterizedBy(
entityType,
STAR,
STAR,
projectionTypeVariable,
)
}

override fun TypeAliasSpec.Builder.additionalTypeAliasBuilder(yawnContext: YawnContext): TypeAliasSpec.Builder {
return addTypeVariable(projectionTypeVariable)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.faire.yawn.generators.typealiases

import com.faire.ksp.getUniqueSimpleName
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName

/**
* Generates: `typealias DbBookTableDefType = DbBookTableDef<DbBook>`
*/
internal object TableDefTypeAliasGenerator : YawnTableDefTypeAliasGenerator {
override fun getName(entityType: ClassName): String = "${entityType.getUniqueSimpleName()}TableDefType"

override fun getType(
entityType: ClassName,
tableDefType: ParameterizedTypeName,
): ParameterizedTypeName = tableDefType
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.faire.yawn.generators.typealiases

import com.faire.ksp.getUniqueSimpleName
import com.faire.yawn.criteria.query.TypeSafeCriteriaQuery
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.asClassName

/**
* Generates: `typealias DbBookCriteriaQuery = TypeSafeCriteriaQuery<DbBook, DbBookTableDef<DbBook>>`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be cleaner to have this be constructed using the previous simpler ones (like DbBookTableDefType)?
or is that just making it more convoluted?
(honestly not sure just something that crossed my mind)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends how deep the nesting goes. As written, I'd expect this is a bit easier to reason, if we leverage the type aliases across the generated code then I would say we should use the aliases.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I thought about this too. I lean towards not nesting the aliases to make it more grokkable for someone trying to determine what resolved type the typealias is aliasing. I think it'll be generally simpler to reason about and easier to maintain if we still with referencing the base types throughout the generated code, and only provide the typealiases for convenience to the end user

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with this fully! IMO the typealiases should just be convenience, Yawn should use its types directly internally for clarity

*/
internal object TypeSafeCriteriaQueryTypeAliasGenerator : YawnTableDefTypeAliasGenerator {
override fun getName(entityType: ClassName): String = "${entityType.getUniqueSimpleName()}CriteriaQuery"

override fun getType(
entityType: ClassName,
tableDefType: ParameterizedTypeName,
): ParameterizedTypeName = TypeSafeCriteriaQuery::class.asClassName().parameterizedBy(entityType, tableDefType)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.faire.yawn.generators.typealiases

import com.faire.ksp.getEffectiveVisibility
import com.faire.yawn.util.YawnContext
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.TypeAliasSpec
import com.squareup.kotlinpoet.ksp.toClassName

/**
* [YawnTypeAliasGenerator] for [com.faire.yawn.YawnTableDef]s and related types.
*/
internal interface YawnTableDefTypeAliasGenerator : YawnTypeAliasGenerator {
override fun generate(yawnContext: YawnContext): TypeAliasSpec {
val entityType = yawnContext.classDeclaration.toClassName()
val tableDefType = yawnContext.newClassName.parameterizedBy(entityType)
val visibility = yawnContext.classDeclaration.getEffectiveVisibility()

val typeAliasName = getName(entityType)
val typeAliasType = getType(entityType, tableDefType)

return TypeAliasSpec.builder(typeAliasName, typeAliasType)
.addModifiers(visibility)
.additionalTypeAliasBuilder(yawnContext)
.build()
}

fun getName(entityType: ClassName): String

fun getType(entityType: ClassName, tableDefType: ParameterizedTypeName): ParameterizedTypeName

fun TypeAliasSpec.Builder.additionalTypeAliasBuilder(yawnContext: YawnContext): TypeAliasSpec.Builder = this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.faire.yawn.generators.typealiases

import com.faire.yawn.util.YawnContext
import com.squareup.kotlinpoet.TypeAliasSpec

/**
* These typealias generators are used to generate typealiases for generated content.
*
* For example,
* ```
* typealias DbBookCriteriaQuery = TypeSafeCriteriaQuery<DbBook, DbBookTableDef<DbBook>>
* ```
*/
internal interface YawnTypeAliasGenerator {
fun generate(yawnContext: YawnContext): TypeAliasSpec
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.faire.yawn.YawnTableDefParent
import com.faire.yawn.generators.addGeneratedAnnotation
import com.faire.yawn.generators.objects.YawnReferenceObjectGenerator
import com.faire.yawn.util.YawnContext
import com.faire.yawn.util.addTypeAliases
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.Resolver
Expand All @@ -20,6 +21,7 @@ import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeAliasSpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName
Expand Down Expand Up @@ -63,10 +65,12 @@ internal abstract class BaseYawnProcessor(
val yawnContext = buildYawnContext(classDeclaration, newClassName)
val objectDef = objectRefGenerator.generate(yawnContext)
val classDef = generateClassDefinition(yawnContext)
val typeAliases = generateTypeAliases(yawnContext)

val fileSpec = FileSpec.builder(packageName, newClassName)
.addType(objectDef)
.addType(classDef)
.addTypeAliases(typeAliases)
.build()

val outputFile = codeGenerator.createNewFile(
Expand Down Expand Up @@ -132,6 +136,8 @@ internal abstract class BaseYawnProcessor(

protected abstract fun generateYawnDefClassName(originalClassName: ClassName): String

protected open fun generateTypeAliases(yawnContext: YawnContext): List<TypeAliasSpec> = listOf()

companion object {
const val PARENT_PARAMETER_NAME = "parent"
val parentType = YawnTableDefParent::class.asTypeName()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import com.faire.yawn.generators.properties.EmbeddedIdDefGenerator
import com.faire.yawn.generators.properties.JoinColumnDefGenerator
import com.faire.yawn.generators.properties.JoinColumnDefWithCompositeKeyGenerator
import com.faire.yawn.generators.properties.JoinColumnDefWithForeignKeyGenerator
import com.faire.yawn.generators.typealiases.JoinTypeSafeCriteriaQueryTypeAliasGenerator
import com.faire.yawn.generators.typealiases.ProjectedTypeSafeCriteriaQueryTypeAliasGenerator
import com.faire.yawn.generators.typealiases.TableDefTypeAliasGenerator
import com.faire.yawn.generators.typealiases.TypeSafeCriteriaQueryTypeAliasGenerator
import com.faire.yawn.generators.types.EmbeddedIdTypeGenerator
import com.faire.yawn.generators.types.EmbeddedTypeGenerator
import com.faire.yawn.util.YawnContext
Expand All @@ -37,6 +41,7 @@ import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeAliasSpec
import com.squareup.kotlinpoet.TypeSpec
import kotlin.reflect.KClass

Expand Down Expand Up @@ -95,6 +100,17 @@ internal class YawnEntityProcessor(codeGenerator: CodeGenerator) : BaseYawnProce
.addTypes(generateEmbeddedDefinitions(yawnContext))
}

override fun generateTypeAliases(yawnContext: YawnContext): List<TypeAliasSpec> {
val generators = listOf(
TableDefTypeAliasGenerator,
TypeSafeCriteriaQueryTypeAliasGenerator,
ProjectedTypeSafeCriteriaQueryTypeAliasGenerator,
JoinTypeSafeCriteriaQueryTypeAliasGenerator,
)
Comment on lines +104 to +109
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This list should be static or a field defintion given it doesn't change and isn't based on the yawnContext.


return generators.map { it.generate(yawnContext) }
}

/**
* Within the generated table definition, we might need to define subclasses to represent embedded definitions.
* This will be either fields tagged with @Embedded or composite primary keys tagged with @EmbeddedId.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.faire.yawn.util

import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.TypeAliasSpec

internal fun FileSpec.Builder.addTypeAliases(typeAliases: Collection<TypeAliasSpec>): FileSpec.Builder {
for (typeAlias in typeAliases) {
addTypeAlias(typeAlias)
}
return this
Comment on lines +7 to +10
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (typeAlias in typeAliases) {
addTypeAlias(typeAlias)
}
return this
return typeAliases.fold(this, (that, typeAlias) -> that.addTypeAlias(typeAlias))

though, not sure that's actually cleaner 😓

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I find the for loop a little easier to grok so gonna leave as-is, lmk if you feel strongly though

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I find this confusing but it might be because my Kotlin brain is too small

}
Loading