Skip to content
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

Iso and iso generation #270

Merged
merged 11 commits into from
Sep 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion kategory-annotations-processor/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ apply plugin: 'kotlin-kapt'
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlinVersion"
compile project(':kategory-annotations')
compile 'com.squareup:kotlinpoet:0.4.0'
compile 'me.eugeniomarletti:kotlin-metadata:1.2.0'
compileOnly 'com.google.auto.service:auto-service:1.0-rc3'
kapt 'com.google.auto.service:auto-service:1.0-rc3'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import me.eugeniomarletti.kotlin.metadata.getPropertyOrNull
import me.eugeniomarletti.kotlin.metadata.getValueParameterOrNull
import me.eugeniomarletti.kotlin.metadata.jvm.getJvmMethodSignature
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
import me.eugeniomarletti.kotlin.metadata.modality
import org.jetbrains.kotlin.serialization.ProtoBuf
import javax.lang.model.element.Element
import javax.lang.model.element.ExecutableElement
Expand Down Expand Up @@ -45,6 +46,12 @@ val ProtoBuf.Class.Kind.isCompanionOrObject get() = when (this) {
else -> false
}

val ProtoBuf.Class.isSealed
get() = modality == ProtoBuf.Modality.SEALED

val ClassOrPackageDataWrapper.Class.fullName: String
get() = nameResolver.getName(classProto.fqName).asString()

fun ClassOrPackageDataWrapper.getParameter(function: ProtoBuf.Function, parameterElement: VariableElement) =
getValueParameterOrNull(nameResolver, function, parameterElement)
?: knownError("Can't find annotated parameter ${parameterElement.simpleName} in ${function.getJvmMethodSignature(nameResolver)}")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
package kategory.optics

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

sealed class AnnotatedLens {
data class Element(val type: TypeElement, val properties: Collection<VariableElement>) : AnnotatedLens()
data class InvalidElement(val reason: String) : AnnotatedLens()
}

sealed class AnnotatedPrism {
data class Element(val type: TypeElement, val subTypes: Collection<TypeElement>) : AnnotatedPrism()
data class InvalidElement(val reason: String) : AnnotatedPrism()
}
data class AnnotatedOptic(val type: TypeElement, val classData: ClassOrPackageDataWrapper.Class, val targets: List<Target>)
data class Target(val fullName: String, val paramName: String)
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package kategory.optics

import kategory.isos
import kategory.lenses
import kategory.prisms

val lensesAnnotationKClass = lenses::class
val lensesAnnotationClass = lensesAnnotationKClass.java
val lensesAnnotationName = "@" + lensesAnnotationClass.simpleName
Expand All @@ -8,4 +12,9 @@ val lensesAnnotationTarget = "data class"
val prismsAnnotationKClass = prisms::class
val prismsAnnotationClass = prismsAnnotationKClass.java
val prismsAnnotationName = "@" + prismsAnnotationClass.simpleName
val prismsAnnotationTarget = "sealed class"
val prismsAnnotationTarget = "sealed class"

val isosAnnotationKClass = isos::class
val isosAnnotationClass = isosAnnotationKClass.java
val isosAnnotationName = "@" + isosAnnotationClass.simpleName
val isosAnnotationTarget = "data class"
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package kategory.optics

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

class IsosFileGenerator(
private val annotatedList: Collection<AnnotatedOptic>,
private val generatedDir: File
) {

private val tuple = "kategory.Tuple"
private val letters = "abcdefghij"

fun generate() = buildIsos(annotatedList)

private fun buildIsos(optics: Collection<AnnotatedOptic>) =
optics.map(this::processElement)
.forEach { (name, funString) ->
File(generatedDir, isosAnnotationClass.simpleName + ".$name.kt").printWriter().use { w ->
w.println(funString)
}
}

private fun processElement(annotatedIso: AnnotatedOptic): Pair<String, String> {
val sourceClassName = annotatedIso.classData.fullName.escapedClassName
val sourceName = annotatedIso.type.simpleName.toString().toLowerCase()
val targetName = annotatedIso.targets.map(Target::fullName)

return sourceName to """
|package ${annotatedIso.classData.`package`.escapedClassName}
|
|fun ${sourceName}Iso() = ${isoConstructor(sourceClassName, targetName)}(
| get = { $sourceName: $sourceClassName -> ${tupleConstructor(annotatedIso.targets, sourceName)} },
| reverseGet = { tuple: ${tupleType(targetName)} -> ${classConstructorFromTuple(sourceClassName, targetName.size)} }
|)
|""".trimMargin()
}

private fun isoConstructor(sourceName: String, targetTypes: List<String>) = "kategory.optics.Iso<$sourceName, ${tupleType(targetTypes)}>"

private fun tupleConstructor(targetTypes: List<Target>, sourceName: String) =
targetTypes.joinToString(prefix = "$tuple${targetTypes.size}(", postfix = ")", transform = { "$sourceName.${it.paramName}" })

private fun tupleType(targetTypes: List<String>) =
targetTypes.joinToString(prefix = "$tuple${targetTypes.size}<", postfix = ">")

private fun classConstructorFromTuple(sourceClassName: String, propertiesSize: Int) =
(0 until propertiesSize).joinToString(prefix = "$sourceClassName(", postfix = ")", transform = { "tuple.${letters[it]}" })

}
Original file line number Diff line number Diff line change
@@ -1,43 +1,39 @@
package kategory.optics

import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KotlinFile
import com.squareup.kotlinpoet.asClassName
import kategory.common.utils.fullName
import me.eugeniomarletti.kotlin.metadata.escapedClassName
import java.io.File

class LensesFileGenerator(
private val annotatedList: Collection<AnnotatedLens.Element>,
private val annotatedList: Collection<AnnotatedOptic>,
private val generatedDir: File
) {

fun generate() = buildLenses(annotatedList).forEach {
it.writeTo(generatedDir)
}
private val lens = "kategory.optics.Lens"

private fun buildLenses(elements: Collection<AnnotatedLens.Element>): List<KotlinFile> = elements.map(this::processElement)
.map { (name, funs) ->
funs.fold(KotlinFile.builder(name.packageName(), "${name.simpleName().toLowerCase()}.lenses").skipJavaLangImports(true), { builder, lensSpec ->
builder.addFun(lensSpec)
}).build()
}
fun generate() = annotatedList.map(this::processElement)
.map { (element, funs) ->
"${lensesAnnotationClass.simpleName}.${element.type.simpleName.toString().toLowerCase()}.kt" to
funs.joinToString(prefix = "package ${element.classData.`package`.escapedClassName}\n\n", separator = "\n")
}.forEach { (name, fileString) -> File(generatedDir, name).writeText(fileString) }

private fun processElement(annotatedLens: AnnotatedLens.Element): Pair<ClassName, List<FunSpec>> =
annotatedLens.type.asClassName() to annotatedLens.properties.map { variable ->
val className = annotatedLens.type.simpleName.toString().toLowerCase()
val variableName = variable.simpleName
private fun processElement(annotatedOptic: AnnotatedOptic): Pair<AnnotatedOptic, List<String>> =
annotatedOptic to annotatedOptic.targets.map { variable ->
val sourceClassName = annotatedOptic.classData.fullName.escapedClassName
val sourceName = annotatedOptic.type.simpleName.toString().toLowerCase()
val targetClassName = variable.fullName
val targetName = variable.paramName

FunSpec.builder("$className${variableName.toString().capitalize()}")
.addStatement(
"""return kategory.optics.Lens(
| get = { $className: %T -> $className.$variableName },
| set = { $variableName: %T ->
| { $className: %T ->
| $className.copy($variableName = $variableName)
| }
| }
|)""".trimMargin(), annotatedLens.type, variable, annotatedLens.type)
.build()
"""
|fun $sourceName${targetName.capitalize()}() = $lens(
| get = { $sourceName: $sourceClassName -> $sourceName.$targetName },
| set = { $targetName: $targetClassName ->
| { $sourceName: $sourceClassName ->
| $sourceName.copy($targetName = $targetName)
| }
| }
|)
""".trimMargin()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,37 @@ package kategory.optics

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.extractFullName
import me.eugeniomarletti.kotlin.metadata.isDataClass
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
import me.eugeniomarletti.kotlin.metadata.modality
import me.eugeniomarletti.kotlin.metadata.proto
import org.jetbrains.kotlin.serialization.ProtoBuf
import java.io.File
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.TypeElement

import javax.lang.model.element.Element

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

private val annotatedLenses = mutableListOf<AnnotatedLens.Element>()
private val annotatedLenses = mutableListOf<AnnotatedOptic>()

private val annotatedPrisms = mutableListOf<AnnotatedPrism.Element>()
private val annotatedPrisms = mutableListOf<AnnotatedOptic>()

override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
private val annotatedIsos = mutableListOf<AnnotatedOptic>()

override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
override fun getSupportedAnnotationTypes() = setOf(
lensesAnnotationClass.canonicalName,
prismsAnnotationClass.canonicalName
prismsAnnotationClass.canonicalName,
isosAnnotationClass.canonicalName
)

override fun onProcess(annotations: Set<TypeElement>, roundEnv: RoundEnvironment) {
Expand All @@ -40,35 +44,40 @@ class OptikalProcessor : AbstractProcessor() {
.getElementsAnnotatedWith(prismsAnnotationClass)
.map(this::evalAnnotatedPrismElement)

annotatedIsos += roundEnv
.getElementsAnnotatedWith(isosAnnotationClass)
.map(this::evalAnnotatedIsoElement)

if (roundEnv.processingOver()) {
val generatedDir = File(this.generatedDir!!, "").also { it.mkdirs() }
LensesFileGenerator(annotatedLenses, generatedDir).generate()
PrismsFileGenerator(annotatedPrisms, generatedDir).generate()
IsosFileGenerator(annotatedIsos, generatedDir).generate()
}
}

private fun evalAnnotatedElement(element: Element): AnnotatedLens.Element = when {
private fun evalAnnotatedElement(element: Element): AnnotatedOptic = when {
element.let { it.kotlinMetadata as? KotlinClassMetadata }?.data?.classProto?.isDataClass == true ->
AnnotatedLens.Element(
AnnotatedOptic(
element as TypeElement,
element.enclosedElements.firstOrNull { it.kind == ElementKind.CONSTRUCTOR }
?.let { it as ExecutableElement }
?.parameters ?: emptyList()
getClassData(element),
getConstructorTypesNames(element).zip(getConstructorParamNames(element), ::Target)
)

else -> knownError(opticsAnnotationError(element, lensesAnnotationName, lensesAnnotationTarget))
}

private fun evalAnnotatedPrismElement(element: Element): AnnotatedPrism.Element = when {
private fun evalAnnotatedPrismElement(element: Element): AnnotatedOptic = when {
element.let { it.kotlinMetadata as? KotlinClassMetadata }?.data?.classProto?.isSealed == true -> {
val (nameResolver, classProto) = element.kotlinMetadata.let { it as KotlinClassMetadata }.data

AnnotatedPrism.Element(
AnnotatedOptic(
element as TypeElement,
getClassData(element),
classProto.sealedSubclassFqNameList
.map(nameResolver::getString)
.map { it.replace('/', '.') }
.mapNotNull(elementUtils::getTypeElement)
.map { Target(it, it.substringAfterLast(".")) }
)
}

Expand All @@ -79,7 +88,41 @@ class OptikalProcessor : AbstractProcessor() {
|Cannot use $annotationName on ${element.enclosingElement}.${element.simpleName}.
|It can only be used on $targetName.""".trimMargin()

private val ProtoBuf.Class.isSealed
get() = modality == ProtoBuf.Modality.SEALED
private fun evalAnnotatedIsoElement(element: Element): AnnotatedOptic = when {
(element.kotlinMetadata as? KotlinClassMetadata)?.data?.classProto?.isDataClass == true -> {
val properties = getConstructorTypesNames(element).zip(getConstructorParamNames(element), ::Target)

if (properties.size < 2 || properties.size > 10)
knownError("${element.enclosingElement}.${element.simpleName} constructor parameters should be between 2 and 10")
else
AnnotatedOptic(element as TypeElement, getClassData(element), properties)
}

else -> knownError(opticsAnnotationError(element, isosAnnotationName, isosAnnotationTarget))
}

private fun getConstructorTypesNames(element: Element): List<String> = element.kotlinMetadata
.let { it as KotlinClassMetadata }.data
.let { data ->
data.proto.constructorOrBuilderList
.first()
.valueParameterList
.map { it.type.extractFullName(data) }
}

private fun getConstructorParamNames(element: Element): List<String> = element.kotlinMetadata
.let { it as KotlinClassMetadata }.data
.let { (nameResolver, classProto) ->
classProto.constructorOrBuilderList
.first()
.valueParameterList
.map(ProtoBuf.ValueParameter::getName)
.map(nameResolver::getString)
}

private fun getClassData(element: Element) = element.kotlinMetadata
.let { it as KotlinClassMetadata }
.data
.asClassOrPackageDataWrapper(elementUtils.getPackageOf(element).toString())

}
Loading