Skip to content

Commit 6da6d8c

Browse files
authored
Merge pull request #267 from nomisRev/prism-processor
Prism processor
2 parents b63e270 + 84157ea commit 6da6d8c

File tree

14 files changed

+165
-50
lines changed

14 files changed

+165
-50
lines changed

kategory-annotations-processor/src/main/java/kategory/optics/AnnotatedLens.kt kategory-annotations-processor/src/main/java/kategory/optics/AnnotatedDomain.kt

+5
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ import javax.lang.model.element.VariableElement
66
sealed class AnnotatedLens {
77
data class Element(val type: TypeElement, val properties: Collection<VariableElement>) : AnnotatedLens()
88
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()
914
}

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,10 @@ package kategory.optics
22

33
val lensesAnnotationKClass = lenses::class
44
val lensesAnnotationClass = lensesAnnotationKClass.java
5-
val lensesAnnotationName = "@" + lensesAnnotationClass.simpleName
5+
val lensesAnnotationName = "@" + lensesAnnotationClass.simpleName
6+
val lensesAnnotationTarget = "data class"
7+
8+
val prismsAnnotationKClass = prisms::class
9+
val prismsAnnotationClass = prismsAnnotationKClass.java
10+
val prismsAnnotationName = "@" + prismsAnnotationClass.simpleName
11+
val prismsAnnotationTarget = "sealed class"

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package kategory.optics
22

3+
import com.squareup.kotlinpoet.ClassName
34
import com.squareup.kotlinpoet.FunSpec
45
import com.squareup.kotlinpoet.KotlinFile
6+
import com.squareup.kotlinpoet.asClassName
57
import java.io.File
68

79
class LensesFileGenerator(
@@ -15,19 +17,19 @@ class LensesFileGenerator(
1517

1618
private fun buildLenses(elements: Collection<AnnotatedLens.Element>): List<KotlinFile> = elements.map(this::processElement)
1719
.map { (name, funs) ->
18-
funs.fold(KotlinFile.builder("kategory.optics", "optics.kategory.lens.$name").skipJavaLangImports(true), { builder, lensSpec ->
20+
funs.fold(KotlinFile.builder(name.packageName(), "${name.simpleName().toLowerCase()}.lenses").skipJavaLangImports(true), { builder, lensSpec ->
1921
builder.addFun(lensSpec)
2022
}).build()
2123
}
2224

23-
private fun processElement(annotatedLens: AnnotatedLens.Element): Pair<String, List<FunSpec>> =
24-
annotatedLens.type.simpleName.toString().toLowerCase() to annotatedLens.properties.map { variable ->
25+
private fun processElement(annotatedLens: AnnotatedLens.Element): Pair<ClassName, List<FunSpec>> =
26+
annotatedLens.type.asClassName() to annotatedLens.properties.map { variable ->
2527
val className = annotatedLens.type.simpleName.toString().toLowerCase()
2628
val variableName = variable.simpleName
2729

2830
FunSpec.builder("$className${variableName.toString().capitalize()}")
2931
.addStatement(
30-
"""return Lens(
32+
"""return kategory.optics.Lens(
3133
| get = { $className: %T -> $className.$variableName },
3234
| set = { $variableName: %T ->
3335
| { $className: %T ->

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

+45-19
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,80 @@ import kategory.common.utils.knownError
66
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
77
import me.eugeniomarletti.kotlin.metadata.isDataClass
88
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
9+
import me.eugeniomarletti.kotlin.metadata.modality
10+
import org.jetbrains.kotlin.serialization.ProtoBuf
911
import java.io.File
1012
import javax.annotation.processing.Processor
1113
import javax.annotation.processing.RoundEnvironment
1214
import javax.lang.model.SourceVersion
1315
import javax.lang.model.element.Element
16+
import javax.lang.model.element.ElementKind
17+
import javax.lang.model.element.ExecutableElement
1418
import javax.lang.model.element.TypeElement
15-
import javax.lang.model.element.VariableElement
16-
import javax.lang.model.type.TypeKind
1719

1820
@AutoService(Processor::class)
1921
class OptikalProcessor : AbstractProcessor() {
2022

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

25+
private val annotatedPrisms = mutableListOf<AnnotatedPrism.Element>()
26+
2327
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
2428

25-
override fun getSupportedAnnotationTypes() = setOf(lensesAnnotationClass.canonicalName)
29+
override fun getSupportedAnnotationTypes() = setOf(
30+
lensesAnnotationClass.canonicalName,
31+
prismsAnnotationClass.canonicalName
32+
)
2633

2734
override fun onProcess(annotations: Set<TypeElement>, roundEnv: RoundEnvironment) {
2835
annotatedLenses += roundEnv
2936
.getElementsAnnotatedWith(lensesAnnotationClass)
3037
.map(this::evalAnnotatedElement)
31-
.map { annotatedLens ->
32-
when (annotatedLens) {
33-
is AnnotatedLens.InvalidElement -> knownError(annotatedLens.reason)
34-
is AnnotatedLens.Element -> annotatedLens
35-
}
36-
}
38+
39+
annotatedPrisms += roundEnv
40+
.getElementsAnnotatedWith(prismsAnnotationClass)
41+
.map(this::evalAnnotatedPrismElement)
3742

3843
if (roundEnv.processingOver()) {
3944
val generatedDir = File(this.generatedDir!!, "").also { it.mkdirs() }
4045
LensesFileGenerator(annotatedLenses, generatedDir).generate()
46+
PrismsFileGenerator(annotatedPrisms, generatedDir).generate()
4147
}
4248
}
4349

44-
fun evalAnnotatedElement(element: Element): AnnotatedLens = when {
45-
element.kotlinMetadata !is KotlinClassMetadata -> AnnotatedLens.InvalidElement("""
46-
|Cannot use @Lenses on ${element.enclosingElement}.${element.simpleName}.
47-
|It can only be used on data classes.""".trimMargin())
48-
49-
(element.kotlinMetadata as KotlinClassMetadata).data.classProto.isDataClass ->
50+
private fun evalAnnotatedElement(element: Element): AnnotatedLens.Element = when {
51+
element.let { it.kotlinMetadata as? KotlinClassMetadata }?.data?.classProto?.isDataClass == true ->
5052
AnnotatedLens.Element(
5153
element as TypeElement,
52-
element.enclosedElements
53-
.filter { it.asType().kind == TypeKind.DECLARED }
54-
.map { it as VariableElement })
54+
element.enclosedElements.firstOrNull { it.kind == ElementKind.CONSTRUCTOR }
55+
?.let { it as ExecutableElement }
56+
?.parameters ?: emptyList()
57+
)
5558

56-
else -> AnnotatedLens.InvalidElement("${element.enclosingElement}.${element.simpleName} cannot be annotated with @Lenses")
59+
else -> knownError(opticsAnnotationError(element, lensesAnnotationName, lensesAnnotationTarget))
5760
}
5861

62+
private fun evalAnnotatedPrismElement(element: Element): AnnotatedPrism.Element = when {
63+
element.let { it.kotlinMetadata as? KotlinClassMetadata }?.data?.classProto?.isSealed == true -> {
64+
val (nameResolver, classProto) = element.kotlinMetadata.let { it as KotlinClassMetadata }.data
65+
66+
AnnotatedPrism.Element(
67+
element as TypeElement,
68+
classProto.sealedSubclassFqNameList
69+
.map(nameResolver::getString)
70+
.map { it.replace('/', '.') }
71+
.mapNotNull(elementUtils::getTypeElement)
72+
)
73+
}
74+
75+
else -> knownError(opticsAnnotationError(element, prismsAnnotationName, prismsAnnotationTarget))
76+
}
77+
78+
private fun opticsAnnotationError(element: Element, annotationName: String, targetName: String): String = """
79+
|Cannot use $annotationName on ${element.enclosingElement}.${element.simpleName}.
80+
|It can only be used on $targetName.""".trimMargin()
81+
82+
private val ProtoBuf.Class.isSealed
83+
get() = modality == ProtoBuf.Modality.SEALED
84+
5985
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package kategory.optics
2+
3+
import com.squareup.kotlinpoet.ClassName
4+
import com.squareup.kotlinpoet.FunSpec
5+
import com.squareup.kotlinpoet.KotlinFile
6+
import com.squareup.kotlinpoet.asClassName
7+
import java.io.File
8+
9+
class PrismsFileGenerator(
10+
private val annotatedList: Collection<AnnotatedPrism.Element>,
11+
private val generatedDir: File
12+
) {
13+
14+
fun generate() = buildPrisms(annotatedList).forEach {
15+
it.writeTo(generatedDir)
16+
}
17+
18+
private fun buildPrisms(elements: Collection<AnnotatedPrism.Element>): List<KotlinFile> = elements.map(this::processElement)
19+
.map { (name, funs) ->
20+
funs.fold(KotlinFile.builder(name.packageName(), "${name.simpleName().toLowerCase()}.prisms").skipJavaLangImports(true), { builder, prismSpec ->
21+
builder.addFun(prismSpec)
22+
}).addStaticImport("kategory", "right", "left").build()
23+
}
24+
25+
private fun processElement(annotatedPrism: AnnotatedPrism.Element): Pair<ClassName, List<FunSpec>> =
26+
annotatedPrism.type.asClassName() to annotatedPrism.subTypes.map { subClass ->
27+
val sealedClassName = annotatedPrism.type.simpleName.toString().toLowerCase()
28+
val subTypeName = subClass.simpleName.toString()
29+
30+
FunSpec.builder("$sealedClassName$subTypeName")
31+
.addStatement(
32+
"""return kategory.optics.Prism(
33+
| getOrModify = { $sealedClassName: %T ->
34+
| when ($sealedClassName) {
35+
| is %T -> $sealedClassName.right()
36+
| else -> $sealedClassName.left()
37+
| }
38+
| },
39+
| reverseGet = { it }
40+
|)""".trimMargin(), annotatedPrism.type, subClass)
41+
.build()
42+
}
43+
44+
}

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ import kotlin.annotation.AnnotationTarget.CLASS
55

66
@Retention(SOURCE)
77
@Target(CLASS)
8-
annotation class lenses
8+
annotation class lenses
9+
10+
@Retention(SOURCE)
11+
@Target(CLASS)
12+
annotation class prisms

kategory-optics/build.gradle

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
dependencies {
22
compile project(':kategory-core')
3-
compile project(':kategory-test')
43
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
54
testCompile "io.kotlintest:kotlintest:$kotlinTestVersion"
65
testCompile project(':kategory-test')

kategory-optics/src/main/kotlin/kategory/optics/Lens.kt

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import kategory.HK
66
import kategory.Option
77
import kategory.Tuple2
88
import kategory.functor
9+
import kategory.identity
910
import kategory.toT
1011

1112
/**
@@ -25,6 +26,12 @@ abstract class Lens<A, B> {
2526
abstract fun set(b: B): (A) -> A
2627

2728
companion object {
29+
30+
fun <A> codiagonal() = Lens<Either<A, A>, A>(
31+
get = { it.fold(::identity, ::identity) },
32+
set = { a -> { it.bimap({ a }, { a }) } }
33+
)
34+
2835
operator fun <A, B> invoke(get: (A) -> B, set: (B) -> (A) -> A) = object : Lens<A, B>() {
2936
override fun get(a: A): B = get(a)
3037

kategory-optics/src/main/kotlin/kategory/optics/Prism.kt

+28-8
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package kategory.optics
22

33
import kategory.Applicative
44
import kategory.Either
5+
import kategory.Eq
56
import kategory.HK
67
import kategory.Option
78
import kategory.Tuple2
9+
import kategory.compose
10+
import kategory.eq
811
import kategory.flatMap
912
import kategory.identity
1013
import kategory.left
@@ -31,11 +34,20 @@ abstract class Prism<A, B> {
3134
abstract fun reverseGet(b: B): A
3235

3336
companion object {
34-
operator fun <A,B> invoke(getOrModify: (A) -> Either<A, B>, reverseGet: (B) -> A) = object : Prism<A,B>() {
37+
operator fun <A, B> invoke(getOrModify: (A) -> Either<A, B>, reverseGet: (B) -> A) = object : Prism<A, B>() {
3538
override fun getOrModify(a: A): Either<A, B> = getOrModify(a)
3639

3740
override fun reverseGet(b: B): A = reverseGet(b)
3841
}
42+
43+
/**
44+
* a [Prism] that checks for equality with a given value
45+
*/
46+
inline fun <reified A> only(a: A, EQA: Eq<A> = eq()) = Prism<A, Unit>(
47+
getOrModify = { a2 -> (if (EQA.eqv(a, a2)) a.left() else Unit.right()) },
48+
reverseGet = { a }
49+
)
50+
3951
}
4052

4153
/**
@@ -68,11 +80,6 @@ abstract class Prism<A, B> {
6880
*/
6981
fun set(b: B): (A) -> A = modify { b }
7082

71-
infix fun <C> composePrism(other: Prism<B, C>): Prism<A, C> = Prism(
72-
{ a -> getOrModify(a).flatMap { b: B -> other.getOrModify(b).bimap({ set(it)(a) }, ::identity) } },
73-
{ reverseGet(other.reverseGet(it)) }
74-
)
75-
7683
/**
7784
* Set the target of a [Prism] with a value
7885
*/
@@ -81,12 +88,12 @@ abstract class Prism<A, B> {
8188
/**
8289
* Check if there is a target
8390
*/
84-
fun isNotEmpty(a: A): Boolean = getOption(a).isDefined
91+
fun nonEmpty(a: A): Boolean = getOption(a).nonEmpty
8592

8693
/**
8794
* Check if there is no target
8895
*/
89-
fun isEmpty(a: A): Boolean = !isNotEmpty(a)
96+
fun isEmpty(a: A): Boolean = !nonEmpty(a)
9097

9198
/**
9299
* Find if the target satisfies the predicate
@@ -119,6 +126,19 @@ abstract class Prism<A, B> {
119126
{ (c, b) -> c toT reverseGet(b) }
120127
)
121128

129+
/**
130+
* Compose a [Prism] with another [Prism]
131+
*/
132+
infix fun <C> composePrism(other: Prism<B, C>): Prism<A, C> = Prism(
133+
{ a -> getOrModify(a).flatMap { b: B -> other.getOrModify(b).bimap({ set(it)(a) }, ::identity) } },
134+
this::reverseGet compose other::reverseGet
135+
)
136+
137+
/**
138+
* Plus operator overload to compose lenses
139+
*/
140+
operator fun <C> plus(other: Prism<B, C>): Prism<A, C> = composePrism(other)
141+
122142
}
123143

124144
/**

kategory-optics/src/test/kotlin/kategory/optics/LensTest.kt

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import io.kotlintest.KTestJUnitRunner
44
import io.kotlintest.properties.Gen
55
import io.kotlintest.properties.forAll
66
import kategory.Eq
7+
import kategory.LensLaws
78
import kategory.Option
89
import kategory.Tuple2
910
import kategory.UnitSpec

kategory-optics/src/test/kotlin/kategory/optics/PrismTest.kt

+12-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import io.kotlintest.properties.forAll
66
import kategory.Eq
77
import kategory.NonEmptyList
88
import kategory.Option
9+
import kategory.PrismLaws
910
import kategory.Try
1011
import kategory.UnitSpec
1112
import kategory.applicative
@@ -96,7 +97,16 @@ class PrismTest : UnitSpec() {
9697

9798
"Joining two prisms together with same target should yield same result" {
9899
forAll(SumGen, { a ->
99-
(sumPrism composePrism stringPrism).getOption(a) == sumPrism.getOption(a).flatMap(stringPrism::getOption)
100+
(sumPrism composePrism stringPrism).getOption(a) == sumPrism.getOption(a).flatMap(stringPrism::getOption) &&
101+
(sumPrism + stringPrism).getOption(a) == (sumPrism composePrism stringPrism).getOption(a)
102+
})
103+
}
104+
105+
"Checking if a prism exists with a target" {
106+
forAll(SumGen, Gen.bool(), { a, bool ->
107+
Prism.only(a, object : Eq<SumType> {
108+
override fun eqv(a: SumType, b: SumType): Boolean = bool
109+
}).isEmpty(a) == bool
100110
})
101111
}
102112

@@ -108,7 +118,7 @@ class PrismTest : UnitSpec() {
108118

109119
"Checking if a target exists" {
110120
forAll(SumGen, { sum ->
111-
sumPrism.isNotEmpty(sum) == sum is SumType.A
121+
sumPrism.nonEmpty(sum) == sum is SumType.A
112122
})
113123
}
114124

kategory-test/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
dependencies {
22
compile project(':kategory-core')
3+
compile project(':kategory-optics')
34
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
45
compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion"
56
compile "io.kotlintest:kotlintest:$kotlinTestVersion"

kategory-optics/src/test/kotlin/kategory/optics/LensLaws.kt kategory-test/src/main/kotlin/kategory/laws/LensLaws.kt

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
package kategory.optics
1+
package kategory
22

33
import io.kotlintest.properties.Gen
44
import io.kotlintest.properties.forAll
5-
import kategory.Applicative
6-
import kategory.Eq
7-
import kategory.Law
8-
import kategory.compose
9-
import kategory.exists
10-
import kategory.identity
5+
import kategory.optics.Lens
116

127
object LensLaws {
138

0 commit comments

Comments
 (0)