main.de.jensklingenberg.ktorfit.model.FunctionData.kt Maven / Gradle / Ivy
The newest version!
package de.jensklingenberg.ktorfit.model
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ksp.toAnnotationSpec
import de.jensklingenberg.ktorfit.model.annotations.FormUrlEncoded
import de.jensklingenberg.ktorfit.model.annotations.FunctionAnnotation
import de.jensklingenberg.ktorfit.model.annotations.Headers
import de.jensklingenberg.ktorfit.model.annotations.HttpMethod
import de.jensklingenberg.ktorfit.model.annotations.HttpMethodAnnotation
import de.jensklingenberg.ktorfit.model.annotations.Multipart
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Header
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.HeaderMap
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Body
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Path
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.RequestType
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Field
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Tag
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.FieldMap
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Url
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.RequestBuilder
import de.jensklingenberg.ktorfit.poetspec.findTypeName
import de.jensklingenberg.ktorfit.utils.anyInstance
import de.jensklingenberg.ktorfit.utils.getFormUrlEncodedAnnotation
import de.jensklingenberg.ktorfit.utils.getHeaderAnnotation
import de.jensklingenberg.ktorfit.utils.getKsFile
import de.jensklingenberg.ktorfit.utils.getMultipartAnnotation
import de.jensklingenberg.ktorfit.utils.getStreamingAnnotation
import de.jensklingenberg.ktorfit.utils.isSuspend
import de.jensklingenberg.ktorfit.utils.parseHTTPMethodAnno
import de.jensklingenberg.ktorfit.utils.resolveTypeName
data class FunctionData(
val name: String,
val returnType: ReturnTypeData,
val isSuspend: Boolean = false,
val parameterDataList: List,
val annotations: List = emptyList(),
val httpMethodAnnotation: HttpMethodAnnotation,
val modifiers: List = emptyList(),
val optInAnnotations: List
)
/**
* Collect all [HttpMethodAnnotation] from a [KSFunctionDeclaration]
* @return list of [HttpMethodAnnotation]
*/
private fun getHttpMethodAnnotations(ksFunctionDeclaration: KSFunctionDeclaration): List {
val getAnno = ksFunctionDeclaration.parseHTTPMethodAnno("GET")
val putAnno = ksFunctionDeclaration.parseHTTPMethodAnno("PUT")
val postAnno = ksFunctionDeclaration.parseHTTPMethodAnno("POST")
val deleteAnno = ksFunctionDeclaration.parseHTTPMethodAnno("DELETE")
val headAnno = ksFunctionDeclaration.parseHTTPMethodAnno("HEAD")
val optionsAnno = ksFunctionDeclaration.parseHTTPMethodAnno("OPTIONS")
val patchAnno = ksFunctionDeclaration.parseHTTPMethodAnno("PATCH")
val httpAnno = ksFunctionDeclaration.parseHTTPMethodAnno("HTTP")
return listOfNotNull(getAnno, postAnno, putAnno, deleteAnno, headAnno, optionsAnno, patchAnno, httpAnno)
}
fun KSFunctionDeclaration.toFunctionData(
logger: KSPLogger,
addImport: (String) -> Unit
): FunctionData {
val funcDeclaration = this
val functionName = funcDeclaration.simpleName.asString()
val functionParameters = funcDeclaration.parameters.map { it.createParameterData(logger) }
val resolvedReturnType =
funcDeclaration.returnType?.resolve() ?: throw IllegalStateException("Return type not found")
val returnType =
ReturnTypeData(
name = resolvedReturnType.resolveTypeName(),
parameterType = resolvedReturnType,
typeName = findTypeName(resolvedReturnType, funcDeclaration.getKsFile().filePath),
)
val functionAnnotationList = mutableListOf()
funcDeclaration.getMultipartAnnotation()?.let {
functionAnnotationList.add(it)
}
if (funcDeclaration.typeParameters.isNotEmpty()) {
logger.error(
KtorfitError.FUNCTION_OR_PARAMETERS_TYPES_MUST_NOT_INCLUDE_ATYPE_VARIABLE_OR_WILDCARD,
funcDeclaration,
)
}
funcDeclaration.getHeaderAnnotation()?.let { headers ->
headers.value.forEach {
// Check if headers are in valid format
try {
val (key, value) = it.split(":")
} catch (exception: Exception) {
logger.error(KtorfitError.HEADERS_VALUE_MUST_BE_IN_FORM + it, funcDeclaration)
}
}
functionAnnotationList.add(headers)
}
funcDeclaration.getFormUrlEncodedAnnotation()?.let { formUrlEncoded ->
val isWithoutFieldOrFieldMap =
functionParameters.none { it.hasAnnotation() || it.hasAnnotation() }
if (isWithoutFieldOrFieldMap) {
logger.error(
KtorfitError.FORM_ENCODED_METHOD_MUST_CONTAIN_AT_LEAST_ONE_FIELD_OR_FIELD_MAP,
funcDeclaration,
)
}
if (funcDeclaration.getMultipartAnnotation() != null) {
logger.error(KtorfitError.ONLY_ONE_ENCODING_ANNOTATION_IS_ALLOWED, funcDeclaration)
}
functionAnnotationList.add(formUrlEncoded)
}
funcDeclaration.getStreamingAnnotation()?.let { streaming ->
val returnsHttpStatement = returnType.name == "HttpStatement"
if (!returnsHttpStatement) {
logger.error(
KtorfitError.FOR_STREAMING_THE_RETURN_TYPE_MUST_BE_HTTP_STATEMENT,
funcDeclaration,
)
}
functionAnnotationList.add(streaming)
}
val httpMethodAnnoList = getHttpMethodAnnotations(funcDeclaration)
if (httpMethodAnnoList.isEmpty()) {
logger.error(KtorfitError.noHttpAnnotationAt(functionName), funcDeclaration)
} else {
addImport("io.ktor.http.HttpMethod")
}
if (httpMethodAnnoList.size > 1) {
logger.error(
KtorfitError.ONLY_ONE_HTTP_METHOD_IS_ALLOWED + "Found: " +
httpMethodAnnoList.joinToString {
it.httpMethod.keyword
} + " at " + functionName,
funcDeclaration,
)
}
val firstHttpMethodAnnotation = httpMethodAnnoList.first()
val isEmptyHttpPathWithoutUrlAnnotation =
firstHttpMethodAnnotation.path.isEmpty() && functionParameters.none { it.hasAnnotation() }
if (isEmptyHttpPathWithoutUrlAnnotation) {
logger.error(
KtorfitError.missingEitherKeywordUrlOrUrlParameter(firstHttpMethodAnnotation.httpMethod.keyword),
funcDeclaration,
)
}
if (functionParameters.filter { it.hasAnnotation() }.size > 1) {
logger.error(
KtorfitError.ONLY_ONE_REQUEST_BUILDER_IS_ALLOWED + " Found: " + httpMethodAnnoList.joinToString { it.toString() } + " at " +
functionName,
funcDeclaration,
)
}
when (firstHttpMethodAnnotation.httpMethod) {
HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH -> {}
else -> {
if (functionAnnotationList.anyInstance()) {
logger.error(
KtorfitError.MULTIPART_CAN_ONLY_BE_SPECIFIED_ON_HTTPMETHODS,
funcDeclaration,
)
}
if (funcDeclaration.getFormUrlEncodedAnnotation() != null) {
logger.error(
KtorfitError.FORM_URL_ENCODED_CAN_ONLY_BE_SPECIFIED_ON_HTTP_METHODS_WITH_REQUEST_BODY,
funcDeclaration,
)
}
}
}
functionParameters.forEach { parameterData ->
parameterData.annotations.forEach {
if (it is Path) {
if (!firstHttpMethodAnnotation.path.contains("{${it.value}}")) {
logger.error(
KtorfitError.missingXInRelativeUrlPath(it.value),
funcDeclaration,
)
}
}
if (it is Header || it is HeaderMap) {
addImport("io.ktor.client.request.headers")
}
if (it is Tag) {
addImport("io.ktor.util.AttributeKey")
}
if (it is Body ||
it is ParameterAnnotation.PartMap ||
it is ParameterAnnotation.Part ||
it is FieldMap ||
it is Field
) {
addImport("io.ktor.client.request.setBody")
}
if (it is Path && !it.encoded) {
addImport("io.ktor.http.encodeURLPath")
}
if (it is RequestType) {
addImport("kotlin.reflect.cast")
}
if (it is Path && firstHttpMethodAnnotation.path.isEmpty()) {
logger.error(
KtorfitError.PATH_CAN_ONLY_BE_USED_WITH_RELATIVE_URL_ON + "@${firstHttpMethodAnnotation.httpMethod.keyword}",
funcDeclaration,
)
}
if (it is Url) {
if (functionParameters.filter { it.hasAnnotation() }.size > 1) {
logger.error(KtorfitError.MULTIPLE_URL_METHOD_ANNOTATIONS_FOUND, funcDeclaration)
}
if (firstHttpMethodAnnotation.path.isNotEmpty()) {
logger.error(
KtorfitError.urlCanOnlyBeUsedWithEmpty(firstHttpMethodAnnotation.httpMethod.keyword),
funcDeclaration,
)
}
}
if (it is Field && funcDeclaration.getFormUrlEncodedAnnotation() == null) {
logger.error(KtorfitError.FIELD_PARAMETERS_CAN_ONLY_BE_USED_WITH_FORM_ENCODING, funcDeclaration)
}
if (it is FieldMap && funcDeclaration.getFormUrlEncodedAnnotation() == null) {
logger.error(
KtorfitError.FIELD_MAP_PARAMETERS_CAN_ONLY_BE_USED_WITH_FORM_ENCODING,
funcDeclaration,
)
}
if (it is Body && funcDeclaration.getFormUrlEncodedAnnotation() != null) {
logger.error(
KtorfitError.BODY_PARAMETERS_CANNOT_BE_USED_WITH_FORM_OR_MULTI_PART_ENCODING,
funcDeclaration,
)
}
}
}
functionAnnotationList.forEach {
if (it is Headers || it is FormUrlEncoded) {
addImport("io.ktor.client.request.headers")
}
if (it is FormUrlEncoded ||
it is Multipart ||
functionParameters.any { param -> param.hasAnnotation() || param.hasAnnotation() }
) {
addImport("io.ktor.client.request.forms.FormDataContent")
addImport("io.ktor.client.request.forms.MultiPartFormDataContent")
addImport("io.ktor.client.request.forms.formData")
addImport("io.ktor.http.Parameters")
}
}
val modifiers =
mutableListOf(KModifier.OVERRIDE).also {
if (this.isSuspend) {
it.add(KModifier.SUSPEND)
}
}
val optInAnnotations =
funcDeclaration.annotations
.filter { it.shortName.getShortName() == "OptIn" }
.map { it.toAnnotationSpec() }
.toList()
return FunctionData(
functionName,
returnType,
funcDeclaration.isSuspend,
functionParameters,
functionAnnotationList,
firstHttpMethodAnnotation,
modifiers,
optInAnnotations
)
}