diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/GenerateAccessor.java b/crystal-map-api/src/main/java/com/schwarz/crystalapi/GenerateAccessor.java index 7056a46b..399f3cea 100644 --- a/crystal-map-api/src/main/java/com/schwarz/crystalapi/GenerateAccessor.java +++ b/crystal-map-api/src/main/java/com/schwarz/crystalapi/GenerateAccessor.java @@ -11,7 +11,4 @@ public @interface GenerateAccessor { Class value() default Void.class; - - // We need to explicitly specify this since the information is lost during annotation processing - boolean isNullableSuspendFun() default false; } diff --git a/crystal-map-api/src/main/java/com/schwarz/crystalapi/util/CrystalWrap.kt b/crystal-map-api/src/main/java/com/schwarz/crystalapi/util/CrystalWrap.kt index 613cb2c3..a33e3cd4 100644 --- a/crystal-map-api/src/main/java/com/schwarz/crystalapi/util/CrystalWrap.kt +++ b/crystal-map-api/src/main/java/com/schwarz/crystalapi/util/CrystalWrap.kt @@ -69,7 +69,7 @@ object CrystalWrap { fieldName: String ): List? = (changes[fieldName] ?: doc[fieldName])?.let { value -> catchTypeConversionError(fieldName, value) { - value as List + (value as List).map { it as T } } } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/CodeGenerator.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/CodeGenerator.kt index 68418ad2..1501e41e 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/CodeGenerator.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/generation/CodeGenerator.kt @@ -2,14 +2,21 @@ package com.schwarz.crystalprocessor.generation import com.schwarz.crystalprocessor.CoachBaseBinderProcessor import com.schwarz.crystalprocessor.ProcessingContext +import com.schwarz.crystalprocessor.model.accessor.CblGenerateAccessorHolder import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.OriginatingElementsHolder import java.io.File import java.io.IOException import javax.annotation.processing.Filer import javax.annotation.processing.ProcessingEnvironment +import javax.tools.StandardLocation +import kotlin.io.path.createDirectories +import kotlin.io.path.isDirectory +import kotlin.io.path.notExists +import kotlin.io.path.outputStream class CodeGenerator(private val filer: Filer) { @@ -30,6 +37,59 @@ class CodeGenerator(private val filer: Filer) { } } + @Throws(IOException::class) + fun generateAndFixAccessors( + entityToGenerate: FileSpec, + generateAccessors: MutableList, + processingEnvironment: ProcessingEnvironment + ) { + ClassName(entityToGenerate.packageName, entityToGenerate.name)?.apply { + ProcessingContext.createdQualifiedClazzNames.add(this) + } + + val codePath = processingEnvironment.options[CoachBaseBinderProcessor.KAPT_KOTLIN_GENERATED_OPTION_NAME] + val fileWithHeader = entityToGenerate.toBuilder().addFileComment(HEADER).build() + + val fixedFileString = generateAccessors.fold(fileWithHeader.toString()) { acc, generateAccessor -> + if (generateAccessor.memberFunction != null && generateAccessor.memberFunction.isSuspend) { + acc.replace(Regex("(${generateAccessor.memberFunction.name}\\([^)]*\\)):\\s*Unit(\\s*=)"), "$1$2") + } else { + acc + } + } + + // used for kapt returns null for legacy annotationprocessor declarations + if (codePath != null) { + val directory = File(codePath).toPath() + require(directory.notExists() || directory.isDirectory()) { + "path $directory exists but is not a directory." + } + val outputPath = directory.resolve(fileWithHeader.relativePath) + outputPath.parent.createDirectories() + outputPath.outputStream().bufferedWriter().use { it.write(fixedFileString) } + } else { + val originatingElements = fileWithHeader.members.asSequence() + .filterIsInstance() + .flatMap { it.originatingElements.asSequence() } + .toSet() + val filerSourceFile = filer.createResource( + StandardLocation.SOURCE_OUTPUT, + fileWithHeader.packageName, + "${fileWithHeader.name}.kt", + *originatingElements.toTypedArray() + ) + try { + filerSourceFile.openWriter().use { it.write(fixedFileString) } + } catch (e: Exception) { + try { + filerSourceFile.delete() + } catch (ignored: Exception) { + } + throw e + } + } + } + companion object { private val HEADER = ( diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/accessor/CblGenerateAccessorHolder.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/accessor/CblGenerateAccessorHolder.kt index b8632ef4..278acb5a 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/accessor/CblGenerateAccessorHolder.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/accessor/CblGenerateAccessorHolder.kt @@ -9,8 +9,8 @@ import com.squareup.kotlinpoet.TypeName class CblGenerateAccessorHolder( private val sourceClassTypeName: TypeName, - private val memberFunction: SourceMemberFunction?, - private val memberProperty: SourceMemberField? + val memberFunction: SourceMemberFunction?, + val memberProperty: SourceMemberField? ) { fun accessorFunSpec(): FunSpec? { @@ -32,11 +32,11 @@ class CblGenerateAccessorHolder( memberFunction.name ) - val isNullableSuspendFun = memberFunction.generateAccessor?.isNullableSuspendFun ?: false - - if (isNullableSuspendFun) { - methodBuilder.returns(memberFunction.returnTypeName.copy(nullable = true)) - } else { + // We only specify a return value if the function is non-suspending. If the function + // is suspending, we cannot safely obtain the return type. Since KotlinPoet doesn't + // allow us to use implicit return types, we default to Unit and remove it later + // in the CodeGenerator. + if (!memberFunction.isSuspend) { methodBuilder.returns(memberFunction.returnTypeName) } diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/source/SourceModel.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/source/SourceModel.kt index e1d70be3..25261a0b 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/source/SourceModel.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/model/source/SourceModel.kt @@ -20,7 +20,6 @@ import com.schwarz.crystalapi.Reduces import com.schwarz.crystalapi.deprecated.Deprecated import com.schwarz.crystalapi.query.Queries import com.schwarz.crystalapi.query.Query -import com.sun.tools.javac.code.Type import org.apache.commons.lang3.text.WordUtils import org.jetbrains.annotations.Nullable import javax.lang.model.element.Element @@ -111,14 +110,7 @@ data class SourceModel(private val sourceElement: Element) : ISourceModel, IClas } } - val returnType = if (isSuspend) { - val continuationParam = (it.parameters.last()) as Symbol.VarSymbol - val continuationParamType = continuationParam.type as Type.ClassType - val wildcardTypeParam = continuationParamType.allparams().first() as Type.WildcardType - wildcardTypeParam.type.asTypeName().javaToKotlinType() - } else { - it.returnType.asTypeName().javaToKotlinType().copy(it.getAnnotation(Nullable::class.java) != null) - } + val returnType = it.returnType.asTypeName().javaToKotlinType().copy(it.getAnnotation(Nullable::class.java) != null) relevantStaticsFunctions.add( SourceMemberFunction( diff --git a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorker.kt b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorker.kt index 773e62b1..0195dba9 100644 --- a/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorker.kt +++ b/crystal-map-processor/src/main/java/com/schwarz/crystalprocessor/processing/model/ModelWorker.kt @@ -70,11 +70,11 @@ class ModelWorker(override val logger: Logger, override val codeGenerator: CodeG generateInterface(generatedInterfaces, baseModelHolder, useSuspend, typeConvertersByConvertedClass) } - process(workSet.entities, generatedInterfaces, useSuspend, typeConvertersByConvertedClass) { + processAndFixAccessors(workSet.entities, generatedInterfaces, useSuspend, typeConvertersByConvertedClass) { EntityGeneration().generateModel(it, useSuspend, typeConvertersByConvertedClass) } - process(workSet.wrappers, generatedInterfaces, useSuspend, typeConvertersByConvertedClass) { + processAndFixAccessors(workSet.wrappers, generatedInterfaces, useSuspend, typeConvertersByConvertedClass) { WrapperGeneration().generateModel(it, useSuspend, typeConvertersByConvertedClass) } @@ -105,6 +105,35 @@ class ModelWorker(override val logger: Logger, override val codeGenerator: CodeG } } + private fun processAndFixAccessors( + models: List, + generatedInterfaces: MutableSet, + useSuspend: Boolean, + typeConvertersByConvertedClass: Map, + generate: (T) -> FileSpec + ) { + for (model in models) { + generateInterface(generatedInterfaces, model, useSuspend, typeConvertersByConvertedClass) + documentationGenerator?.addEntitySegments(model) + schemaGenerator?.addEntity(model) + entityRelationshipGenerator?.addEntityNodes(model) + generate(model).apply { + if (model.generateAccessors.isNotEmpty()) { + codeGenerator.generateAndFixAccessors( + this, + model.generateAccessors, + processingEnv + ) + } else { + codeGenerator.generate( + this, + processingEnv + ) + } + } + } + } + private fun generateInterface( generatedInterfaces: MutableSet, holder: BaseEntityHolder, diff --git a/demo/src/main/java/com/schwarz/crystaldemo/tesst/Task.kt b/demo/src/main/java/com/schwarz/crystaldemo/tesst/Task.kt index fa683b61..50ac5614 100644 --- a/demo/src/main/java/com/schwarz/crystaldemo/tesst/Task.kt +++ b/demo/src/main/java/com/schwarz/crystaldemo/tesst/Task.kt @@ -35,15 +35,30 @@ open class Task { return "" } + @GenerateAccessor + fun ultraComplexQueryReturningEntity(storeId: String): List { + return emptyList() + } + @GenerateAccessor suspend fun suspendingUltraComplexQueryReturningList(storeId: String): List { return listOf("") } - @GenerateAccessor(isNullableSuspendFun = true) + @GenerateAccessor() suspend fun suspendingUltraComplexQueryReturningNullableList(storeId: String): List? { return null } + + @GenerateAccessor() + suspend fun suspendingUltraComplexQueryReturningEntity(storeId: String): List { + return emptyList() + } + + @GenerateAccessor() + suspend fun suspendingUltraComplexQueryWithAVeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeryLongName(storeId: String): List { + return emptyList() + } } // // override fun documentId(): String {