Skip to content

Commit

Permalink
Replace mobiuskt-codegen-test module with compilation tests in mobius…
Browse files Browse the repository at this point in the history
…kt-codegen (#249)
  • Loading branch information
DrewCarlson committed May 26, 2024
1 parent 53eaa44 commit 19a9f4f
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 180 deletions.
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ robolectric = "4.12.2"
turbine = "1.1.0"
compose_multiplatform = "1.6.10"
androidxComposeBom = "2024.05.00"
compileTesting = "0.4.1"

[plugins]
multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
Expand All @@ -42,6 +43,8 @@ turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" }
compileTesting = { module = "dev.zacsweers.kctfork:core", version.ref = "compileTesting" }
compileTesting-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref = "compileTesting" }
androidx-livedata = { module = "androidx.lifecycle:lifecycle-livedata", version.ref = "ax_lifecycle" }
androidx-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "ax_lifecycle" }
androidx-lifecycleRuntime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "ax_lifecycle" }
Expand Down
22 changes: 0 additions & 22 deletions mobiuskt-codegen-test/build.gradle.kts

This file was deleted.

48 changes: 0 additions & 48 deletions mobiuskt-codegen-test/src/main/kotlin/test.kt

This file was deleted.

60 changes: 0 additions & 60 deletions mobiuskt-codegen-test/src/main/kotlin/withSealedClass.kt

This file was deleted.

5 changes: 4 additions & 1 deletion mobiuskt-codegen/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ plugins {
kotlin {
sourceSets.all {
languageSettings {
optIn("com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview")
optIn("com.google.devtools.ksp.KspExperimental")
optIn("kt.mobius.gen.ExperimentalCodegenApi")
}
Expand All @@ -15,8 +14,12 @@ kotlin {

dependencies {
implementation(libs.ksp)
implementation(libs.compileTesting)
implementation(libs.compileTesting.ksp)
implementation(libs.kotlinpoet)
implementation(libs.kotlinpoet.ksp)
implementation(projects.mobiusktCore)
implementation(projects.mobiusktCodegenApi)
testImplementation(kotlin("test"))
testImplementation(kotlin("test-junit"))
}
121 changes: 73 additions & 48 deletions mobiuskt-codegen/src/main/kotlin/UpdateSpecGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import kt.mobius.Update
object UpdateSpecGenerator {

fun generate(updateSymbol: KSAnnotated, codeGenerator: CodeGenerator) {

val updateParent = (updateSymbol as KSClassDeclaration).superTypes.firstOrNull { typeRef ->
typeRef.element?.typeArguments?.size == 3
}
checkNotNull(updateParent) {
"Classes annotated with @GenerateUpdate must implement Update<M, E, F>: ${updateSymbol.simpleName}"
}
// Extract Update class type information and process names
val typeArgs = updateParent.element?.typeArguments.orEmpty()
val modelClassDec = checkNotNull(typeArgs[0].type?.resolve()).declaration as KSClassDeclaration
val eventClassDec = checkNotNull(typeArgs[1].type?.resolve()).declaration as KSClassDeclaration
Expand All @@ -30,16 +30,20 @@ object UpdateSpecGenerator {
val modelClassName = modelClassDec.toClassName()
val eventClassName = eventClassDec.toClassName()
val effectClassName = effectClassDec.toClassName()
val updateTypeName = Update::class.asTypeName()
.parameterizedBy(modelClassName, eventClassName, effectClassName)

// Collect and sort event subclasses
val eventSubclasses = eventClassDec.getSealedSubclasses()
val sealedClasses = eventSubclasses.filterSealed()
val objects = eventSubclasses.filterObjects()
val dataClasses = eventSubclasses.filterDataClasses()

val nextReturnTypeName = Next::class.asTypeName()
.parameterizedBy(modelClassName, effectClassName)
val specName = "${modelClassDec.simpleName.asString().removeSuffix("Model")}GeneratedUpdate"
val specName = modelClassDec.createSpecName()

// Create the function representations for each event
val objectFunctions = objects.map { createObjectFunctionSpec(it, modelClassName, nextReturnTypeName) }.toList()
val dataClassFunctions = dataClasses.map {
createDataClassFunctionSpec(it, modelClassName, nextReturnTypeName)
Expand All @@ -60,22 +64,14 @@ object UpdateSpecGenerator {
}
}.flatten().toList()

val updateTypeName = Update::class.asTypeName()
.parameterizedBy(modelClassName, eventClassName, effectClassName)
// Construct and write the spec file
val whenBlock = createWhenBlock(objects, dataClasses, sealedClasses, specName)
val specFile = FileSpec.builder(modelClassDec.packageName.asString(), specName)
.addType(
TypeSpec.interfaceBuilder(specName)
.addModifiers(KModifier.INTERNAL)
.addSuperinterface(updateTypeName)
.addFunction(
FunSpec.builder("update")
.addModifiers(KModifier.OVERRIDE)
.addParameter("model", modelClassName)
.addParameter("event", eventClassName)
.returns(nextReturnTypeName)
.addCode(createWhenBlock(objects, dataClasses, sealedClasses, specName))
.build()
)
.addUpdateOverride(modelClassName, eventClassName, nextReturnTypeName, whenBlock)
.addFunctions(objectFunctions)
.addFunctions(dataClassFunctions)
.addFunctions(sealedClassFunctions)
Expand All @@ -89,6 +85,23 @@ object UpdateSpecGenerator {
specFile.writeTo(codeGenerator, specFile.kspDependencies(true))
}

private fun TypeSpec.Builder.addUpdateOverride(
modelClassName: ClassName,
eventClassName: ClassName,
nextReturnTypeName: ParameterizedTypeName,
whenBlock: CodeBlock
): TypeSpec.Builder {
return addFunction(
FunSpec.builder("update")
.addModifiers(KModifier.OVERRIDE)
.addParameter("model", modelClassName)
.addParameter("event", eventClassName)
.returns(nextReturnTypeName)
.addCode(whenBlock)
.build()
)
}

private fun createWhenBlock(
objects: Sequence<KSClassDeclaration>,
dataClasses: Sequence<KSClassDeclaration>,
Expand All @@ -106,28 +119,10 @@ object UpdateSpecGenerator {
}
}
.indent()
.apply {
objects.forEach {
addObjectBranch(it)
}
dataClasses.forEach {
addDataClassBranch(it)
}
sealedClasses.forEach { sealedClass ->
val sealedSubclasses = sealedClass.getSealedSubclasses()
val subObjects = sealedSubclasses.filterObjects()
val subDataClasses = sealedSubclasses.filterDataClasses()
val subSealedClasses = sealedSubclasses.filterSealed()

add(
CodeBlock.builder()
.addStatement("is %T -> ", sealedClass.toClassName())
.add(createWhenBlock(subObjects, subDataClasses, subSealedClasses, specName, false))
.build()
)
}
add("else -> error(%P)", "$specName: unexpected missing branch for \$event")
}
.addObjectBranches(objects)
.addDataClassBranches(dataClasses)
.addSealedClassBranches(sealedClasses, specName)
.add("else -> error(%P)", "$specName: unexpected missing branch for \$event")
.unindent()
.add("\n}\n")
.build()
Expand Down Expand Up @@ -169,21 +164,51 @@ object UpdateSpecGenerator {
.returns(nextReturnTypeName)
.build()

private fun CodeBlock.Builder.addDataClassBranch(ksClass: KSClassDeclaration) {
addStatement(
"is %T -> ${ksClass.asFunName()}(%L, %L)",
ksClass.toClassName(),
"model",
"event",
)
private fun CodeBlock.Builder.addSealedClassBranches(
declarations: Sequence<KSClassDeclaration>,
specName: String
): CodeBlock.Builder {
declarations.forEach { sealedClass ->
val sealedSubclasses = sealedClass.getSealedSubclasses()
val subObjects = sealedSubclasses.filterObjects()
val subDataClasses = sealedSubclasses.filterDataClasses()
val subSealedClasses = sealedSubclasses.filterSealed()

add(
CodeBlock.builder()
.addStatement("is %T -> ", sealedClass.toClassName())
.add(createWhenBlock(subObjects, subDataClasses, subSealedClasses, specName, false))
.build()
)
}
return this
}

private fun CodeBlock.Builder.addObjectBranch(it: KSClassDeclaration) {
addStatement(
"%T -> ${it.asFunName()}(%L)",
it.toClassName(),
"model"
)
private fun CodeBlock.Builder.addDataClassBranches(declarations: Sequence<KSClassDeclaration>): CodeBlock.Builder {
declarations.forEach { declaration ->
addStatement(
"is %T -> ${declaration.asFunName()}(%L, %L)",
declaration.toClassName(),
"model",
"event",
)
}
return this
}

private fun CodeBlock.Builder.addObjectBranches(declarations: Sequence<KSClassDeclaration>): CodeBlock.Builder {
declarations.forEach { declaration ->
addStatement(
"%T -> ${declaration.asFunName()}(%L)",
declaration.toClassName(),
"model"
)
}
return this
}

private fun KSClassDeclaration.createSpecName(): String {
return "${simpleName.asString().removeSuffix("Model")}GeneratedUpdate"
}

private fun KSDeclaration.asFunName(): String {
Expand Down
Loading

0 comments on commit 19a9f4f

Please sign in to comment.