Skip to content
10 changes: 10 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,10 @@
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")
}
54 changes: 48 additions & 6 deletions wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.angrypodo.wisp

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 +12,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 +34,53 @@ 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
}

if (!validateDuplicatePaths(routesWithSymbols)) return deferredSymbols

if (routesWithSymbols.isNotEmpty()) {
val routeInfos = routesWithSymbols.map { it.first }
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 validateDuplicatePaths(
routesWithSymbols: List<Pair<RouteInfo, KSClassDeclaration>>
): Boolean {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

이 부분도 구조적인 측면에서 제안이 있어요😊
이 검증 함수를 WispValidator클래스로 옮기는 건 어떻게 생각하시나요?
저는 WispProcessor는 심볼을 찾고 생성기를 호출하는 흐름을 담당하고 Wispvalidator는 모든 종류의 유효성 검사를 책임지는 구조가 될것 같아요🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

좋은 제안입니다! 😊 작성할 때 기존에 있던 validateSerializable 때문에 검증 로직을 어디에 두는 게 맞을지 저도 살짝 고민했었거든요. ㅎㅎ 제안해주신 대로 WispValidator 객체에서 유효성 검사를 할 수 있도록 리팩토링 진행하겠습니다!

val duplicates = routesWithSymbols.groupBy { it.first.wispPath }
.filter { it.value.size > 1 }

if (duplicates.isEmpty()) return true

duplicates.forEach { (path, pairs) ->
pairs.forEach { (_, symbol) ->
logger.error(
"Wisp Error: The path '$path' is already used by another route.",
symbol
)
}
}
return false
}

private fun validateSerializable(routeClass: KSClassDeclaration): Boolean {
Expand Down Expand Up @@ -77,6 +110,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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.angrypodo.wisp.generator

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
Expand All @@ -25,7 +26,6 @@ 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",
Expand All @@ -47,7 +47,7 @@ internal class RouteFactoryGenerator(

val factoryObject = TypeSpec.objectBuilder(routeInfo.factoryClassName)
.addModifiers(KModifier.INTERNAL)
.addSuperinterface(routeFactoryInterface)
.addSuperinterface(ROUTE_FACTORY)
Copy link
Copy Markdown
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
Copy Markdown
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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.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"

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", mapType)
.addModifiers(KModifier.INTERNAL)
.initializer(initializerBlock.build())
.build()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

생성 로직 잘 구현해주셨네요! 고생하셨습니다🥲
여기서 한가지 제안은 WispRegistry 내부에 registry라는 이름의 private 맵을 만들고 이 맵을 사용하는 createRoute같은 public 함수를 만드는건 어떨까요?

위의 제안은 향후에 외부에서 registry맵에 직접 접근하는 것을 막아서 더 안정적인 구조를 만들 수 있을 것 같습니다👍🏻

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

좋은 아이디어 같습니다👍 레지스트리 객체를 생성하는 것에만 집중하다 보니 캡슐화와 유지보수 측면을 놓쳤던 것 같습니다. 말씀하신 대로 registryprivate으로 숨기고 접근 메서드를 제공하는 구조가 훨씬 안정적이겠네요. 바로 반영하겠습니다!


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

return FileSpec.builder(GENERATED_PACKAGE, REGISTRY_NAME)
.addType(registryObject)
.build()
}
}
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