Skip to content

Commit 9b47f64

Browse files
authored
Merge pull request #270 from nomisRev/Iso
Iso and iso generation
2 parents 6da6d8c + 3e23c4f commit 9b47f64

File tree

17 files changed

+651
-147
lines changed

17 files changed

+651
-147
lines changed

kategory-annotations-processor/build.gradle

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ apply plugin: 'kotlin-kapt'
66
dependencies {
77
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlinVersion"
88
compile project(':kategory-annotations')
9-
compile 'com.squareup:kotlinpoet:0.4.0'
109
compile 'me.eugeniomarletti:kotlin-metadata:1.2.0'
1110
compileOnly 'com.google.auto.service:auto-service:1.0-rc3'
1211
kapt 'com.google.auto.service:auto-service:1.0-rc3'

kategory-annotations-processor/src/main/java/kategory/common/utils/ProcessorUtils.kt

+7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import me.eugeniomarletti.kotlin.metadata.getPropertyOrNull
1010
import me.eugeniomarletti.kotlin.metadata.getValueParameterOrNull
1111
import me.eugeniomarletti.kotlin.metadata.jvm.getJvmMethodSignature
1212
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
13+
import me.eugeniomarletti.kotlin.metadata.modality
1314
import org.jetbrains.kotlin.serialization.ProtoBuf
1415
import javax.lang.model.element.Element
1516
import javax.lang.model.element.ExecutableElement
@@ -45,6 +46,12 @@ val ProtoBuf.Class.Kind.isCompanionOrObject get() = when (this) {
4546
else -> false
4647
}
4748

49+
val ProtoBuf.Class.isSealed
50+
get() = modality == ProtoBuf.Modality.SEALED
51+
52+
val ClassOrPackageDataWrapper.Class.fullName: String
53+
get() = nameResolver.getName(classProto.fqName).asString()
54+
4855
fun ClassOrPackageDataWrapper.getParameter(function: ProtoBuf.Function, parameterElement: VariableElement) =
4956
getValueParameterOrNull(nameResolver, function, parameterElement)
5057
?: knownError("Can't find annotated parameter ${parameterElement.simpleName} in ${function.getJvmMethodSignature(nameResolver)}")
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
package kategory.optics
22

3+
import kategory.common.utils.ClassOrPackageDataWrapper
34
import javax.lang.model.element.TypeElement
4-
import javax.lang.model.element.VariableElement
55

6-
sealed class AnnotatedLens {
7-
data class Element(val type: TypeElement, val properties: Collection<VariableElement>) : AnnotatedLens()
8-
data class InvalidElement(val reason: String) : AnnotatedLens()
9-
}
10-
11-
sealed class AnnotatedPrism {
12-
data class Element(val type: TypeElement, val subTypes: Collection<TypeElement>) : AnnotatedPrism()
13-
data class InvalidElement(val reason: String) : AnnotatedPrism()
14-
}
6+
data class AnnotatedOptic(val type: TypeElement, val classData: ClassOrPackageDataWrapper.Class, val targets: List<Target>)
7+
data class Target(val fullName: String, val paramName: String)
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package kategory.optics
22

3+
import kategory.isos
4+
import kategory.lenses
5+
import kategory.prisms
6+
37
val lensesAnnotationKClass = lenses::class
48
val lensesAnnotationClass = lensesAnnotationKClass.java
59
val lensesAnnotationName = "@" + lensesAnnotationClass.simpleName
@@ -8,4 +12,9 @@ val lensesAnnotationTarget = "data class"
812
val prismsAnnotationKClass = prisms::class
913
val prismsAnnotationClass = prismsAnnotationKClass.java
1014
val prismsAnnotationName = "@" + prismsAnnotationClass.simpleName
11-
val prismsAnnotationTarget = "sealed class"
15+
val prismsAnnotationTarget = "sealed class"
16+
17+
val isosAnnotationKClass = isos::class
18+
val isosAnnotationClass = isosAnnotationKClass.java
19+
val isosAnnotationName = "@" + isosAnnotationClass.simpleName
20+
val isosAnnotationTarget = "data class"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package kategory.optics
2+
3+
import kategory.common.utils.fullName
4+
import me.eugeniomarletti.kotlin.metadata.escapedClassName
5+
import java.io.File
6+
7+
class IsosFileGenerator(
8+
private val annotatedList: Collection<AnnotatedOptic>,
9+
private val generatedDir: File
10+
) {
11+
12+
private val tuple = "kategory.Tuple"
13+
private val letters = "abcdefghij"
14+
15+
fun generate() = buildIsos(annotatedList)
16+
17+
private fun buildIsos(optics: Collection<AnnotatedOptic>) =
18+
optics.map(this::processElement)
19+
.forEach { (name, funString) ->
20+
File(generatedDir, isosAnnotationClass.simpleName + ".$name.kt").printWriter().use { w ->
21+
w.println(funString)
22+
}
23+
}
24+
25+
private fun processElement(annotatedIso: AnnotatedOptic): Pair<String, String> {
26+
val sourceClassName = annotatedIso.classData.fullName.escapedClassName
27+
val sourceName = annotatedIso.type.simpleName.toString().toLowerCase()
28+
val targetName = annotatedIso.targets.map(Target::fullName)
29+
30+
return sourceName to """
31+
|package ${annotatedIso.classData.`package`.escapedClassName}
32+
|
33+
|fun ${sourceName}Iso() = ${isoConstructor(sourceClassName, targetName)}(
34+
| get = { $sourceName: $sourceClassName -> ${tupleConstructor(annotatedIso.targets, sourceName)} },
35+
| reverseGet = { tuple: ${tupleType(targetName)} -> ${classConstructorFromTuple(sourceClassName, targetName.size)} }
36+
|)
37+
|""".trimMargin()
38+
}
39+
40+
private fun isoConstructor(sourceName: String, targetTypes: List<String>) = "kategory.optics.Iso<$sourceName, ${tupleType(targetTypes)}>"
41+
42+
private fun tupleConstructor(targetTypes: List<Target>, sourceName: String) =
43+
targetTypes.joinToString(prefix = "$tuple${targetTypes.size}(", postfix = ")", transform = { "$sourceName.${it.paramName}" })
44+
45+
private fun tupleType(targetTypes: List<String>) =
46+
targetTypes.joinToString(prefix = "$tuple${targetTypes.size}<", postfix = ">")
47+
48+
private fun classConstructorFromTuple(sourceClassName: String, propertiesSize: Int) =
49+
(0 until propertiesSize).joinToString(prefix = "$sourceClassName(", postfix = ")", transform = { "tuple.${letters[it]}" })
50+
51+
}
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,39 @@
11
package kategory.optics
22

3-
import com.squareup.kotlinpoet.ClassName
4-
import com.squareup.kotlinpoet.FunSpec
5-
import com.squareup.kotlinpoet.KotlinFile
6-
import com.squareup.kotlinpoet.asClassName
3+
import kategory.common.utils.fullName
4+
import me.eugeniomarletti.kotlin.metadata.escapedClassName
75
import java.io.File
86

97
class LensesFileGenerator(
10-
private val annotatedList: Collection<AnnotatedLens.Element>,
8+
private val annotatedList: Collection<AnnotatedOptic>,
119
private val generatedDir: File
1210
) {
1311

14-
fun generate() = buildLenses(annotatedList).forEach {
15-
it.writeTo(generatedDir)
16-
}
12+
private val lens = "kategory.optics.Lens"
1713

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

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

30-
FunSpec.builder("$className${variableName.toString().capitalize()}")
31-
.addStatement(
32-
"""return kategory.optics.Lens(
33-
| get = { $className: %T -> $className.$variableName },
34-
| set = { $variableName: %T ->
35-
| { $className: %T ->
36-
| $className.copy($variableName = $variableName)
37-
| }
38-
| }
39-
|)""".trimMargin(), annotatedLens.type, variable, annotatedLens.type)
40-
.build()
27+
"""
28+
|fun $sourceName${targetName.capitalize()}() = $lens(
29+
| get = { $sourceName: $sourceClassName -> $sourceName.$targetName },
30+
| set = { $targetName: $targetClassName ->
31+
| { $sourceName: $sourceClassName ->
32+
| $sourceName.copy($targetName = $targetName)
33+
| }
34+
| }
35+
|)
36+
""".trimMargin()
4137
}
4238

4339
}

kategory-annotations-processor/src/main/java/kategory/optics/OpticsProcessor.kt

+61-18
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,37 @@ package kategory.optics
22

33
import com.google.auto.service.AutoService
44
import kategory.common.utils.AbstractProcessor
5+
import kategory.common.utils.asClassOrPackageDataWrapper
6+
import kategory.common.utils.isSealed
57
import kategory.common.utils.knownError
68
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
9+
import me.eugeniomarletti.kotlin.metadata.extractFullName
710
import me.eugeniomarletti.kotlin.metadata.isDataClass
811
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
9-
import me.eugeniomarletti.kotlin.metadata.modality
12+
import me.eugeniomarletti.kotlin.metadata.proto
1013
import org.jetbrains.kotlin.serialization.ProtoBuf
1114
import java.io.File
1215
import javax.annotation.processing.Processor
1316
import javax.annotation.processing.RoundEnvironment
1417
import javax.lang.model.SourceVersion
15-
import javax.lang.model.element.Element
16-
import javax.lang.model.element.ElementKind
17-
import javax.lang.model.element.ExecutableElement
1818
import javax.lang.model.element.TypeElement
1919

20+
import javax.lang.model.element.Element
21+
2022
@AutoService(Processor::class)
2123
class OptikalProcessor : AbstractProcessor() {
2224

23-
private val annotatedLenses = mutableListOf<AnnotatedLens.Element>()
25+
private val annotatedLenses = mutableListOf<AnnotatedOptic>()
2426

25-
private val annotatedPrisms = mutableListOf<AnnotatedPrism.Element>()
27+
private val annotatedPrisms = mutableListOf<AnnotatedOptic>()
2628

27-
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
29+
private val annotatedIsos = mutableListOf<AnnotatedOptic>()
2830

31+
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
2932
override fun getSupportedAnnotationTypes() = setOf(
3033
lensesAnnotationClass.canonicalName,
31-
prismsAnnotationClass.canonicalName
34+
prismsAnnotationClass.canonicalName,
35+
isosAnnotationClass.canonicalName
3236
)
3337

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

47+
annotatedIsos += roundEnv
48+
.getElementsAnnotatedWith(isosAnnotationClass)
49+
.map(this::evalAnnotatedIsoElement)
50+
4351
if (roundEnv.processingOver()) {
4452
val generatedDir = File(this.generatedDir!!, "").also { it.mkdirs() }
4553
LensesFileGenerator(annotatedLenses, generatedDir).generate()
4654
PrismsFileGenerator(annotatedPrisms, generatedDir).generate()
55+
IsosFileGenerator(annotatedIsos, generatedDir).generate()
4756
}
4857
}
4958

50-
private fun evalAnnotatedElement(element: Element): AnnotatedLens.Element = when {
59+
private fun evalAnnotatedElement(element: Element): AnnotatedOptic = when {
5160
element.let { it.kotlinMetadata as? KotlinClassMetadata }?.data?.classProto?.isDataClass == true ->
52-
AnnotatedLens.Element(
61+
AnnotatedOptic(
5362
element as TypeElement,
54-
element.enclosedElements.firstOrNull { it.kind == ElementKind.CONSTRUCTOR }
55-
?.let { it as ExecutableElement }
56-
?.parameters ?: emptyList()
63+
getClassData(element),
64+
getConstructorTypesNames(element).zip(getConstructorParamNames(element), ::Target)
5765
)
5866

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

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

66-
AnnotatedPrism.Element(
74+
AnnotatedOptic(
6775
element as TypeElement,
76+
getClassData(element),
6877
classProto.sealedSubclassFqNameList
6978
.map(nameResolver::getString)
7079
.map { it.replace('/', '.') }
71-
.mapNotNull(elementUtils::getTypeElement)
80+
.map { Target(it, it.substringAfterLast(".")) }
7281
)
7382
}
7483

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

82-
private val ProtoBuf.Class.isSealed
83-
get() = modality == ProtoBuf.Modality.SEALED
91+
private fun evalAnnotatedIsoElement(element: Element): AnnotatedOptic = when {
92+
(element.kotlinMetadata as? KotlinClassMetadata)?.data?.classProto?.isDataClass == true -> {
93+
val properties = getConstructorTypesNames(element).zip(getConstructorParamNames(element), ::Target)
94+
95+
if (properties.size < 2 || properties.size > 10)
96+
knownError("${element.enclosingElement}.${element.simpleName} constructor parameters should be between 2 and 10")
97+
else
98+
AnnotatedOptic(element as TypeElement, getClassData(element), properties)
99+
}
100+
101+
else -> knownError(opticsAnnotationError(element, isosAnnotationName, isosAnnotationTarget))
102+
}
103+
104+
private fun getConstructorTypesNames(element: Element): List<String> = element.kotlinMetadata
105+
.let { it as KotlinClassMetadata }.data
106+
.let { data ->
107+
data.proto.constructorOrBuilderList
108+
.first()
109+
.valueParameterList
110+
.map { it.type.extractFullName(data) }
111+
}
112+
113+
private fun getConstructorParamNames(element: Element): List<String> = element.kotlinMetadata
114+
.let { it as KotlinClassMetadata }.data
115+
.let { (nameResolver, classProto) ->
116+
classProto.constructorOrBuilderList
117+
.first()
118+
.valueParameterList
119+
.map(ProtoBuf.ValueParameter::getName)
120+
.map(nameResolver::getString)
121+
}
122+
123+
private fun getClassData(element: Element) = element.kotlinMetadata
124+
.let { it as KotlinClassMetadata }
125+
.data
126+
.asClassOrPackageDataWrapper(elementUtils.getPackageOf(element).toString())
84127

85128
}

0 commit comments

Comments
 (0)