-
Notifications
You must be signed in to change notification settings - Fork 0
Feat#10 RouteFactory 코드 생성 기능 구현 #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
916bd24
refactor: improve wisp processor to validate @Wisp annotation and gen…
angryPodo 459a7f2
fix: correct WispProcessorProvider to properly instantiate WispProces…
angryPodo e0fe587
add: implement route factory generator for parameter conversion and v…
angryPodo 99f5d4d
add: RouteInfo and ParameterInfo data classes
angryPodo 9fbad7b
feat: add RouteInfoMapper for route configuration mapping from class …
angryPodo e2d6016
refactor: extract conversion logic into separate method for clarity a…
angryPodo 57c18f6
refactor: extract symbol processing into separate method for clarity …
angryPodo 70789e3
refactor: optimize route factory code generation for object and class…
angryPodo 909006d
refactor: update RouteInfo to use sealed interface with specific data…
angryPodo 3c57e38
refactor: update route info mapping to distinguish object and class r…
angryPodo 08fcdf3
add: define RouteFactory interface for runtime route creation impleme…
angryPodo a061c36
add: generate route factory code for object and class routes with par…
angryPodo 00db1f3
add: generate route factory code for object and class routes with par…
angryPodo 47d3e3d
fix: lint format
angryPodo 582ddbc
fix: improve parameter handling and error type selection in route fac…
angryPodo 1bbb0b2
fix: correct indentation and formatting in RouteInfoMapper.kt for lint
angryPodo 6e1a0f0
fix: improve symbol validation and code generation in WispProcessor.kt
angryPodo 76d1913
refactor: Apply review for import and incremental build
angryPodo 59fef41
feat: Add support for Double query parameter type
angryPodo 4780cf8
refactor: Use KSPLogger for unsupported type errors
angryPodo 30b279f
test: Update tests for RouteFactoryGenerator
angryPodo 7f53466
fix: lint format
angryPodo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
99 changes: 62 additions & 37 deletions
99
wisp-processor/src/main/java/com/angrypodo/wisp/WispProcessor.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,71 +1,96 @@ | ||
| package com.angrypodo.wisp | ||
|
|
||
| import com.angrypodo.wisp.annotations.Wisp | ||
| import com.angrypodo.wisp.generator.RouteFactoryGenerator | ||
| import com.angrypodo.wisp.mapper.toRouteInfo | ||
| import com.angrypodo.wisp.model.RouteInfo | ||
| import com.google.devtools.ksp.processing.CodeGenerator | ||
| import com.google.devtools.ksp.processing.Dependencies | ||
| import com.google.devtools.ksp.processing.KSPLogger | ||
| import com.google.devtools.ksp.processing.Resolver | ||
| import com.google.devtools.ksp.processing.SymbolProcessor | ||
| import com.google.devtools.ksp.processing.SymbolProcessorEnvironment | ||
| import com.google.devtools.ksp.symbol.KSAnnotated | ||
| import com.google.devtools.ksp.symbol.KSAnnotation | ||
| import com.google.devtools.ksp.symbol.KSClassDeclaration | ||
| import com.google.devtools.ksp.validate | ||
| import com.squareup.kotlinpoet.ksp.writeTo | ||
|
|
||
| /** | ||
| * @Wisp 어노테이션이 붙은 클래스를 찾아 유효성을 검증하고 코드를 생성하는 메인 프로세서 클래스입니다. | ||
| */ | ||
| internal class WispProcessor( | ||
| environment: SymbolProcessorEnvironment | ||
| private val codeGenerator: CodeGenerator, | ||
| private val logger: KSPLogger | ||
| ) : SymbolProcessor { | ||
| private val logger = environment.logger | ||
|
|
||
| /** | ||
| * KSP가 코드를 분석할 때 호출되는 함수입니다. | ||
| */ | ||
| private val factoryGenerator = RouteFactoryGenerator(logger) | ||
|
|
||
| override fun process(resolver: Resolver): List<KSAnnotated> { | ||
| val symbols = resolver.getSymbolsWithAnnotation(WISP_ANNOTATION) | ||
| .filterIsInstance<KSClassDeclaration>() | ||
|
|
||
| if (symbols.none()) return emptyList() | ||
|
|
||
| logger.info("Wisp: found ${symbols.count()} @Wisp Route Symbols.") | ||
|
|
||
| val (processableSymbols, deferredSymbols) = symbols.partition { it.validate() } | ||
|
|
||
| processableSymbols.forEach { routeClass -> | ||
| val routeInfo = routeClass.toRouteClassInfo() | ||
| val result = WispValidator.validate(routeInfo) | ||
| processableSymbols.forEach { processSymbol(it) } | ||
|
|
||
| return deferredSymbols | ||
| } | ||
|
|
||
| when (result) { | ||
| is WispValidator.ValidationResult.Success -> | ||
| logger.info( | ||
| "Wisp: Route '${routeClass.simpleName.asString()}' validation successful." | ||
| ) | ||
| private fun processSymbol(routeClass: KSClassDeclaration) { | ||
| if (!validateSerializable(routeClass)) { | ||
| return | ||
| } | ||
|
|
||
| is WispValidator.ValidationResult.Failure -> | ||
| logger.error(result.message, routeClass) | ||
| } | ||
| val routeInfo = routeClass.toRouteInfo() ?: run { | ||
| logInvalidRouteError(routeClass) | ||
| return | ||
| } | ||
|
|
||
| return deferredSymbols | ||
| generateRouteFactory(routeClass, routeInfo) | ||
| } | ||
|
|
||
| /** | ||
| * KSP의 KSClassDeclaration을 RouteClassInfo로 변환하는 매퍼 함수입니다. | ||
| */ | ||
| private fun KSClassDeclaration.toRouteClassInfo(): RouteClassInfo = RouteClassInfo( | ||
| qualifiedName = qualifiedName?.asString(), | ||
| simpleName = simpleName.asString(), | ||
| annotations = annotations.map { it.toAnnotationInfo() }.toList() | ||
| ) | ||
|
|
||
| /** | ||
| * KSP의 KSAnnotation을 AnnotationInfo로 변환하는 매퍼 함수입니다. | ||
| */ | ||
| private fun KSAnnotation.toAnnotationInfo(): AnnotationInfo = AnnotationInfo( | ||
| qualifiedName = annotationType.resolve().declaration.qualifiedName?.asString(), | ||
| shortName = shortName.asString() | ||
| ) | ||
| private fun validateSerializable(routeClass: KSClassDeclaration): Boolean { | ||
| if (routeClass.hasSerializableAnnotation()) return true | ||
| val routeName = routeClass.qualifiedName?.asString() | ||
| logger.error( | ||
| "Wisp Error: Route '$routeName' must be annotated with @Serializable.", | ||
| routeClass | ||
| ) | ||
| return false | ||
| } | ||
|
|
||
| private fun logInvalidRouteError(routeClass: KSClassDeclaration) { | ||
| val routeName = routeClass.simpleName.asString() | ||
| logger.error( | ||
| "Wisp Error: Route '$routeName' is missing @Wisp path or has invalid parameters.", | ||
| routeClass | ||
| ) | ||
| } | ||
|
|
||
| private fun generateRouteFactory( | ||
| routeClass: KSClassDeclaration, | ||
| routeInfo: RouteInfo | ||
| ) { | ||
| val fileSpec = factoryGenerator.generate(routeInfo) | ||
| val dependencies = Dependencies(false, routeClass.containingFile!!) | ||
| fileSpec.writeTo(codeGenerator, dependencies) | ||
| } | ||
|
|
||
| private fun KSClassDeclaration.hasSerializableAnnotation(): Boolean { | ||
| return annotations.any { annotation -> | ||
| val shortName = annotation.shortName.asString() | ||
| val qualifiedName = annotation.annotationType.resolve() | ||
| .declaration.qualifiedName?.asString() | ||
| val isSerializable = shortName == SERIALIZABLE_SHORT_NAME && | ||
| qualifiedName == SERIALIZABLE_ANNOTATION | ||
| isSerializable | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| private val WISP_ANNOTATION = requireNotNull(Wisp::class.qualifiedName) | ||
| private const val SERIALIZABLE_SHORT_NAME = "Serializable" | ||
| private const val SERIALIZABLE_ANNOTATION = "kotlinx.serialization.Serializable" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
wisp-processor/src/main/java/com/angrypodo/wisp/generator/RouteFactoryGenerator.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| package com.angrypodo.wisp.generator | ||
|
|
||
| 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.google.devtools.ksp.processing.KSPLogger | ||
| import com.squareup.kotlinpoet.ANY | ||
| import com.squareup.kotlinpoet.BOOLEAN | ||
| import com.squareup.kotlinpoet.ClassName | ||
| import com.squareup.kotlinpoet.CodeBlock | ||
| import com.squareup.kotlinpoet.DOUBLE | ||
| import com.squareup.kotlinpoet.FLOAT | ||
| import com.squareup.kotlinpoet.FileSpec | ||
| import com.squareup.kotlinpoet.FunSpec | ||
| import com.squareup.kotlinpoet.INT | ||
| import com.squareup.kotlinpoet.KModifier | ||
| import com.squareup.kotlinpoet.LONG | ||
| import com.squareup.kotlinpoet.MAP | ||
| import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy | ||
| import com.squareup.kotlinpoet.STRING | ||
| import com.squareup.kotlinpoet.TypeSpec | ||
|
|
||
| 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", | ||
| "MissingParameter" | ||
| ) | ||
| private val invalidParameterError = ClassName( | ||
| "com.angrypodo.wisp.runtime", | ||
| "WispError", | ||
| "InvalidParameter" | ||
| ) | ||
|
|
||
| fun generate(routeInfo: RouteInfo): FileSpec { | ||
| val createFun = FunSpec.builder("create") | ||
| .addModifiers(KModifier.OVERRIDE) | ||
| .addParameter("params", MAP.parameterizedBy(STRING, STRING)) | ||
| .returns(ANY) | ||
| .addCode(buildCreateFunctionBody(routeInfo)) | ||
| .build() | ||
|
|
||
| val factoryObject = TypeSpec.objectBuilder(routeInfo.factoryClassName) | ||
| .addModifiers(KModifier.INTERNAL) | ||
| .addSuperinterface(routeFactoryInterface) | ||
| .addFunction(createFun) | ||
| .build() | ||
|
|
||
| return FileSpec.builder( | ||
| routeInfo.factoryClassName.packageName, | ||
| routeInfo.factoryClassName.simpleName | ||
| ) | ||
| .addType(factoryObject) | ||
| .build() | ||
| } | ||
|
|
||
| private fun buildCreateFunctionBody(routeInfo: RouteInfo): CodeBlock { | ||
| return when (routeInfo) { | ||
| is ObjectRouteInfo -> CodeBlock.of("return %T", routeInfo.routeClassName) | ||
| is ClassRouteInfo -> { | ||
| val block = CodeBlock.builder() | ||
| routeInfo.parameters.forEach { parameter -> | ||
| val conversion = buildConversionCode(parameter, routeInfo.wispPath) | ||
| block.addStatement("val %L = %L", parameter.name, conversion) | ||
| } | ||
| val constructorArgs = routeInfo.parameters.joinToString(", ") { | ||
| "${it.name} = ${it.name}" | ||
| } | ||
| block.addStatement("return %T(%L)", routeInfo.routeClassName, constructorArgs) | ||
| block.build() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun buildConversionCode(param: ParameterInfo, wispPath: String): CodeBlock { | ||
| val rawAccess = CodeBlock.of("params[%S]", param.name) | ||
| val conversionLogic = getConversionLogic(param, rawAccess) | ||
|
|
||
| if (param.isNullable) { | ||
| return conversionLogic | ||
| } | ||
|
|
||
| val nonNullableType = param.typeName.copy(nullable = false) | ||
| val errorType = when (nonNullableType) { | ||
| STRING -> missingParameterError | ||
| else -> invalidParameterError | ||
| } | ||
|
|
||
| return CodeBlock.of( | ||
| "(%L ?: throw %T(%S, %S))", | ||
| conversionLogic, | ||
| errorType, | ||
| wispPath, | ||
| param.name | ||
| ) | ||
| } | ||
|
|
||
| private fun getConversionLogic(param: ParameterInfo, rawAccess: CodeBlock): CodeBlock { | ||
| val nonNullableType = param.typeName.copy(nullable = false) | ||
| return when { | ||
| param.isEnum -> CodeBlock.of( | ||
| "runCatching { %T.valueOf(%L!!.uppercase()) }.getOrNull()", | ||
| nonNullableType, | ||
| rawAccess | ||
| ) | ||
| nonNullableType == STRING -> rawAccess | ||
| nonNullableType == INT -> CodeBlock.of("%L?.toIntOrNull()", rawAccess) | ||
| nonNullableType == LONG -> CodeBlock.of("%L?.toLongOrNull()", rawAccess) | ||
| nonNullableType == BOOLEAN -> CodeBlock.of("%L?.toBooleanStrictOrNull()", rawAccess) | ||
| nonNullableType == FLOAT -> CodeBlock.of("%L?.toFloatOrNull()", rawAccess) | ||
| nonNullableType == DOUBLE -> CodeBlock.of("%L?.toDoubleOrNull()", rawAccess) | ||
| else -> { | ||
| logger.error( | ||
| "Wisp Error: Unsupported type " + | ||
| "'${param.typeName}' for parameter '${param.name}'." | ||
| ) | ||
| CodeBlock.of("null") | ||
| } | ||
| } | ||
| } | ||
| } | ||
61 changes: 61 additions & 0 deletions
61
wisp-processor/src/main/java/com/angrypodo/wisp/mapper/RouteInfoMapper.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package com.angrypodo.wisp.mapper | ||
|
|
||
| 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.google.devtools.ksp.symbol.ClassKind | ||
| import com.google.devtools.ksp.symbol.KSClassDeclaration | ||
| import com.squareup.kotlinpoet.ClassName | ||
| import com.squareup.kotlinpoet.ksp.toClassName | ||
| import com.squareup.kotlinpoet.ksp.toTypeName | ||
|
|
||
| private const val WISP_SIMPLE_NAME = "Wisp" | ||
| private const val WISP_PATH_ARGUMENT = "path" | ||
|
|
||
| internal fun KSClassDeclaration.toRouteInfo(): RouteInfo? { | ||
| val wispPath = getWispPath() ?: return null | ||
| val routeClassName = toClassName() | ||
| val factoryClassName = ClassName(packageName.asString(), "${simpleName.asString()}RouteFactory") | ||
|
|
||
| if (classKind == ClassKind.OBJECT) { | ||
| return ObjectRouteInfo( | ||
| routeClassName = routeClassName, | ||
| factoryClassName = factoryClassName, | ||
| wispPath = wispPath | ||
| ) | ||
| } | ||
|
|
||
| return ClassRouteInfo( | ||
| routeClassName = routeClassName, | ||
| factoryClassName = factoryClassName, | ||
| wispPath = wispPath, | ||
| parameters = extractParameters() | ||
| ) | ||
| } | ||
|
|
||
| private fun KSClassDeclaration.getWispPath(): String? { | ||
| val wispAnnotation = annotations.find { it.shortName.asString() == WISP_SIMPLE_NAME } | ||
| ?: return null | ||
| val pathArgument = | ||
| wispAnnotation.arguments.firstOrNull { it.name?.asString() == WISP_PATH_ARGUMENT } | ||
| ?: wispAnnotation.arguments.firstOrNull() | ||
| return pathArgument?.value as? String | ||
| } | ||
|
|
||
| private fun KSClassDeclaration.extractParameters(): List<ParameterInfo> { | ||
| 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 | ||
|
|
||
| ParameterInfo( | ||
| name = parameterName, | ||
| typeName = resolvedType.toTypeName(), | ||
| isNullable = resolvedType.isMarkedNullable, | ||
| isEnum = isEnum | ||
| ) | ||
| } ?: emptyList() | ||
| } |
42 changes: 42 additions & 0 deletions
42
wisp-processor/src/main/java/com/angrypodo/wisp/model/RouteInfo.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.angrypodo.wisp.model | ||
|
|
||
| import com.squareup.kotlinpoet.ClassName | ||
| import com.squareup.kotlinpoet.TypeName | ||
|
|
||
| /** | ||
| * 라우트 정보를 타입에 따라 분리하여 표현하는 Sealed Interface 입니다. | ||
| */ | ||
| sealed interface RouteInfo { | ||
| val routeClassName: ClassName | ||
| val factoryClassName: ClassName | ||
| val wispPath: String | ||
| } | ||
|
|
||
| /** | ||
| * 파라미터가 없는 object/data object 타입 라우트의 정보입니다. | ||
| */ | ||
| internal data class ObjectRouteInfo( | ||
| override val routeClassName: ClassName, | ||
| override val factoryClassName: ClassName, | ||
| override val wispPath: String | ||
| ) : RouteInfo | ||
|
|
||
| /** | ||
| * 생성자 파라미터가 있는 class/data class 타입 라우트의 정보입니다. | ||
| */ | ||
| internal data class ClassRouteInfo( | ||
| override val routeClassName: ClassName, | ||
| override val factoryClassName: ClassName, | ||
| override val wispPath: String, | ||
| val parameters: List<ParameterInfo> | ||
| ) : RouteInfo | ||
|
|
||
| /** | ||
| * 라우트 생성자 파라미터 정보를 표현하는 데이터 클래스입니다. | ||
| */ | ||
| internal data class ParameterInfo( | ||
| val name: String, | ||
| val typeName: TypeName, | ||
| val isNullable: Boolean, | ||
| val isEnum: Boolean | ||
| ) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요기
Double은 따로 지원하지 않는 이유가 있나요?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추가적으로 지원하지 않는 타입에 대해서 예외를 던지기보다는
KSPLogger로 처리하는건 어떻게 생각하시나요?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Long만 고려하고Double은 누락했네요😅 바로 추가하도록 하겠습니다!좋은 생각이네요!! 오류가 발생한 위치를 알려주는게 사용자 경험 측면에서 훨씬 좋을 것 같습니다ㅎㅎ 이것도 리팩토링 하겠습니다~!