Skip to content

Commit

Permalink
Autofold (#448)
Browse files Browse the repository at this point in the history
* Annotation

* Processor
  • Loading branch information
nomisRev authored and raulraja committed Nov 13, 2017
1 parent 185cf36 commit 6993464
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package kategory.fold

import kategory.common.utils.ClassOrPackageDataWrapper
import javax.lang.model.element.TypeElement

data class AnnotatedFold(
val type: TypeElement,
val typeParams: List<String>,
val classData: ClassOrPackageDataWrapper.Class,
val targets: List<Variant>
)
data class Variant(val fullName: String, val typeParams: List<String>, val simpleName: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kategory.fold

import kategory.autofold

val foldAnnotationKClass = autofold::class
val foldAnnotationClass = foldAnnotationKClass.java
val foldAnnotationName = "@" + foldAnnotationClass.simpleName
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package kategory.fold

import kategory.common.utils.fullName
import me.eugeniomarletti.kotlin.metadata.escapedClassName
import java.io.File

class AutoFoldFileGenerator(
private val annotatedList: Collection<AnnotatedFold>,
private val generatedDir: File
) {

fun generate() = annotatedList.map(this::processElement)
.map { (element, fold) ->
"${foldAnnotationClass.simpleName}.${element.type.simpleName.toString().toLowerCase()}.kt" to
fileHeader(element.classData.`package`.escapedClassName) + fold
}.map { (name, fileString) -> File(generatedDir, name).writeText(fileString) }

private fun processElement(annotatedFold: AnnotatedFold): Pair<AnnotatedFold, String> =
annotatedFold to annotatedFold.targets.let { targets ->
val sourceClassName = annotatedFold.classData.fullName.escapedClassName
val sumTypeParams = typeParams(annotatedFold.typeParams)
val returnType = getFoldType(annotatedFold.typeParams)
val functionTypeParams = functionTypeParams(annotatedFold.typeParams, returnType)

"""inline fun $functionTypeParams $sourceClassName$sumTypeParams.fold(
|${params(targets, returnType)}
|): $returnType = when (this) {
|${patternMatching(targets)}
|}
""".trimMargin()
}

fun typeParams(params: List<String>): String =
if (params.isNotEmpty()) params.joinToString(prefix = "<", postfix = ">")
else ""

fun params(variants: List<Variant>, returnType: String): String = variants.joinToString(transform = { variant ->
" crossinline ${variant.simpleName.decapitalize()}: (${variant.fullName.escapedClassName}${typeParams(variant.typeParams)}) -> $returnType"
}, separator = ",\n")

fun patternMatching(variants: List<Variant>): String = variants.joinToString(transform = { variant ->
" is ${variant.fullName.escapedClassName} -> ${variant.simpleName.decapitalize().escapedClassName}(this)"
}, separator = "\n")

fun functionTypeParams(params: List<String>, returnType: String): String =
if (params.isEmpty()) ""
else params.joinToString(prefix = "<", postfix = ", $returnType>")

fun getFoldType(params: List<String>): String {
fun check(param: String, next: List<String>): String = (param[0] + 1).let {
if (next.contains(it.toString())) check(next.firstOrNull() ?: "", next.drop(1))
else it.toString()
}

return check(params.firstOrNull() ?: "", params.drop(1))
}

fun fileHeader(packageName: String): String =
"""package $packageName
|
|""".trimMargin()

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package kategory.fold

import com.google.auto.service.AutoService
import kategory.common.utils.AbstractProcessor
import kategory.common.utils.asClassOrPackageDataWrapper
import kategory.common.utils.isSealed
import kategory.common.utils.knownError
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
import java.io.File
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.TypeElement
import javax.lang.model.element.TypeParameterElement

@AutoService(Processor::class)
class AutoFoldProcessor : AbstractProcessor() {

private val annotatedList = mutableListOf<AnnotatedFold>()

override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()

override fun getSupportedAnnotationTypes(): Set<String> = setOf(foldAnnotationClass.canonicalName)

/**
* Processor entry point
*/
override fun onProcess(annotations: Set<TypeElement>, roundEnv: RoundEnvironment) {
annotatedList += roundEnv
.getElementsAnnotatedWith(foldAnnotationClass)
.map { element ->
when {
element.let { it.kotlinMetadata as? KotlinClassMetadata }?.data?.classProto?.isSealed == true -> {
val (nameResolver, classProto) = element.kotlinMetadata.let { it as KotlinClassMetadata }.data

AnnotatedFold(
element as TypeElement,
element.typeParameters.map(TypeParameterElement::toString),
element.kotlinMetadata
.let { it as KotlinClassMetadata }
.data
.asClassOrPackageDataWrapper(elementUtils.getPackageOf(element).toString()),
classProto.sealedSubclassFqNameList
.map(nameResolver::getString)
.map { it.replace('/', '.') }
.map {
Variant(it,
elementUtils.getTypeElement(it).typeParameters.map(TypeParameterElement::toString),
it.substringAfterLast("."))
}
)
}

else -> knownError("Generation of fold is only supported for sealed classes.")
}
}

if (roundEnv.processingOver()) {
val generatedDir = File(this.generatedDir!!, foldAnnotationClass.simpleName).also { it.mkdirs() }
AutoFoldFileGenerator(annotatedList, generatedDir).generate()
}
}

}
6 changes: 6 additions & 0 deletions kategory-annotations/src/main/java/kategory/autofold.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kategory

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@MustBeDocumented
annotation class autofold

0 comments on commit 6993464

Please sign in to comment.