diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e03d95c..985f6d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,13 @@ on: push: branches: [ main, develop ] pull_request: + types: [opened, synchronize, reopened] branches: [ main, develop ] + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true jobs: lint: @@ -12,6 +18,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Set up JDK 17 uses: actions/setup-java@v4 @@ -30,6 +38,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Set up JDK 17 uses: actions/setup-java@v4 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 765043d..7beb525 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ android:supportsRtl="true" android:theme="@style/Theme.Wisp"> diff --git a/app/src/main/java/com/angrypodo/wisp/MainActivity.kt b/app/src/main/java/com/angrypodo/wisp/MainActivity.kt deleted file mode 100644 index ba72628..0000000 --- a/app/src/main/java/com/angrypodo/wisp/MainActivity.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.angrypodo.wisp - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.ui.Modifier -import androidx.core.net.toUri -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute -import com.angrypodo.wisp.runtime.navigateTo -import com.angrypodo.wisp.ui.screens.HomeScreen -import com.angrypodo.wisp.ui.screens.ProductDetailScreen -import com.angrypodo.wisp.ui.screens.SettingsScreen -import com.angrypodo.wisp.ui.theme.WispTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - WispTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - val navController = rememberNavController() - NavHost(navController = navController, startDestination = Home) { - composable { - HomeScreen( - onNavigateToProduct = { - // "product/123"과 "settings"를 백스택으로 하는 딥링크 URI - val deepLinkUri = - "app://wisp?stack=product/123|settings".toUri() - navController.navigateTo(deepLinkUri) - }, - onNavigateToSettings = { - // 단일 목적지로 이동하는 딥링크 URI - val settingsUri = "app://wisp?stack=settings".toUri() - navController.navigateTo(settingsUri) - } - ) - } - composable { backStackEntry -> - val productDetail: ProductDetail = backStackEntry.toRoute() - ProductDetailScreen(productId = productDetail.productId) - } - composable { - SettingsScreen() - } - } - } - } - } - } -} diff --git a/app/src/main/java/com/angrypodo/wisp/Routes.kt b/app/src/main/java/com/angrypodo/wisp/navigation/Routes.kt similarity index 75% rename from app/src/main/java/com/angrypodo/wisp/Routes.kt rename to app/src/main/java/com/angrypodo/wisp/navigation/Routes.kt index a5bb596..fd49259 100644 --- a/app/src/main/java/com/angrypodo/wisp/Routes.kt +++ b/app/src/main/java/com/angrypodo/wisp/navigation/Routes.kt @@ -1,4 +1,4 @@ -package com.angrypodo.wisp +package com.angrypodo.wisp.navigation import com.angrypodo.wisp.annotations.Wisp import kotlinx.serialization.Serializable @@ -14,3 +14,7 @@ data class ProductDetail(val productId: String) @Serializable @Wisp("settings") data object Settings + +@Serializable +@Wisp("splash") +data object Splash diff --git a/app/src/main/java/com/angrypodo/wisp/ui/main/MainActivity.kt b/app/src/main/java/com/angrypodo/wisp/ui/main/MainActivity.kt new file mode 100644 index 0000000..1d0e134 --- /dev/null +++ b/app/src/main/java/com/angrypodo/wisp/ui/main/MainActivity.kt @@ -0,0 +1,77 @@ +package com.angrypodo.wisp.ui.main + +import android.net.Uri +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.core.net.toUri +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import com.angrypodo.wisp.navigation.Home +import com.angrypodo.wisp.navigation.ProductDetail +import com.angrypodo.wisp.navigation.Settings +import com.angrypodo.wisp.navigation.Splash +import com.angrypodo.wisp.runtime.navigateTo +import com.angrypodo.wisp.ui.screens.HomeScreen +import com.angrypodo.wisp.ui.screens.ProductDetailScreen +import com.angrypodo.wisp.ui.screens.SettingsScreen +import com.angrypodo.wisp.ui.screens.SplashScreen +import com.angrypodo.wisp.ui.theme.WispTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val deepLinkUri: Uri? = intent?.data + setContent { + WispTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + WispNavHost(deepLinkUri = deepLinkUri) + } + } + } + } +} + +@Composable +private fun WispNavHost(deepLinkUri: Uri?) { + val navController = rememberNavController() + + NavHost(navController = navController, startDestination = Splash) { + composable { + SplashScreen( + navController = navController, + uri = deepLinkUri + ) + } + + composable { + HomeScreen( + onNavigateToProduct = { + val uri = "app://wisp?stack=product/123|settings".toUri() + navController.navigateTo(uri) + }, + onNavigateToSettings = { + val uri = "app://wisp?stack=settings".toUri() + navController.navigateTo(uri) + } + ) + } + composable { backStackEntry -> + val productDetail: ProductDetail = backStackEntry.toRoute() + ProductDetailScreen(productId = productDetail.productId) + } + composable { + SettingsScreen() + } + } +} diff --git a/app/src/main/java/com/angrypodo/wisp/ui/screens/SampleScreens.kt b/app/src/main/java/com/angrypodo/wisp/ui/screens/SampleScreens.kt index a2dd45a..8edfc91 100644 --- a/app/src/main/java/com/angrypodo/wisp/ui/screens/SampleScreens.kt +++ b/app/src/main/java/com/angrypodo/wisp/ui/screens/SampleScreens.kt @@ -1,5 +1,6 @@ package com.angrypodo.wisp.ui.screens +import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -8,10 +9,15 @@ import androidx.compose.foundation.layout.height import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.angrypodo.wisp.navigation.Home +import com.angrypodo.wisp.runtime.Wisp +import kotlinx.coroutines.delay @Composable fun HomeScreen(onNavigateToProduct: () -> Unit, onNavigateToSettings: () -> Unit) { @@ -55,3 +61,42 @@ fun SettingsScreen() { Text(text = "Settings Screen", fontSize = 24.sp) } } + +@Composable +fun SplashScreen( + navController: NavController, + uri: Uri? +) { + val wisp = Wisp.getDefaultInstance() + + // A real app would check login status, fetch initial data, etc. + // Here we just simulate a delay. + LaunchedEffect(Unit) { + delay(1500) + + val routes = uri?.let { wisp.resolveRoutes(it) } + + if (routes.isNullOrEmpty()) { + // No deep link, navigate to home as usual. + navController.navigate(Home) { + popUpTo(navController.graph.id) { inclusive = true } + } + } else { + // Deep link found. + // The first route is "splash", which we are already on. + // We navigate to the rest of the stack. + val routesToNavigate = routes.drop(1) + wisp.navigateTo(navController, routesToNavigate) + } + } + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "Wisp", fontSize = 48.sp) + Spacer(modifier = Modifier.height(16.dp)) + Text(text = "Loading...", fontSize = 24.sp) + } +} diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/ProcessorsUtils.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/ProcessorsUtils.kt deleted file mode 100644 index b0ecd22..0000000 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/ProcessorsUtils.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.angrypodo.wisp - -private const val SERIALIZABLE_SHORT_NAME = "Serializable" -private const val SERIALIZABLE_ANNOTATION = "kotlinx.serialization.Serializable" - -/** - * RouteClassInfo가 @Serializable 어노테이션을 가지고 있는지 검사하는 함수입니다. - */ -internal fun RouteClassInfo.isSerializable(): Boolean = annotations.any { - it.shortName == SERIALIZABLE_SHORT_NAME && it.qualifiedName == SERIALIZABLE_ANNOTATION -} diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/WispRouteMetadata.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/WispRouteMetadata.kt deleted file mode 100644 index 41a1b46..0000000 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/WispRouteMetadata.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.angrypodo.wisp - -/** - * KSClassDeclaration을 대체하는 순수 Kotlin 모델입니다. - */ -internal data class RouteClassInfo( - val qualifiedName: String?, - val simpleName: String, - val annotations: List -) - -/** - * KSAnnotation을 대체하는 순수 Kotlin 모델입니다. - */ -internal data class AnnotationInfo( - val qualifiedName: String?, - val shortName: String -) diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/generator/RouteFactoryGenerator.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/generator/RouteFactoryGenerator.kt index 2b717e6..a32c9ff 100644 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/generator/RouteFactoryGenerator.kt +++ b/wisp-processor/src/main/java/com/angrypodo/wisp/generator/RouteFactoryGenerator.kt @@ -1,12 +1,12 @@ 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.angrypodo.wisp.util.WispClassName.INVALID_PARAMETER_ERROR +import com.angrypodo.wisp.util.WispClassName.MISSING_PARAMETER_ERROR +import com.angrypodo.wisp.util.WispClassName.ROUTE_FACTORY import com.google.devtools.ksp.processing.KSPLogger import com.squareup.kotlinpoet.ANY import com.squareup.kotlinpoet.BOOLEAN @@ -105,8 +105,8 @@ internal class RouteFactoryGenerator( nonNullableType == DOUBLE -> CodeBlock.of("%L?.toDoubleOrNull()", rawAccess) else -> { logger.error( - "Wisp Error: Unsupported type " + - "'${param.typeName}' for parameter '${param.name}'." + "Wisp Error: Unsupported type '${param.typeName}'" + + " for parameter '${param.name}'." ) CodeBlock.of("null") } 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 b947863..f5a753a 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,7 +1,7 @@ package com.angrypodo.wisp.generator -import com.angrypodo.wisp.WispClassName 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 diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/mapper/RouteInfoMapper.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/mapper/RouteInfoMapper.kt index 88bca50..9ee8d25 100644 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/mapper/RouteInfoMapper.kt +++ b/wisp-processor/src/main/java/com/angrypodo/wisp/mapper/RouteInfoMapper.kt @@ -47,9 +47,8 @@ private fun KSClassDeclaration.extractParameters(): List { return primaryConstructor?.parameters?.mapNotNull { parameter -> val parameterName = parameter.name?.asString() ?: return@mapNotNull null val resolvedType = parameter.type.resolve() - val declaration = resolvedType.declaration val isEnum = - declaration is KSClassDeclaration && declaration.classKind == ClassKind.ENUM_CLASS + (resolvedType.declaration as? KSClassDeclaration)?.classKind == ClassKind.ENUM_CLASS ParameterInfo( name = parameterName, diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessor.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/processor/WispProcessor.kt similarity index 96% rename from wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessor.kt rename to wisp-processor/src/main/java/com/angrypodo/wisp/processor/WispProcessor.kt index f5922c4..281b923 100644 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessor.kt +++ b/wisp-processor/src/main/java/com/angrypodo/wisp/processor/WispProcessor.kt @@ -1,11 +1,11 @@ -package com.angrypodo.wisp +package com.angrypodo.wisp.processor -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.angrypodo.wisp.validation.WispValidator import com.google.devtools.ksp.processing.CodeGenerator import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.KSPLogger @@ -43,7 +43,7 @@ internal class WispProcessor( val routeInfos = routesWithSymbols.map { it.first } - val duplicateValidationResult = validateDuplicatePaths(routeInfos) + val duplicateValidationResult = WispValidator.validateDuplicatePaths(routeInfos) if (duplicateValidationResult is WispValidator.ValidationResult.Failure) { duplicateValidationResult.errors.forEach { logger.error(it) } diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessorProvider.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/processor/WispProcessorProvider.kt similarity index 93% rename from wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessorProvider.kt rename to wisp-processor/src/main/java/com/angrypodo/wisp/processor/WispProcessorProvider.kt index 93e1b43..d0b65a2 100644 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessorProvider.kt +++ b/wisp-processor/src/main/java/com/angrypodo/wisp/processor/WispProcessorProvider.kt @@ -1,4 +1,4 @@ -package com.angrypodo.wisp +package com.angrypodo.wisp.processor import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/WispClassName.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/util/WispClassName.kt similarity index 61% rename from wisp-processor/src/main/java/com/angrypodo/wisp/WispClassName.kt rename to wisp-processor/src/main/java/com/angrypodo/wisp/util/WispClassName.kt index 18b0d50..51d9d19 100644 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/WispClassName.kt +++ b/wisp-processor/src/main/java/com/angrypodo/wisp/util/WispClassName.kt @@ -1,4 +1,4 @@ -package com.angrypodo.wisp +package com.angrypodo.wisp.util import com.squareup.kotlinpoet.ClassName @@ -6,9 +6,9 @@ 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 WISP_REGISTRY_SPEC = ClassName(RUNTIME_PACKAGE, "WispRegistrySpec") - val WISP_URI_MATCHER = ClassName(RUNTIME_PACKAGE, "WispUriMatcher") + 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 UNKNOWN_PATH_ERROR = ClassName(RUNTIME_PACKAGE, "WispError", "UnknownPath") val MISSING_PARAMETER_ERROR = ClassName(RUNTIME_PACKAGE, "WispError", "MissingParameter") diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/WispValidator.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/validation/WispValidator.kt similarity index 61% rename from wisp-processor/src/main/java/com/angrypodo/wisp/WispValidator.kt rename to wisp-processor/src/main/java/com/angrypodo/wisp/validation/WispValidator.kt index a73417d..15c94c9 100644 --- a/wisp-processor/src/main/java/com/angrypodo/wisp/WispValidator.kt +++ b/wisp-processor/src/main/java/com/angrypodo/wisp/validation/WispValidator.kt @@ -1,4 +1,4 @@ -package com.angrypodo.wisp +package com.angrypodo.wisp.validation import com.angrypodo.wisp.model.RouteInfo @@ -8,24 +8,13 @@ internal object WispValidator { data class Failure(val errors: List) : ValidationResult } - fun validate(routeInfo: RouteClassInfo): ValidationResult { - if (!routeInfo.isSerializable()) { - return ValidationResult.Failure( - listOf( - "Wisp Error: Route Class '${routeInfo.qualifiedName}' " + - "must be annotated with @Serializable." - ) - ) - } - - return ValidationResult.Success - } - fun validateDuplicatePaths(routes: List): ValidationResult { val duplicates = routes.groupBy { it.wispPath } .filter { it.value.size > 1 } - if (duplicates.isEmpty()) return ValidationResult.Success + if (duplicates.isEmpty()) { + return ValidationResult.Success + } val errorMessages = duplicates.map { (path, routeInfos) -> val conflictingClasses = routeInfos.joinToString(", ") { it.routeClassName.simpleName } diff --git a/wisp-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/wisp-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider index 89ba897..5c0eaf0 100644 --- a/wisp-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider +++ b/wisp-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -1 +1 @@ -com.angrypodo.wisp.WispProcessorProvider \ No newline at end of file +com.angrypodo.wisp.processor.WispProcessorProvider diff --git a/wisp-processor/src/test/java/com/angrypodo/wisp/WispValidatorTest.kt b/wisp-processor/src/test/java/com/angrypodo/wisp/WispValidatorTest.kt deleted file mode 100644 index bcee6d1..0000000 --- a/wisp-processor/src/test/java/com/angrypodo/wisp/WispValidatorTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.angrypodo.wisp - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -/** - * WispValidator의 순수 유효성 검증 로직을 테스트합니다. - */ -class WispValidatorTest { - @Test - fun `Serializable 어노테이션이 없으면 Failure를 반환한다`() { - // Given - val testInfo = RouteClassInfo( - qualifiedName = "com.example.TestRoute", - simpleName = "TestRoute", - annotations = emptyList() - ) - val expectedMessage = "Wisp Error: Route Class '${testInfo.qualifiedName}' " + - "must be annotated with @Serializable." - - // When - val result = WispValidator.validate(testInfo) - - // Then - assertTrue(result is WispValidator.ValidationResult.Failure) { - "결과 타입은 Failure여야 합니다." - } - assertEquals( - listOf(expectedMessage), - (result as WispValidator.ValidationResult.Failure).errors - ) - } - - @Test - fun `Serializable 어노테이션이 있으면 Success를 반환한다`() { - // Given - val serializableAnnotation = AnnotationInfo( - qualifiedName = "kotlinx.serialization.Serializable", - shortName = "Serializable" - ) - val testInfo = RouteClassInfo( - qualifiedName = "com.example.TestRoute", - simpleName = "TestRoute", - annotations = listOf(serializableAnnotation) - ) - val expectedResult = WispValidator.ValidationResult.Success - - // When - val result = WispValidator.validate(testInfo) - - // Then - assertEquals(expectedResult, result) - } -} 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 604bcdc..5355b10 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 @@ -29,11 +29,9 @@ internal class RegistryGeneratorTest { val routes = listOf(homeRoute, profileRoute) // When: 코드 생성 실행 - val fileSpec = WispRegistryGenerator.generate(routes) + val fileSpec = WispRegistryGenerator().generate(routes) val generatedCode = fileSpec.toString() - println(generatedCode) - // Then: 생성된 WispRegistry 객체를 반환 assertTrue(generatedCode.contains("object WispRegistry")) assertTrue(generatedCode.contains("val factories: Map = mapOf(")) 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 20eb975..8ee96d3 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,6 +3,9 @@ package com.angrypodo.wisp.runtime import android.net.Uri import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination +import com.angrypodo.wisp.runtime.parser.DefaultWispUriParser +import com.angrypodo.wisp.runtime.parser.WispUriParser +import com.angrypodo.wisp.runtime.spi.WispRegistrySpec /** * Wisp 라이브러리의 핵심 로직을 수행하고, 내비게이션 기능을 실행하는 클래스입니다. @@ -53,13 +56,15 @@ class Wisp( companion object { private var instance: Wisp? = null + @JvmStatic + @Synchronized fun initialize(registry: WispRegistrySpec) { if (instance == null) { instance = Wisp(registry) } } - internal fun getDefaultInstance(): Wisp { + fun getDefaultInstance(): Wisp { return instance ?: throw IllegalStateException( "Wisp.initialize() must be called first in your Application class." ) diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispUriMatcher.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/matcher/WispUriMatcher.kt similarity index 77% rename from wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispUriMatcher.kt rename to wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/matcher/WispUriMatcher.kt index 7922af6..3b48ec1 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispUriMatcher.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/matcher/WispUriMatcher.kt @@ -1,4 +1,4 @@ -package com.angrypodo.wisp.runtime +package com.angrypodo.wisp.runtime.matcher object WispUriMatcher { @@ -19,23 +19,34 @@ object WispUriMatcher { if (pathSegments.size != patternSegments.size) return null val params = mutableMapOf() + if (!matchPathSegments(params, pathSegments, patternSegments)) { + return null + } + + if (query.isNotEmpty()) { + parseQueryString(query, params) + } + + return params + } + private fun matchPathSegments( + params: MutableMap, + pathSegments: List, + patternSegments: List + ): Boolean { for (i in patternSegments.indices) { val patternSegment = patternSegments[i] val pathSegment = pathSegments[i] if (isPlaceholder(patternSegment)) { val key = patternSegment.removeSurrounding("{", "}") - params[key] = pathSegment } else if (!patternSegment.equals(pathSegment, ignoreCase = true)) { - return null + return false } } - - if (query.isNotEmpty()) parseQueryString(query, params) - - return params + return true } private fun isPlaceholder(segment: String): Boolean = diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/DefaultWispUriParser.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/parser/DefaultWispUriParser.kt similarity index 89% rename from wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/DefaultWispUriParser.kt rename to wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/parser/DefaultWispUriParser.kt index 7a250ab..0bf677b 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/DefaultWispUriParser.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/parser/DefaultWispUriParser.kt @@ -1,6 +1,7 @@ -package com.angrypodo.wisp.runtime +package com.angrypodo.wisp.runtime.parser import android.net.Uri +import com.angrypodo.wisp.runtime.WispError import java.net.URLDecoder import java.nio.charset.StandardCharsets diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispUriParser.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/parser/WispUriParser.kt similarity index 83% rename from wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispUriParser.kt rename to wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/parser/WispUriParser.kt index 50e16b2..f0a4095 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispUriParser.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/parser/WispUriParser.kt @@ -1,4 +1,4 @@ -package com.angrypodo.wisp.runtime +package com.angrypodo.wisp.runtime.parser import android.net.Uri diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/RouteFactory.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/RouteFactory.kt similarity index 87% rename from wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/RouteFactory.kt rename to wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/RouteFactory.kt index e5c1e50..d330eeb 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/RouteFactory.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/RouteFactory.kt @@ -1,4 +1,4 @@ -package com.angrypodo.wisp.runtime +package com.angrypodo.wisp.runtime.spi /** * KSP가 생성하는 팩토리 클래스들이 구현할 인터페이스입니다. diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/WispRegistrySpec.kt similarity index 64% rename from wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt rename to wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/WispRegistrySpec.kt index d97814a..cb825f1 100644 --- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/WispRegistrySpec.kt +++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/spi/WispRegistrySpec.kt @@ -1,4 +1,4 @@ -package com.angrypodo.wisp.runtime +package com.angrypodo.wisp.runtime.spi interface WispRegistrySpec { fun createRoute(path: String): Any? diff --git a/wisp-runtime/src/test/java/com/angrypodo/wisp/WispUriMatcherTest.kt b/wisp-runtime/src/test/java/com/angrypodo/wisp/WispUriMatcherTest.kt index 7f49b1c..0a1e0d0 100644 --- a/wisp-runtime/src/test/java/com/angrypodo/wisp/WispUriMatcherTest.kt +++ b/wisp-runtime/src/test/java/com/angrypodo/wisp/WispUriMatcherTest.kt @@ -1,6 +1,6 @@ package com.angrypodo.wisp -import com.angrypodo.wisp.runtime.WispUriMatcher +import com.angrypodo.wisp.runtime.matcher.WispUriMatcher import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue