diff --git a/app/src/main/java/com/angrypodo/wisp/WispSampleApp.kt b/app/src/main/java/com/angrypodo/wisp/WispSampleApp.kt index 30566f2..5d4a427 100644 --- a/app/src/main/java/com/angrypodo/wisp/WispSampleApp.kt +++ b/app/src/main/java/com/angrypodo/wisp/WispSampleApp.kt @@ -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() } } diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/generator/WispRegistryGenerator.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/generator/WispRegistryGenerator.kt index f5a753a..e5e8bc9 100644 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/generator/WispRegistryGenerator.kt +++ b/wisp-processor/src/main/java/com/angrypodo/wisp/generator/WispRegistryGenerator.kt @@ -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 @@ -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): GeneratedRegistry { + val hash = computeRoutesHash(routes) + val registryClassName = "WispModuleRegistry_$hash" - fun generate(routes: List): 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): 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): PropertySpec { @@ -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() } } diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/processor/WispProcessor.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/processor/WispProcessor.kt index 281b923..e129539 100644 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/processor/WispProcessor.kt +++ b/wisp-processor/src/main/java/com/angrypodo/wisp/processor/WispProcessor.kt @@ -102,9 +102,24 @@ internal class WispProcessor( routeInfos: List, sourceFiles: List ) { - 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 { diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/util/WispClassName.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/util/WispClassName.kt index 51d9d19..1df7ef5 100644 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/util/WispClassName.kt +++ b/wisp-processor/src/main/java/com/angrypodo/wisp/util/WispClassName.kt @@ -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") diff --git a/wisp-processor/src/test/java/com/angrypodo/wisp/generator/RegistryGeneratorTest.kt b/wisp-processor/src/test/java/com/angrypodo/wisp/generator/RegistryGeneratorTest.kt index 5355b10..e47e003 100644 --- a/wisp-processor/src/test/java/com/angrypodo/wisp/generator/RegistryGeneratorTest.kt +++ b/wisp-processor/src/test/java/com/angrypodo/wisp/generator/RegistryGeneratorTest.kt @@ -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( @@ -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 = mapOf(")) + // getRoutes 메서드 확인 + assertTrue(generatedCode.contains("override fun getRoutes")) + // 라우트 팩토리 매핑 확인 assertTrue(generatedCode.contains("import com.example.HomeRouteFactory")) assertTrue(generatedCode.contains("\"home\" to HomeRouteFactory")) diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/Wisp.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/Wisp.kt index 8ee96d3..ca147c8 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/Wisp.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/Wisp.kt @@ -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, private val parser: WispUriParser = DefaultWispUriParser() ) { @@ -22,10 +25,20 @@ class Wisp( fun resolveRoutes(uri: Uri): List { 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를 순차적으로 호출하여 백스택을 구성합니다. @@ -58,9 +71,16 @@ class Wisp( @JvmStatic @Synchronized - fun initialize(registry: WispRegistrySpec) { + fun initialize() { if (instance == null) { - instance = Wisp(registry) + val aggregatedRoutes = mutableMapOf() + val loader = ServiceLoader.load(WispModuleRegistry::class.java) + + for (registry in loader) { + aggregatedRoutes.putAll(registry.getRoutes()) + } + + instance = Wisp(aggregatedRoutes) } } diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/WispModuleRegistry.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/WispModuleRegistry.kt new file mode 100644 index 0000000..5b6bfd3 --- /dev/null +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/WispModuleRegistry.kt @@ -0,0 +1,5 @@ +package com.angrypodo.wisp.runtime.spi + +interface WispModuleRegistry { + fun getRoutes(): Map +} diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/WispRegistrySpec.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/WispRegistrySpec.kt deleted file mode 100644 index cb825f1..0000000 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/WispRegistrySpec.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.angrypodo.wisp.runtime.spi - -interface WispRegistrySpec { - fun createRoute(path: String): Any? -}