From c7715ee6a23973877cd1efc253609df9c8e55b88 Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 20:48:37 +0900 Subject: [PATCH 01/13] chore: add navigation testing dependency to support navigation component tests --- app/build.gradle.kts | 1 + gradle/libs.versions.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4ee64bd..e7399d0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.navigation.testing) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3bf5224..5c69935 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,6 +53,7 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation" } +androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "composeNavigation" } # Test junit-bom = { group = "org.junit", name = "junit-bom", version.ref = "junit" } From 6fa6f83bb89c94969a014ce535f440057a30917b Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 21:41:49 +0900 Subject: [PATCH 02/13] chore: add kotlin serialization plugin to build configuration --- app/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e7399d0..3229a5d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.serialization) } android { From 6ed7ffd3220f96639e0f63c1a8254f57fc3c7b88 Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 22:00:33 +0900 Subject: [PATCH 03/13] fix: add NavigationFailed error class to WispError for better runtime error handling --- .../src/main/java/com/angrypodo/wisp/runtime/WispError.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispError.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispError.kt index ba0d563..a553e9d 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispError.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispError.kt @@ -2,7 +2,6 @@ package com.angrypodo.wisp.runtime /** * Wisp 라이브러리에서 발생하는 런타임 에러를 정의하는 Sealed Class 입니다. - * Issue #3에서 필요한 최소한의 에러 타입만 우선 정의합니다. */ sealed class WispError(override val message: String) : Exception(message) { class MissingParameter(path: String, paramName: String) : @@ -16,4 +15,7 @@ sealed class WispError(override val message: String) : Exception(message) { class UnknownPath(path: String) : WispError("The path \"$path\" is not registered with any @Wisp annotation.") + + class NavigationFailed(reason: String, detail: String?) : + WispError("Navigation failed: $reason. Detail: $detail") } From f276644f667011bbae6be39815694505195da490 Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 22:05:22 +0900 Subject: [PATCH 04/13] fix: enhance Wisp class with navigation and route resolution logic, add argument bundle creation for NavDestination --- .../java/com/angrypodo/wisp/runtime/Wisp.kt | 106 +++++++++++++++--- 1 file changed, 93 insertions(+), 13 deletions(-) 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 68f00b7..c976a8f 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 @@ -1,34 +1,114 @@ package com.angrypodo.wisp.runtime +import android.content.Context import android.net.Uri +import android.os.Bundle +import androidx.navigation.NavController +import androidx.navigation.NavDeepLinkBuilder +import androidx.navigation.NavDestination +import androidx.navigation.NavType +/** + * Wisp 라이브러리의 핵심 로직을 수행하고, 내비게이션 기능을 실행하는 클래스입니다. + * [WispRegistrySpec]과 [WispUriParser]를 주입받아 URI를 라우트 객체 리스트로 변환하고, + * [NavController]를 통해 실제 탐색을 수행합니다. + * + * 고급 사용자는 이 클래스를 직접 생성하여 DI 컨테이너로 관리할 수 있습니다. + * 대부분의 사용자는 [Wisp.initialize]와 [NavController.navigateTo] 확장 함수를 통해 Wisp를 간접적으로 사용합니다. + */ class Wisp( private val registry: WispRegistrySpec, private val parser: WispUriParser = DefaultWispUriParser() ) { + /** + * URI를 분석하여 @Serializable 라우트 객체의 리스트로 변환합니다. + */ fun resolveRoutes(uri: Uri): List { - val inputUris = parser.parse(uri) - - return inputUris.map { inputUri -> - createRouteObject(inputUri) + val paths = parser.parse(uri) + return paths.mapNotNull { path -> + registry.createRoute(path) } } - private fun createRouteObject(inputUri: String): Any { - val allPatterns = registry.getPatterns() + /** + * 주어진 라우트 객체 리스트를 사용하여 백스택을 새로 구성하고 탐색합니다. + * 백스택 생성을 위해 NavDeepLinkBuilder를 사용합니다. + */ + fun navigateTo(navController: NavController, context: Context, routes: List) { + if (routes.isEmpty()) return - for (pattern in allPatterns) { - val params = WispUriMatcher.match(inputUri, pattern) + try { + val builder = NavDeepLinkBuilder(context).setGraph(navController.graph) + routes.forEach { route -> + val routePattern = registry.getRoutePattern(route) + ?: throw IllegalArgumentException( + "Route pattern not found for ${route::class.simpleName}" + ) - if (params != null) { - val factory = registry.getRouteFactory(pattern) - ?: throw WispError.UnknownPath(pattern) + val destination = navController.graph.findNode(routePattern) + ?: throw IllegalArgumentException( + "Destination not found for route pattern: $routePattern" + ) - return factory.create(params) + builder.addDestination(destination.id, destination.buildArguments(route)) } + builder.createPendingIntent().send() + } catch (e: Exception) { + throw WispError.NavigationFailed( + reason = e::class.simpleName ?: "Unknown", + detail = e.message + ) + } + } + + /** + * Wisp의 기본 인스턴스를 제공하고 초기화하는 역할을 담당합니다. + */ + companion object { + private var instance: Wisp? = null + + /** + * 대부분의 사용자를 위한 초기화 함수입니다. + * Application.onCreate()에서 KSP가 생성한 WispRegistry를 전달하여 호출합니다. + */ + fun initialize(registry: WispRegistrySpec) { + if (instance == null) { + instance = Wisp(registry) + } + } + + /** + * 라이브러리 내부에서 기본 인스턴스를 사용하기 위한 함수입니다. + * @throws IllegalStateException Wisp.initialize()가 먼저 호출되지 않은 경우 + */ + internal fun getDefaultInstance(): Wisp { + return instance ?: throw IllegalStateException( + "Wisp.initialize() must be called first in your Application class." + ) } + } +} + +/** + * NavDestination의 정보를 기반으로 route 객체로부터 Bundle을 생성합니다. + */ +@Suppress("UNCHECKED_CAST") +private fun NavDestination.buildArguments(route: Any): Bundle? { + if (arguments.isEmpty()) return null + + if (arguments.size > 1) { + throw IllegalArgumentException( + "Destination for ${route::class.simpleName} should have only one argument " + + "for the route object, but found ${arguments.size}" + ) + } + + val argumentEntry = arguments.entries.first() + val argumentName = argumentEntry.key + val navType = argumentEntry.value.type as NavType - throw WispError.UnknownPath(inputUri) + return Bundle().apply { + navType.put(this, argumentName, route) } } From 2c5410663e36597a77574270d1d05b87c7ac7bc5 Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 22:06:20 +0900 Subject: [PATCH 05/13] refactor: update WispRegistry interface to use createRoute and getRoutePattern methods for route handling --- .../main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt index b881364..2b27ed8 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt @@ -1,6 +1,6 @@ package com.angrypodo.wisp.runtime interface WispRegistrySpec { - fun getRouteFactory(routePattern: String): RouteFactory? - fun getPatterns(): Set + fun createRoute(path: String): Any? + fun getRoutePattern(route: Any): String? } From d931c8e0be64aed3c2403ad88b5664be5a8dc459 Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 22:06:29 +0900 Subject: [PATCH 06/13] add: implement extension function for NavController to navigate using URI with error handling --- .../wisp/runtime/NavControllerWisp.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/NavControllerWisp.kt diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/NavControllerWisp.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/NavControllerWisp.kt new file mode 100644 index 0000000..711af04 --- /dev/null +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/NavControllerWisp.kt @@ -0,0 +1,20 @@ +package com.angrypodo.wisp.runtime + +import android.net.Uri +import androidx.navigation.NavController + +/** + * URI를 분석하고 즉시 백스택을 새로 구성하여 탐색하는 최종 사용자용 API입니다. + * 내부적으로 `Wisp.getDefaultInstance()`를 호출하여 모든 작업을 위임합니다. + * + * @param uri 딥링크 URI + * @throws WispError.ParsingFailed URI 파싱에 실패한 경우 + * @throws WispError.UnknownPath `WispRegistry`에 등록되지 않은 경로가 포함된 경우 + * @throws WispError.NavigationFailed 내비게이션 실행에 실패한 경우 + * @throws IllegalStateException `Wisp.initialize()`가 먼저 호출되지 않은 경우 + */ +fun NavController.navigateTo(uri: Uri) { + val wisp = Wisp.getDefaultInstance() + val routes = wisp.resolveRoutes(uri) + wisp.navigateTo(this, this.context, routes) +} From fa203fcc152b718803cbb605a0a8a07faac8f41f Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 22:34:58 +0900 Subject: [PATCH 07/13] fix: add null check and exception for route creation and argument bundle building in Wisp.kt --- .../java/com/angrypodo/wisp/runtime/Wisp.kt | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) 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 c976a8f..779c8da 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 @@ -23,11 +23,12 @@ class Wisp( /** * URI를 분석하여 @Serializable 라우트 객체의 리스트로 변환합니다. + * @throws WispError.UnknownPath 등록되지 않은 경로가 포함된 경우 */ fun resolveRoutes(uri: Uri): List { val paths = parser.parse(uri) - return paths.mapNotNull { path -> - registry.createRoute(path) + return paths.map { path -> + registry.createRoute(path) ?: throw WispError.UnknownPath(path) } } @@ -42,14 +43,10 @@ class Wisp( val builder = NavDeepLinkBuilder(context).setGraph(navController.graph) routes.forEach { route -> val routePattern = registry.getRoutePattern(route) - ?: throw IllegalArgumentException( - "Route pattern not found for ${route::class.simpleName}" - ) + ?: throw IllegalArgumentException("Route pattern not found for ${route::class.simpleName}") val destination = navController.graph.findNode(routePattern) - ?: throw IllegalArgumentException( - "Destination not found for route pattern: $routePattern" - ) + ?: throw IllegalArgumentException("Destination not found for route pattern: $routePattern") builder.addDestination(destination.id, destination.buildArguments(route)) } @@ -92,19 +89,18 @@ class Wisp( /** * NavDestination의 정보를 기반으로 route 객체로부터 Bundle을 생성합니다. + * 타입 이름을 비교하여, 여러 인자 중 @Serializable 객체 자신을 담는 인자를 정확히 찾아냅니다. */ @Suppress("UNCHECKED_CAST") private fun NavDestination.buildArguments(route: Any): Bundle? { - if (arguments.isEmpty()) return null + val argumentEntry = arguments.entries.find { (_, arg) -> + arg.type.name == route::class.qualifiedName + } - if (arguments.size > 1) { - throw IllegalArgumentException( - "Destination for ${route::class.simpleName} should have only one argument " + - "for the route object, but found ${arguments.size}" - ) + if (argumentEntry == null) { + return if (arguments.isEmpty()) null else Bundle() } - val argumentEntry = arguments.entries.first() val argumentName = argumentEntry.key val navType = argumentEntry.value.type as NavType From 75177c4f9e7ca3e0089d7ffbdae8945744015055 Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 22:37:11 +0900 Subject: [PATCH 08/13] fix: lint check --- .../src/main/java/com/angrypodo/wisp/runtime/Wisp.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 779c8da..f3fa046 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 @@ -43,10 +43,14 @@ class Wisp( val builder = NavDeepLinkBuilder(context).setGraph(navController.graph) routes.forEach { route -> val routePattern = registry.getRoutePattern(route) - ?: throw IllegalArgumentException("Route pattern not found for ${route::class.simpleName}") + ?: throw IllegalArgumentException( + "Route pattern not found for ${route::class.simpleName}" + ) val destination = navController.graph.findNode(routePattern) - ?: throw IllegalArgumentException("Destination not found for route pattern: $routePattern") + ?: throw IllegalArgumentException( + "Destination not found for route pattern: $routePattern" + ) builder.addDestination(destination.id, destination.buildArguments(route)) } From 00c226ecf8a2f7c145eb963d1cc7c709a7068c2d Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 23:04:03 +0900 Subject: [PATCH 09/13] refactor: update WispRegistry to generate route factory map and pattern matching logic --- .../java/com/angrypodo/wisp/WispClassName.kt | 3 + .../java/com/angrypodo/wisp/WispProcessor.kt | 3 +- .../wisp/generator/WispRegistryGenerator.kt | 114 +++++++++++++----- .../angrypodo/wisp/runtime/WispUriMatcher.kt | 2 +- 4 files changed, 93 insertions(+), 29 deletions(-) diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/WispClassName.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/WispClassName.kt index 2d0ceae..18b0d50 100644 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/WispClassName.kt +++ b/wisp-processor/src/main/java/com/angrypodo/wisp/WispClassName.kt @@ -7,7 +7,10 @@ internal object WispClassName { const val GENERATED_PACKAGE = "com.angrypodo.wisp.generated" val ROUTE_FACTORY = ClassName(RUNTIME_PACKAGE, "RouteFactory") + val WISP_REGISTRY_SPEC = ClassName(RUNTIME_PACKAGE, "WispRegistrySpec") + val WISP_URI_MATCHER = ClassName(RUNTIME_PACKAGE, "WispUriMatcher") + val UNKNOWN_PATH_ERROR = ClassName(RUNTIME_PACKAGE, "WispError", "UnknownPath") val MISSING_PARAMETER_ERROR = ClassName(RUNTIME_PACKAGE, "WispError", "MissingParameter") val INVALID_PARAMETER_ERROR = ClassName(RUNTIME_PACKAGE, "WispError", "InvalidParameter") } diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessor.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessor.kt index 0bc4cc5..f5922c4 100644 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessor.kt +++ b/wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessor.kt @@ -26,6 +26,7 @@ internal class WispProcessor( ) : SymbolProcessor { private val factoryGenerator = RouteFactoryGenerator(logger) + private val registryGenerator = WispRegistryGenerator() override fun process(resolver: Resolver): List { val symbols = resolver.getSymbolsWithAnnotation(WISP_ANNOTATION) @@ -101,7 +102,7 @@ internal class WispProcessor( routeInfos: List, sourceFiles: List ) { - val fileSpec = WispRegistryGenerator.generate(routeInfos) + val fileSpec = registryGenerator.generate(routeInfos) val dependencies = Dependencies(true, *sourceFiles.toTypedArray()) fileSpec.writeTo(codeGenerator, dependencies) } 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 5d57587..b8308fa 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 @@ -1,8 +1,10 @@ package com.angrypodo.wisp.generator -import com.angrypodo.wisp.WispClassName.GENERATED_PACKAGE -import com.angrypodo.wisp.WispClassName.ROUTE_FACTORY +import com.angrypodo.wisp.WispClassName +import com.angrypodo.wisp.model.ClassRouteInfo +import com.angrypodo.wisp.model.ObjectRouteInfo import com.angrypodo.wisp.model.RouteInfo +import com.squareup.kotlinpoet.ANY import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec @@ -10,55 +12,113 @@ 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.SET 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" - private const val GET_PATTERNS = "getPatterns" +internal class WispRegistryGenerator { + + private val registryName = "WispRegistry" + private val factoriesPropertyName = "factories" fun generate(routes: List): FileSpec { - val mapType = MAP.parameterizedBy(STRING, ROUTE_FACTORY) + val factoriesProperty = buildFactoriesProperty(routes) + + val registryObject = TypeSpec.objectBuilder(registryName) + .addSuperinterface(WispClassName.WISP_REGISTRY_SPEC) + .addModifiers(KModifier.PUBLIC) + .addProperty(factoriesProperty) + .addFunction(buildCreateRouteFun(factoriesProperty)) + .addFunction(buildGetRoutePatternFun(routes)) + .build() + return FileSpec.builder(WispClassName.GENERATED_PACKAGE, registryName) + .addType(registryObject) + .build() + } + + private fun buildFactoriesProperty(routes: List): PropertySpec { + val mapType = MAP.parameterizedBy(STRING, WispClassName.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) + return PropertySpec.builder(factoriesPropertyName, mapType) .addModifiers(KModifier.PRIVATE) .initializer(initializerBlock.build()) .build() + } - val getFactoryFun = FunSpec.builder(GET_FACTORY_FUN_NAME) - .addModifiers(KModifier.INTERNAL) + private fun buildCreateRouteFun(factoriesProperty: PropertySpec): FunSpec { + return FunSpec.builder("createRoute") + .addModifiers(KModifier.OVERRIDE) .addParameter("path", STRING) - .returns(ROUTE_FACTORY.copy(nullable = true)) - .addStatement("return %N[path]", factoriesProperty) + .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() + ) .build() + } - val getPatternsFun = FunSpec.builder(GET_PATTERNS) + private fun buildGetRoutePatternFun(routes: List): FunSpec { + return FunSpec.builder("getRoutePattern") .addModifiers(KModifier.OVERRIDE) - .returns(SET.parameterizedBy(STRING)) - .addStatement("return %N.keys", factoriesProperty) + .addParameter("route", ANY) + .returns(STRING.copy(nullable = true)) + .apply { + val whenBlock = CodeBlock.builder() + .beginControlFlow("return when (route)") + routes.forEach { routeInfo -> + val routePattern = buildRoutePatternString(routeInfo) + whenBlock.addStatement("is %T -> %S", routeInfo.routeClassName, routePattern) + } + whenBlock.addStatement("else -> null") + .endControlFlow() + addCode(whenBlock.build()) + } .build() + } - val registryObject = TypeSpec.objectBuilder(REGISTRY_NAME) - .addModifiers(KModifier.INTERNAL) - .addProperty(factoriesProperty) - .addFunction(getFactoryFun) - .build() + private fun buildRoutePatternString(routeInfo: RouteInfo): String { + return when (routeInfo) { + is ClassRouteInfo -> { + val pathParams = routeInfo.parameters + .filter { param -> + !param.isNullable && routeInfo.wispPath.contains( + "{${param.name}}" + ) + } + .joinToString("") { "/{${it.name}}" } - return FileSpec.builder(GENERATED_PACKAGE, REGISTRY_NAME) - .addType(registryObject) - .build() + val queryParams = routeInfo.parameters + .filterNot { param -> + !param.isNullable && routeInfo.wispPath.contains( + "{${param.name}}" + ) + } + .joinToString("&") { "${it.name}={${it.name}}" } + + val canonical = routeInfo.routeClassName.canonicalName + val query = if (queryParams.isNotEmpty()) "?$queryParams" else "" + + return "$canonical$pathParams$query" + } + is ObjectRouteInfo -> routeInfo.routeClassName.canonicalName + } } } diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispUriMatcher.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispUriMatcher.kt index 3a4e63b..7922af6 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispUriMatcher.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispUriMatcher.kt @@ -1,6 +1,6 @@ package com.angrypodo.wisp.runtime -internal object WispUriMatcher { +object WispUriMatcher { /** * 입력된 URI와 라우트 패턴을 비교하여 매칭 여부를 확인하고 파라미터를 추출합니다. From 41db12288e6cc033777bdce255beb507cdbe8023 Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 23:21:53 +0900 Subject: [PATCH 10/13] fix: remove getRoutePattern method from WispRegistrySpec interface to streamline route handling --- .../src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt index 2b27ed8..d97814a 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt @@ -2,5 +2,4 @@ package com.angrypodo.wisp.runtime interface WispRegistrySpec { fun createRoute(path: String): Any? - fun getRoutePattern(route: Any): String? } From 5281474f0d28967884381a92f0270bb4616fc622 Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 23:22:06 +0900 Subject: [PATCH 11/13] refactor: remove getRoutePattern function from WispRegistryGenerator to simplify route pattern handling --- .../wisp/generator/WispRegistryGenerator.kt | 50 ------------------- 1 file changed, 50 deletions(-) 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 b8308fa..b947863 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 @@ -1,8 +1,6 @@ package com.angrypodo.wisp.generator import com.angrypodo.wisp.WispClassName -import com.angrypodo.wisp.model.ClassRouteInfo -import com.angrypodo.wisp.model.ObjectRouteInfo import com.angrypodo.wisp.model.RouteInfo import com.squareup.kotlinpoet.ANY import com.squareup.kotlinpoet.CodeBlock @@ -28,7 +26,6 @@ internal class WispRegistryGenerator { .addModifiers(KModifier.PUBLIC) .addProperty(factoriesProperty) .addFunction(buildCreateRouteFun(factoriesProperty)) - .addFunction(buildGetRoutePatternFun(routes)) .build() return FileSpec.builder(WispClassName.GENERATED_PACKAGE, registryName) @@ -74,51 +71,4 @@ internal class WispRegistryGenerator { ) .build() } - - private fun buildGetRoutePatternFun(routes: List): FunSpec { - return FunSpec.builder("getRoutePattern") - .addModifiers(KModifier.OVERRIDE) - .addParameter("route", ANY) - .returns(STRING.copy(nullable = true)) - .apply { - val whenBlock = CodeBlock.builder() - .beginControlFlow("return when (route)") - routes.forEach { routeInfo -> - val routePattern = buildRoutePatternString(routeInfo) - whenBlock.addStatement("is %T -> %S", routeInfo.routeClassName, routePattern) - } - whenBlock.addStatement("else -> null") - .endControlFlow() - addCode(whenBlock.build()) - } - .build() - } - - private fun buildRoutePatternString(routeInfo: RouteInfo): String { - return when (routeInfo) { - is ClassRouteInfo -> { - val pathParams = routeInfo.parameters - .filter { param -> - !param.isNullable && routeInfo.wispPath.contains( - "{${param.name}}" - ) - } - .joinToString("") { "/{${it.name}}" } - - val queryParams = routeInfo.parameters - .filterNot { param -> - !param.isNullable && routeInfo.wispPath.contains( - "{${param.name}}" - ) - } - .joinToString("&") { "${it.name}={${it.name}}" } - - val canonical = routeInfo.routeClassName.canonicalName - val query = if (queryParams.isNotEmpty()) "?$queryParams" else "" - - return "$canonical$pathParams$query" - } - is ObjectRouteInfo -> routeInfo.routeClassName.canonicalName - } - } } From a58f2c9f35bccd108c8b54b325c175755aaa4cbe Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 23:26:01 +0900 Subject: [PATCH 12/13] fix: simplify navigateTo extension and route handling in Wisp.kt --- .../wisp/runtime/NavControllerWisp.kt | 2 +- .../java/com/angrypodo/wisp/runtime/Wisp.kt | 70 ++++--------------- .../wisp/runtime/WispRegistrySpec.kt | 2 +- 3 files changed, 14 insertions(+), 60 deletions(-) diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/NavControllerWisp.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/NavControllerWisp.kt index 711af04..1160d1f 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/NavControllerWisp.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/NavControllerWisp.kt @@ -16,5 +16,5 @@ import androidx.navigation.NavController fun NavController.navigateTo(uri: Uri) { val wisp = Wisp.getDefaultInstance() val routes = wisp.resolveRoutes(uri) - wisp.navigateTo(this, this.context, routes) + wisp.navigateTo(this, routes) } 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 f3fa046..20eb975 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 @@ -1,20 +1,11 @@ package com.angrypodo.wisp.runtime -import android.content.Context import android.net.Uri -import android.os.Bundle import androidx.navigation.NavController -import androidx.navigation.NavDeepLinkBuilder -import androidx.navigation.NavDestination -import androidx.navigation.NavType +import androidx.navigation.NavGraph.Companion.findStartDestination /** * Wisp 라이브러리의 핵심 로직을 수행하고, 내비게이션 기능을 실행하는 클래스입니다. - * [WispRegistrySpec]과 [WispUriParser]를 주입받아 URI를 라우트 객체 리스트로 변환하고, - * [NavController]를 통해 실제 탐색을 수행합니다. - * - * 고급 사용자는 이 클래스를 직접 생성하여 DI 컨테이너로 관리할 수 있습니다. - * 대부분의 사용자는 [Wisp.initialize]와 [NavController.navigateTo] 확장 함수를 통해 Wisp를 간접적으로 사용합니다. */ class Wisp( private val registry: WispRegistrySpec, @@ -34,27 +25,23 @@ class Wisp( /** * 주어진 라우트 객체 리스트를 사용하여 백스택을 새로 구성하고 탐색합니다. - * 백스택 생성을 위해 NavDeepLinkBuilder를 사용합니다. + * NavController.navigate를 순차적으로 호출하여 백스택을 구성합니다. */ - fun navigateTo(navController: NavController, context: Context, routes: List) { + fun navigateTo(navController: NavController, routes: List) { if (routes.isEmpty()) return try { - val builder = NavDeepLinkBuilder(context).setGraph(navController.graph) - routes.forEach { route -> - val routePattern = registry.getRoutePattern(route) - ?: throw IllegalArgumentException( - "Route pattern not found for ${route::class.simpleName}" - ) - - val destination = navController.graph.findNode(routePattern) - ?: throw IllegalArgumentException( - "Destination not found for route pattern: $routePattern" - ) + val firstRoute = routes.first() + navController.navigate(firstRoute) { + popUpTo(navController.graph.findStartDestination().id) { + inclusive = true + } + launchSingleTop = true + } - builder.addDestination(destination.id, destination.buildArguments(route)) + routes.drop(1).forEach { route -> + navController.navigate(route) } - builder.createPendingIntent().send() } catch (e: Exception) { throw WispError.NavigationFailed( reason = e::class.simpleName ?: "Unknown", @@ -63,26 +50,15 @@ class Wisp( } } - /** - * Wisp의 기본 인스턴스를 제공하고 초기화하는 역할을 담당합니다. - */ companion object { private var instance: Wisp? = null - /** - * 대부분의 사용자를 위한 초기화 함수입니다. - * Application.onCreate()에서 KSP가 생성한 WispRegistry를 전달하여 호출합니다. - */ fun initialize(registry: WispRegistrySpec) { if (instance == null) { instance = Wisp(registry) } } - /** - * 라이브러리 내부에서 기본 인스턴스를 사용하기 위한 함수입니다. - * @throws IllegalStateException Wisp.initialize()가 먼저 호출되지 않은 경우 - */ internal fun getDefaultInstance(): Wisp { return instance ?: throw IllegalStateException( "Wisp.initialize() must be called first in your Application class." @@ -90,25 +66,3 @@ class Wisp( } } } - -/** - * NavDestination의 정보를 기반으로 route 객체로부터 Bundle을 생성합니다. - * 타입 이름을 비교하여, 여러 인자 중 @Serializable 객체 자신을 담는 인자를 정확히 찾아냅니다. - */ -@Suppress("UNCHECKED_CAST") -private fun NavDestination.buildArguments(route: Any): Bundle? { - val argumentEntry = arguments.entries.find { (_, arg) -> - arg.type.name == route::class.qualifiedName - } - - if (argumentEntry == null) { - return if (arguments.isEmpty()) null else Bundle() - } - - val argumentName = argumentEntry.key - val navType = argumentEntry.value.type as NavType - - return Bundle().apply { - navType.put(this, argumentName, route) - } -} diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt index d97814a..5ac8fbe 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt @@ -2,4 +2,4 @@ package com.angrypodo.wisp.runtime interface WispRegistrySpec { fun createRoute(path: String): Any? -} +} \ No newline at end of file From 05181e6ece90e94ff7a4aef53d361873ee5079ea Mon Sep 17 00:00:00 2001 From: hanminjae Date: Mon, 24 Nov 2025 23:26:50 +0900 Subject: [PATCH 13/13] fix: lint check --- .../main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt index 5ac8fbe..d97814a 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt @@ -2,4 +2,4 @@ package com.angrypodo.wisp.runtime interface WispRegistrySpec { fun createRoute(path: String): Any? -} \ No newline at end of file +}