-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fa8f3e3
commit c6e8bc1
Showing
4 changed files
with
210 additions
and
0 deletions.
There are no files selected for viewing
95 changes: 95 additions & 0 deletions
95
komposable-architecture-compiler/src/main/kotlin/processors/StateMappingSymbolProcessor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package com.toggl.komposable.processors | ||
|
||
import com.google.devtools.ksp.KspExperimental | ||
import com.google.devtools.ksp.getAnnotationsByType | ||
import com.google.devtools.ksp.processing.CodeGenerator | ||
import com.google.devtools.ksp.processing.KSPLogger | ||
import com.google.devtools.ksp.processing.Resolver | ||
import com.google.devtools.ksp.symbol.KSClassDeclaration | ||
import com.google.devtools.ksp.symbol.KSType | ||
import com.squareup.kotlinpoet.FileSpec | ||
import com.squareup.kotlinpoet.FunSpec | ||
import com.squareup.kotlinpoet.ksp.toClassName | ||
import com.toggl.komposable.FileGenerationResult | ||
import com.toggl.komposable.architecture.ChildStates | ||
import com.toggl.komposable.getSymbolsAnnotatedWith | ||
import com.toggl.komposable.toCamelCase | ||
|
||
@OptIn(KspExperimental::class) | ||
class StateMappingSymbolProcessor( | ||
codeGenerator: CodeGenerator, | ||
logger: KSPLogger, | ||
) : KotlinPoetSymbolProcessor(codeGenerator, logger) { | ||
override fun generateFiles(resolver: Resolver): Sequence<FileGenerationResult> = | ||
resolver | ||
.getSymbolsAnnotatedWith(ChildStates::class) | ||
.map { parentState -> | ||
val parentStateClassName = parentState.toClassName() | ||
val parentStateTypeName = parentStateClassName.simpleName | ||
|
||
val parentStateArgumentName = parentStateTypeName.toCamelCase() | ||
|
||
( | ||
( | ||
parentState | ||
.annotations | ||
.toList() | ||
.single() | ||
.arguments | ||
.single() | ||
.value as ArrayList<*> | ||
) | ||
.single() as KSType | ||
) | ||
.declaration | ||
.getAnnotationsByType(ChildStates::class) | ||
.single() | ||
.childStates | ||
.fold(parentState.stateMappingFileBuilder()) { fileBuilder, childState -> | ||
val childStateTypeName = childState.simpleName.orEmpty() | ||
val childStateArgumentName = childStateTypeName.toCamelCase() | ||
|
||
fileBuilder | ||
.addFunction( | ||
FunSpec | ||
.builder("map${parentStateTypeName}To$childStateTypeName") | ||
.addParameter(parentStateArgumentName, parentState.toClassName()) | ||
.returns(childState) | ||
.addCode("return $childStateTypeName(${buildChildStateConstructorParameterList()})") | ||
.build(), | ||
) | ||
.addFunction( | ||
FunSpec | ||
.builder("map${childStateTypeName}To$parentStateTypeName") | ||
.addParameter(parentStateArgumentName, parentStateClassName) | ||
.addParameter(childStateArgumentName, childState) | ||
.returns(parentStateClassName) | ||
.addCode("return $parentStateArgumentName.copy(${buildParentStateConstructorParameterList()})") | ||
.build(), | ||
) | ||
|
||
fileBuilder | ||
}.build().run(FileGenerationResult::Success) | ||
} | ||
|
||
@Suppress("Unused") | ||
private fun buildChildStateConstructorParameterList( | ||
// childState: KClass<*>, | ||
// parentState: KSClassDeclaration | ||
): String = "" | ||
|
||
@Suppress("Unused") | ||
private fun buildParentStateConstructorParameterList( | ||
// childState: KClass<*>, | ||
// parentState: KSClassDeclaration | ||
): String = "" | ||
|
||
private fun KSClassDeclaration.stateMappingFileBuilder() = | ||
FileSpec.builder( | ||
packageName = packageName.getQualifier(), | ||
fileName = "${simpleName}StateMappings", | ||
).addImport(toClassName()) | ||
|
||
// mapToLocalState = { appState -> ListState(appState.todoList, appState.backStack) }, | ||
// mapToGlobalState = { appState, listState -> appState.copy(todoList = listState.todoList, backStack = listState.backStack) }, | ||
} |
50 changes: 50 additions & 0 deletions
50
komposable-architecture-compiler/src/test/kotlin/ActionMappingSymbolProcessorTests.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import com.toggl.komposable.processors.ActionMappingSymbolProcessorProvider | ||
import com.tschuchort.compiletesting.KotlinCompilation | ||
import com.tschuchort.compiletesting.symbolProcessorProviders | ||
import io.kotest.matchers.shouldBe | ||
import kotlin.test.Test | ||
|
||
class ActionMappingSymbolProcessorTests { | ||
@Test | ||
fun `Action mapping methods are generated`() { | ||
// Arrange | ||
val compilation = KotlinCompilation().apply { | ||
sources = listOf(SourceFiles.appAction, SourceFiles.settingsAction) | ||
symbolProcessorProviders = listOf(ActionMappingSymbolProcessorProvider()) | ||
inheritClassPath = true | ||
messageOutputStream = System.out | ||
} | ||
|
||
// Act | ||
val result = compilation.compile() | ||
|
||
// Assert | ||
result.exitCode shouldBe KotlinCompilation.ExitCode.OK | ||
|
||
val sources = result.kspGeneratedSources() | ||
sources.size.shouldBe(1) | ||
sources.single().readText().contentEquals(SourceFiles.generatedSettingsFile) | ||
} | ||
|
||
@Test | ||
fun `If a class annotated with @WrapperAction has multiple properties then the compilation fails`() { | ||
// Arrange | ||
val compilation = KotlinCompilation().apply { | ||
sources = listOf(SourceFiles.appActionWithMultipleProperties, SourceFiles.settingsAction) | ||
symbolProcessorProviders = listOf(ActionMappingSymbolProcessorProvider()) | ||
inheritClassPath = true | ||
messageOutputStream = System.out | ||
} | ||
|
||
// Act | ||
val result = compilation.compile() | ||
|
||
// Assert | ||
result.exitCode shouldBe KotlinCompilation.ExitCode.COMPILATION_ERROR | ||
|
||
result.messages.contains("AppAction.Settings needs to have exactly one property to be annotated with @WrapperAction") | ||
|
||
val sources = result.kspGeneratedSources() | ||
sources.size.shouldBe(0) | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
komposable-architecture-compiler/src/test/kotlin/Extensions.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import com.tschuchort.compiletesting.KotlinCompilation | ||
import java.io.File | ||
|
||
private val KotlinCompilation.Result.workingDir: File get() = outputDirectory.parentFile!! | ||
|
||
// HACK: Workaround for finding KSP files. See: | ||
// https://github.com/tschuchortdev/kotlin-compile-testing/issues/129 | ||
fun KotlinCompilation.Result.kspGeneratedSources(): List<File> { | ||
val kspWorkingDir = workingDir.resolve("ksp") | ||
val kspGeneratedDir = kspWorkingDir.resolve("sources") | ||
val kotlinGeneratedDir = kspGeneratedDir.resolve("kotlin") | ||
return kotlinGeneratedDir.walkTopDown().filter { it.isFile }.toList() | ||
} |
52 changes: 52 additions & 0 deletions
52
komposable-architecture-compiler/src/test/kotlin/SourceFiles.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import com.tschuchort.compiletesting.SourceFile | ||
import org.intellij.lang.annotations.Language | ||
|
||
object SourceFiles { | ||
|
||
val settingsAction = SourceFile.kotlin( | ||
"SettingsAction.kt", | ||
""" | ||
sealed interface SettingsAction { | ||
data class ChangeSomeSetting(val newValue: Boolean) : SettingsAction | ||
} | ||
""".trimIndent(), | ||
) | ||
|
||
val appAction = SourceFile.kotlin( | ||
"AppAction.kt", | ||
""" | ||
import com.toggl.komposable.architecture.WrapperAction | ||
sealed interface AppAction { | ||
data object ClearList : AppAction | ||
@WrapperAction | ||
data class Settings(val settingsAction: SettingsAction) : AppAction | ||
} | ||
""".trimIndent(), | ||
) | ||
|
||
val appActionWithMultipleProperties = SourceFile.kotlin( | ||
"AppAction.kt", | ||
""" | ||
import com.toggl.komposable.architecture.WrapperAction | ||
sealed interface AppAction { | ||
data object ClearList : AppAction | ||
@WrapperAction | ||
data class Settings(val settingsAction: SettingsAction, val foo: String) : AppAction | ||
} | ||
""".trimIndent(), | ||
) | ||
|
||
@Language("kotlin") | ||
val generatedSettingsFile = """import AppAction.Settings | ||
public fun mapAppActionToSettingsAction(appAction: AppAction): SettingsAction? = if(appAction is | ||
AppAction.Settings) appAction.settingsAction else null | ||
public fun mapSettingsActionToAppAction(settingsAction: SettingsAction): AppAction.Settings = | ||
AppAction.Settings(settingsAction) | ||
""".trimIndent() | ||
} |