Skip to content
Merged
4 changes: 1 addition & 3 deletions app/src/main/java/com/angrypodo/wisp/WispSampleApp.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.angrypodo.wisp

import android.app.Application
import com.angrypodo.wisp.generated.WispRegistry
import com.angrypodo.wisp.runtime.Wisp

class WispSampleApp : Application() {
override fun onCreate() {
super.onCreate()
// KSP가 생성한 WispRegistry를 전달하여 라이브러리를 초기화합니다.
Wisp.initialize(WispRegistry)
Wisp.initialize()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.angrypodo.wisp.generator

import com.angrypodo.wisp.model.RouteInfo
import com.angrypodo.wisp.util.WispClassName
import com.squareup.kotlinpoet.ANY
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
Expand All @@ -13,24 +12,40 @@ import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.STRING
import com.squareup.kotlinpoet.TypeSpec

internal data class GeneratedRegistry(
val fileSpec: FileSpec,
val className: String
)

internal class WispRegistryGenerator {

private val registryName = "WispRegistry"
private val factoriesPropertyName = "factories"
fun generate(routes: List<RouteInfo>): GeneratedRegistry {
val hash = computeRoutesHash(routes)
val registryClassName = "WispModuleRegistry_$hash"

fun generate(routes: List<RouteInfo>): FileSpec {
val factoriesProperty = buildFactoriesProperty(routes)

val registryObject = TypeSpec.objectBuilder(registryName)
.addSuperinterface(WispClassName.WISP_REGISTRY_SPEC)
val registryObject = TypeSpec.objectBuilder(registryClassName)
.addSuperinterface(WispClassName.WISP_MODULE_REGISTRY) // Changed interface
.addModifiers(KModifier.PUBLIC)
.addProperty(factoriesProperty)
.addFunction(buildCreateRouteFun(factoriesProperty))
.addFunction(buildGetRoutesFun(factoriesProperty))
.build()

return FileSpec.builder(WispClassName.GENERATED_PACKAGE, registryName)
val fileSpec = FileSpec.builder(WispClassName.GENERATED_PACKAGE, registryClassName)
.addType(registryObject)
.build()

return GeneratedRegistry(
fileSpec = fileSpec,
className = "${WispClassName.GENERATED_PACKAGE}.$registryClassName"
)
}

private fun computeRoutesHash(routes: List<RouteInfo>): String {
// Sort routes to ensure deterministic hash
val sortedPaths = routes.map { it.wispPath }.sorted()
return sortedPaths.joinToString("|").hashCode().toString().replace("-", "N")
}

private fun buildFactoriesProperty(routes: List<RouteInfo>): PropertySpec {
Expand All @@ -43,32 +58,18 @@ internal class WispRegistryGenerator {
}
initializerBlock.unindent().add(")")

return PropertySpec.builder(factoriesPropertyName, mapType)
return PropertySpec.builder("factories", mapType)
.addModifiers(KModifier.PRIVATE)
.initializer(initializerBlock.build())
.build()
}

private fun buildCreateRouteFun(factoriesProperty: PropertySpec): FunSpec {
return FunSpec.builder("createRoute")
private fun buildGetRoutesFun(factoriesProperty: PropertySpec): FunSpec {
val returnType = MAP.parameterizedBy(STRING, WispClassName.ROUTE_FACTORY)
return FunSpec.builder("getRoutes")
.addModifiers(KModifier.OVERRIDE)
.addParameter("path", STRING)
.returns(ANY.copy(nullable = true))
.addCode(
CodeBlock.builder()
.beginControlFlow("for (pattern in %N.keys)", factoriesProperty)
.addStatement(
"val params = %T.match(path, pattern)",
WispClassName.WISP_URI_MATCHER
)
.beginControlFlow("if (params != null)")
.addStatement("val factory = %N[pattern]", factoriesProperty)
.addStatement("return factory?.create(params)")
.endControlFlow()
.endControlFlow()
.addStatement("return null")
.build()
)
.returns(returnType)
.addStatement("return %N", factoriesProperty)
.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,24 @@ internal class WispProcessor(
routeInfos: List<RouteInfo>,
sourceFiles: List<KSFile>
) {
val fileSpec = registryGenerator.generate(routeInfos)
val generatedRegistry = registryGenerator.generate(routeInfos)
val dependencies = Dependencies(true, *sourceFiles.toTypedArray())
fileSpec.writeTo(codeGenerator, dependencies)

generatedRegistry.fileSpec.writeTo(codeGenerator, dependencies)

val resourceFile = "META-INF/services/com.angrypodo.wisp.runtime.spi.WispModuleRegistry"
try {
codeGenerator.createNewFile(
dependencies = dependencies,
packageName = "",
fileName = resourceFile,
extensionName = ""
).use { outputStream ->
outputStream.write(generatedRegistry.className.toByteArray())
}
} catch (e: Exception) {
logger.error("Failed to generate ServiceLoader metadata: ${e.message}")
}
}

private fun KSClassDeclaration.hasSerializableAnnotation(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ internal object WispClassName {
const val GENERATED_PACKAGE = "com.angrypodo.wisp.generated"

val ROUTE_FACTORY = ClassName("com.angrypodo.wisp.runtime.spi", "RouteFactory")
val WISP_REGISTRY_SPEC = ClassName("com.angrypodo.wisp.runtime.spi", "WispRegistrySpec")
val WISP_URI_MATCHER = ClassName("com.angrypodo.wisp.runtime.matcher", "WispUriMatcher")
val WISP_MODULE_REGISTRY = ClassName("com.angrypodo.wisp.runtime.spi", "WispModuleRegistry")

val UNKNOWN_PATH_ERROR = ClassName(RUNTIME_PACKAGE, "WispError", "UnknownPath")
val MISSING_PARAMETER_ERROR = ClassName(RUNTIME_PACKAGE, "WispError", "MissingParameter")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test
internal class RegistryGeneratorTest {

@Test
@DisplayName("RouteInfo를 받아 WispRegistry 오브젝트와 맵을 생성한다")
@DisplayName("RouteInfo를 받아 WispModuleRegistry 오브젝트와 맵을 생성한다")
fun `generate_registry_with_multiple_routes`() {
// Given: RouteInfo 데이터 2개
val homeRoute = ObjectRouteInfo(
Expand All @@ -29,13 +29,19 @@ internal class RegistryGeneratorTest {
val routes = listOf(homeRoute, profileRoute)

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

// Then: 생성된 WispRegistry 객체를 반환
assertTrue(generatedCode.contains("object WispRegistry"))
val generatedRegistry = WispRegistryGenerator().generate(routes)
val generatedCode = generatedRegistry.fileSpec.toString()

// Then: 생성된 WispModuleRegistry 객체를 반환 (해시값이 포함된 이름)
assertTrue(generatedCode.contains("object WispModuleRegistry_"))
// SPI 인터페이스 구현 확인
assertTrue(generatedCode.contains(": WispModuleRegistry"))
// 맵 생성 확인
assertTrue(generatedCode.contains("val factories: Map<String, RouteFactory> = mapOf("))
// getRoutes 메서드 확인
assertTrue(generatedCode.contains("override fun getRoutes"))

// 라우트 팩토리 매핑 확인
assertTrue(generatedCode.contains("import com.example.HomeRouteFactory"))
assertTrue(generatedCode.contains("\"home\" to HomeRouteFactory"))

Expand Down
30 changes: 25 additions & 5 deletions wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/Wisp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ package com.angrypodo.wisp.runtime
import android.net.Uri
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import com.angrypodo.wisp.runtime.matcher.WispUriMatcher
import com.angrypodo.wisp.runtime.parser.DefaultWispUriParser
import com.angrypodo.wisp.runtime.parser.WispUriParser
import com.angrypodo.wisp.runtime.spi.WispRegistrySpec
import com.angrypodo.wisp.runtime.spi.RouteFactory
import com.angrypodo.wisp.runtime.spi.WispModuleRegistry
import java.util.ServiceLoader

/**
* Wisp 라이브러리의 핵심 로직을 수행하고, 내비게이션 기능을 실행하는 클래스입니다.
*/
class Wisp(
private val registry: WispRegistrySpec,
private val mergedRoutes: Map<String, RouteFactory>,
private val parser: WispUriParser = DefaultWispUriParser()
) {

Expand All @@ -22,10 +25,20 @@ class Wisp(
fun resolveRoutes(uri: Uri): List<Any> {
val paths = parser.parse(uri)
return paths.map { path ->
registry.createRoute(path) ?: throw WispError.UnknownPath(path)
matchAndCreate(path) ?: throw WispError.UnknownPath(path)
}
}

private fun matchAndCreate(path: String): Any? {
for ((pattern, factory) in mergedRoutes) {
val params = WispUriMatcher.match(path, pattern)
if (params != null) {
return factory.create(params)
}
}
return null
}

/**
* 주어진 라우트 객체 리스트를 사용하여 백스택을 새로 구성하고 탐색합니다.
* NavController.navigate를 순차적으로 호출하여 백스택을 구성합니다.
Expand Down Expand Up @@ -58,9 +71,16 @@ class Wisp(

@JvmStatic
@Synchronized
fun initialize(registry: WispRegistrySpec) {
fun initialize() {
if (instance == null) {
instance = Wisp(registry)
val aggregatedRoutes = mutableMapOf<String, RouteFactory>()
val loader = ServiceLoader.load(WispModuleRegistry::class.java)

for (registry in loader) {
aggregatedRoutes.putAll(registry.getRoutes())
}

instance = Wisp(aggregatedRoutes)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.angrypodo.wisp.runtime.spi

interface WispModuleRegistry {
fun getRoutes(): Map<String, RouteFactory>
}

This file was deleted.

Loading