Skip to content
13 changes: 13 additions & 0 deletions wisp-processor/src/main/java/com/angrypodo/wisp/WispClassName.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.angrypodo.wisp

import com.squareup.kotlinpoet.ClassName

internal object WispClassName {
private const val RUNTIME_PACKAGE = "com.angrypodo.wisp.runtime"
const val GENERATED_PACKAGE = "com.angrypodo.wisp.generated"

val ROUTE_FACTORY = ClassName(RUNTIME_PACKAGE, "RouteFactory")

val MISSING_PARAMETER_ERROR = ClassName(RUNTIME_PACKAGE, "WispError", "MissingParameter")
val INVALID_PARAMETER_ERROR = ClassName(RUNTIME_PACKAGE, "WispError", "InvalidParameter")
}
41 changes: 35 additions & 6 deletions wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessor.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.angrypodo.wisp

import com.angrypodo.wisp.WispValidator.validateDuplicatePaths
import com.angrypodo.wisp.annotations.Wisp
import com.angrypodo.wisp.generator.RouteFactoryGenerator
import com.angrypodo.wisp.generator.WispRegistryGenerator
import com.angrypodo.wisp.mapper.toRouteInfo
import com.angrypodo.wisp.model.RouteInfo
import com.google.devtools.ksp.processing.CodeGenerator
Expand All @@ -11,6 +13,7 @@ import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFile
import com.google.devtools.ksp.validate
import com.squareup.kotlinpoet.ksp.writeTo

Expand All @@ -32,22 +35,39 @@ internal class WispProcessor(

val (processableSymbols, deferredSymbols) = symbols.partition { it.validate() }

processableSymbols.forEach { processSymbol(it) }
val routesWithSymbols = processableSymbols.mapNotNull { routeClass ->
val routeInfo = processSymbol(routeClass) ?: return@mapNotNull null
routeInfo to routeClass
}

val routeInfos = routesWithSymbols.map { it.first }

val duplicateValidationResult = validateDuplicatePaths(routeInfos)

if (duplicateValidationResult is WispValidator.ValidationResult.Failure) {
duplicateValidationResult.errors.forEach { logger.error(it) }
return deferredSymbols
}

if (routesWithSymbols.isNotEmpty()) {
val sourceFiles = routesWithSymbols.mapNotNull { it.second.containingFile }.distinct()
generateRouteRegistry(routeInfos, sourceFiles)
}

return deferredSymbols
}

private fun processSymbol(routeClass: KSClassDeclaration) {
if (!validateSerializable(routeClass)) {
return
}
private fun processSymbol(routeClass: KSClassDeclaration): RouteInfo? {
if (!validateSerializable(routeClass)) return null

val routeInfo = routeClass.toRouteInfo() ?: run {
logInvalidRouteError(routeClass)
return
return null
}

generateRouteFactory(routeClass, routeInfo)

return routeInfo
}

private fun validateSerializable(routeClass: KSClassDeclaration): Boolean {
Expand Down Expand Up @@ -77,6 +97,15 @@ internal class WispProcessor(
fileSpec.writeTo(codeGenerator, dependencies)
}

private fun generateRouteRegistry(
routeInfos: List<RouteInfo>,
sourceFiles: List<KSFile>
) {
val fileSpec = WispRegistryGenerator.generate(routeInfos)
val dependencies = Dependencies(true, *sourceFiles.toTypedArray())
fileSpec.writeTo(codeGenerator, dependencies)
}

private fun KSClassDeclaration.hasSerializableAnnotation(): Boolean {
return annotations.any { annotation ->
val shortName = annotation.shortName.asString()
Expand Down
24 changes: 21 additions & 3 deletions wisp-processor/src/main/java/com/angrypodo/wisp/WispValidator.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
package com.angrypodo.wisp

import com.angrypodo.wisp.model.RouteInfo

internal object WispValidator {
sealed interface ValidationResult {
data object Success : ValidationResult
data class Failure(val message: String) : ValidationResult
data class Failure(val errors: List<String>) : ValidationResult
}

fun validate(routeInfo: RouteClassInfo): ValidationResult {
if (!routeInfo.isSerializable()) {
return ValidationResult.Failure(
message = "Wisp Error: Route Class '${routeInfo.qualifiedName}' " +
"must be annotated with @Serializable."
listOf(
"Wisp Error: Route Class '${routeInfo.qualifiedName}' " +
"must be annotated with @Serializable."
)
)
}

return ValidationResult.Success
}

fun validateDuplicatePaths(routes: List<RouteInfo>): ValidationResult {
val duplicates = routes.groupBy { it.wispPath }
.filter { it.value.size > 1 }

if (duplicates.isEmpty()) return ValidationResult.Success

val errorMessages = duplicates.map { (path, routeInfos) ->
val conflictingClasses = routeInfos.joinToString(", ") { it.routeClassName.simpleName }
"Wisp Error: The path '$path' is already used by multiple routes: [$conflictingClasses]"
}

return ValidationResult.Failure(errorMessages)
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.angrypodo.wisp.generator

import com.angrypodo.wisp.WispClassName.INVALID_PARAMETER_ERROR
import com.angrypodo.wisp.WispClassName.MISSING_PARAMETER_ERROR
import com.angrypodo.wisp.WispClassName.ROUTE_FACTORY
import com.angrypodo.wisp.model.ClassRouteInfo
import com.angrypodo.wisp.model.ObjectRouteInfo
import com.angrypodo.wisp.model.ParameterInfo
import com.angrypodo.wisp.model.RouteInfo
import com.google.devtools.ksp.processing.KSPLogger
import com.squareup.kotlinpoet.ANY
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.DOUBLE
import com.squareup.kotlinpoet.FLOAT
Expand All @@ -24,19 +26,6 @@ import com.squareup.kotlinpoet.TypeSpec
internal class RouteFactoryGenerator(
private val logger: KSPLogger
) {

private val routeFactoryInterface = ClassName("com.angrypodo.wisp.runtime", "RouteFactory")
private val missingParameterError = ClassName(
"com.angrypodo.wisp.runtime",
"WispError",
"MissingParameter"
)
private val invalidParameterError = ClassName(
"com.angrypodo.wisp.runtime",
"WispError",
"InvalidParameter"
)

fun generate(routeInfo: RouteInfo): FileSpec {
val createFun = FunSpec.builder("create")
.addModifiers(KModifier.OVERRIDE)
Expand All @@ -47,7 +36,7 @@ internal class RouteFactoryGenerator(

val factoryObject = TypeSpec.objectBuilder(routeInfo.factoryClassName)
.addModifiers(KModifier.INTERNAL)
.addSuperinterface(routeFactoryInterface)
.addSuperinterface(ROUTE_FACTORY)
Copy link
Owner

Choose a reason for hiding this comment

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

이거 아주 좋네요👍🏻
WispError관련 ClassName도 옮겨도 좋다고 생각해요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

바로 수정하겠습니다!

.addFunction(createFun)
.build()

Expand Down Expand Up @@ -87,8 +76,8 @@ internal class RouteFactoryGenerator(

val nonNullableType = param.typeName.copy(nullable = false)
val errorType = when (nonNullableType) {
STRING -> missingParameterError
else -> invalidParameterError
STRING -> MISSING_PARAMETER_ERROR
else -> INVALID_PARAMETER_ERROR
}

return CodeBlock.of(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.angrypodo.wisp.generator

import com.angrypodo.wisp.WispClassName.GENERATED_PACKAGE
import com.angrypodo.wisp.WispClassName.ROUTE_FACTORY
import com.angrypodo.wisp.model.RouteInfo
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.MAP
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.STRING
import com.squareup.kotlinpoet.TypeSpec

internal object WispRegistryGenerator {
private const val REGISTRY_NAME = "WispRegistry"
private const val FACTORIES_PROPERTY_NAME = "factories"
private const val GET_FACTORY_FUN_NAME = "getRouteFactory"

fun generate(routes: List<RouteInfo>): FileSpec {
val mapType = MAP.parameterizedBy(STRING, ROUTE_FACTORY)

val initializerBlock = CodeBlock.builder()
.add("mapOf(\n")
.indent()

routes.forEach { route ->
initializerBlock.add("%S to %T,\n", route.wispPath, route.factoryClassName)
}

initializerBlock.unindent().add(")")

val factoriesProperty = PropertySpec.builder(FACTORIES_PROPERTY_NAME, mapType)
.addModifiers(KModifier.PRIVATE)
.initializer(initializerBlock.build())
.build()

val getFactoryFun = FunSpec.builder(GET_FACTORY_FUN_NAME)
.addModifiers(KModifier.INTERNAL)
.addParameter("path", STRING)
.returns(ROUTE_FACTORY.copy(nullable = true))
.addStatement("return %N[path]", factoriesProperty)
.build()

val registryObject = TypeSpec.objectBuilder(REGISTRY_NAME)
.addModifiers(KModifier.INTERNAL)
.addProperty(factoriesProperty)
.addFunction(getFactoryFun)
.build()

return FileSpec.builder(GENERATED_PACKAGE, REGISTRY_NAME)
.addType(registryObject)
.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ class WispValidatorTest {
assertTrue(result is WispValidator.ValidationResult.Failure) {
"결과 타입은 Failure여야 합니다."
}
assertEquals(expectedMessage, (result as WispValidator.ValidationResult.Failure).message)
assertEquals(
listOf(expectedMessage),
(result as WispValidator.ValidationResult.Failure).errors
)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.angrypodo.wisp.generator

import com.angrypodo.wisp.model.ClassRouteInfo
import com.angrypodo.wisp.model.ObjectRouteInfo
import com.squareup.kotlinpoet.ClassName
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

internal class RegistryGeneratorTest {

@Test
@DisplayName("RouteInfo를 받아 WispRegistry 오브젝트와 맵을 생성한다")
fun `generate_registry_with_multiple_routes`() {
// Given: RouteInfo 데이터 2개
val homeRoute = ObjectRouteInfo(
routeClassName = ClassName("com.example", "Home"),
factoryClassName = ClassName("com.example", "HomeRouteFactory"),
wispPath = "home"
)

val profileRoute = ClassRouteInfo(
routeClassName = ClassName("com.example", "Profile"),
factoryClassName = ClassName("com.example", "ProfileRouteFactory"),
wispPath = "profile/{id}",
parameters = emptyList()
)

val routes = listOf(homeRoute, profileRoute)

// When: 코드 생성 실행
val fileSpec = WispRegistryGenerator.generate(routes)
val generatedCode = fileSpec.toString()

println(generatedCode)

// Then: 생성된 WispRegistry 객체를 반환
assertTrue(generatedCode.contains("object WispRegistry"))
assertTrue(generatedCode.contains("val factories: Map<String, RouteFactory> = mapOf("))

assertTrue(generatedCode.contains("import com.example.HomeRouteFactory"))
assertTrue(generatedCode.contains("\"home\" to HomeRouteFactory"))

assertTrue(generatedCode.contains("import com.example.ProfileRouteFactory"))
assertTrue(generatedCode.contains("\"profile/{id}\" to ProfileRouteFactory"))
}
}
Loading